javascriptreactjsmathgame-physicsmatter.js

How can I calculate velocity to jump to the target with MatterJS?


I'm trying to create a ball in matter-js that will bounce off rectangles and jump to the next rectangle. All I can know is ball's position and next rectangle's position. I found a similar question at unity forum. I tried to implement code from Iron-Warrior's solution in my project, but I have problem that my ball constantly jumps strongly over the next rectangle.

Here is the code that i got:

const ball = Bodies.circle(0, 0, 10);

const rectangles = Array(20)
  .fill(0)
  .map((_, i) => Bodies.rectangle(i * 140, 200, 20, 20, { isStatic: true }));

Events.on(engine, 'collisionStart', (event) => {
  event.pairs.forEach((pair) => {
    if (pair.bodyA === ball) {
      const target = rectangles[rectangles.indexOf(pair.bodyB) + 1];

      const gravity = engine.gravity.y;
      const initialAngle = 60;
      const yOffset = ball.position.y - target.position.y;

      const angle = initialAngle * (Math.PI / 180);

      const distance = Vector.magnitude(Vector.sub(ball.position, target.position));

      const initialVelocity =
        (1 / Math.cos(angle)) *
        Math.sqrt(
          (0.5 * gravity * Math.pow(distance, 2)) / (distance * Math.tan(angle) + yOffset),
        );

      const velocity = {
        x: initialVelocity * Math.cos(angle),
        y: -initialVelocity * Math.sin(angle),
      };

      const angleBetweenObjects = Vector.angle(ball.position, target.position);
      const finalVelocity = Vector.rotate(velocity, angleBetweenObjects);
  
      Body.setVelocity(ball, finalVelocity);        
    }
  });
});

Composite.add(engine.world, [ball, ...rectangles]);

Tried to increase engine.velocityIterations

Here is codesandbox example: https://codesandbox.io/p/sandbox/relaxed-shape-9wy3kt

I believe this is mostly matter-js problem. I tried to change formula, change ball options, rectangle options, nothing helps. Maybe there is some alternative engine to matterjs?


Solution

  • Finally cracked it! The main difficulty was to see the fact that if you setVelocity in the collisionStart, the effect of the collision is still applied to the ball, so its set velocity was altered, hence the unpredictability of its trajectory. Thus, the main change is to move the action to collisionActive handler.

    There are also some minor additions and corrections to be made to your code:

    Here it goes in a code snippet, with some random positioning of the rectangles and a simple reverse of the direction when the ball reaches the last/first rectangle:

    const Engine = Matter.Engine,
        Render = Matter.Render,
        Runner = Matter.Runner,
        Bodies = Matter.Bodies,
        Composite = Matter.Composite,
        Vector = Matter.Vector,
        Body = Matter.Body,
        World = Matter.World,
        Events = Matter.Events;
    
    // create an engine
    const engine = Engine.create();
    //engine.positionIterations = 200;
    //engine.velocityIterations = 200;
    engine.enableSleeping = true;
    
    const width = 800, height = 300;
    
    // create a renderer
    const render = Render.create({
        element: document.body,
        engine: engine,
        options: {
            width,
            height,
            showAngleIndicator: true,
            showVelocity: true,
        },
    });
    
    const ball = Bodies.circle(0, 0, 10);
    ball.frictionAir = 0;
    
    let reverse = false;
    
    const intRnd = (min, max) => Math.floor(min + Math.random() * (max - min));
    const rectangles = Array(60)
        .fill(0)
        .map((_, i) =>
            Bodies.rectangle(i * 150 + (i === 0 ? 0 : intRnd(-30, 30)), 150 + intRnd(-50, 50), 30, 30, {isStatic: true}),
        );
    
    Events.on(engine, "collisionActive", (event) => {
        event.pairs.forEach((pair) => {
            if (pair.bodyA === ball) {
                let target = rectangles[rectangles.indexOf(pair.bodyB) + (reverse ? -1 : 1)];
                if(!target){
                    reverse = !reverse;
                    target = rectangles[rectangles.indexOf(pair.bodyB) + (reverse ? -1 : 1)];
                }
    
                const gravity = engine.gravity.y * engine.gravity.scale * Body._baseDelta * Body._baseDelta;
                const initialAngle = reverse ? -60 : 60;
                const angle = deg2rad(initialAngle);
                const dx = target.position.x - ball.position.x,
                    dy = target.bounds.min.y - ball.position.y - ball.circleRadius;
                const initialVelocity = dx / Math.cos(angle) * Math.sqrt((0.5 * gravity) / (dy + dx * Math.tan(angle)));
                if (initialVelocity) {
                    const velocity = Vector.create(
                        initialVelocity * Math.cos(angle),
                        -initialVelocity * Math.sin(angle),
                    );
                    Body.setVelocity(ball, velocity);
                }
                else{
                    Runner.stop(runner)
                }
                Body.setAngularVelocity(ball, 0);
            }
        });
    });
    
    
    Composite.add(engine.world, [ball, ...rectangles]);
    
    
    function deg2rad(degrees) {
        return degrees * (Math.PI / 180);
    }
    
    // create runner
    const runner = Runner.create();
    
    const updateCameraPosition = () => {
        // Calculate camera position based on ball's position - just x
    
        if (ball.position.y > height * 0.9) {
            stop();
        }
        const xOffset = ball.position.x - width / 2;
        Render.lookAt(render, {
            min: {x: xOffset, y: 0},
            max: {
                x: xOffset + width,
                y: height,
            },
        });
    };
    
    Events.on(engine, "afterUpdate", () => {
        updateCameraPosition();
    });
    
    const stop = function(){
        Render.stop(render);
        World.clear(engine.world, false);
        Engine.clear(engine);
    
        document.querySelector('#stop').style.visibility = 'hidden';
    }
    
    // run the renderer
    Render.run(render);
    // run the engine
    Runner.run(runner, engine);
    // setTimeout(() => {
    //     stop()
    // }, 300000) // stop after 5min
    *{
       margin: 0
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.js" integrity="sha512-rsntMCBgWYEWKl4HtqWmQ3vdHgvSq8CTtJd19YL7lCtKokLPWt7UEoHVabK1uiNfUdaLit8O090no5BZjcz+bw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <button id='stop' onclick="stop()" style="position:absolute">Stop</button>

    or a jsFiddle.