Search code examples
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:

    • one has to take into consideration the sizes of the bodies, so the target shouldn't be the center of the rectangle, but its upper surface (target.bounds.min.y in this reference system with y increasing downwards) and the center of the ball should be +ball.circleRadius above that.
    • the formula for the initial velocity you were using is not exact, you should replace distance by its x component. I used variables dx and dy (instead of yOffset) for the two components of the distance, with which the formula of the initial velocity needed to throw the ball to the target (see for instance projectile motion) might be written as:
      dx = target.position.x - ball.position.x
      dy = target.bounds.min.y - ball.position.y - ball.circleRadius
      initialVelocity = dx / Math.cos(angle) * 
          Math.sqrt((0.5 * gravity) / (dy + dx * Math.tan(angle)));
      velocity = Vector.create(
          initialVelocity * Math.cos(angle),  // vx
          -initialVelocity * Math.sin(angle)  // vy
      );
      
    • the value of the gravitational acceleration in matter.js is actually engine.gravity.y * engine.gravity.scale; that needs to be multiplied by the factor Body._baseDelta ** 2 where Body._baseDelta is the basic time unit of the engine. I derived this correction factor by looking at the source code of matter.js and it's a peculiarity (not to say a bug) of this library.
    • the air friction should be canceled ball.airFriction = 0; while taking into consideration the air friction is theoretically feasible, that is a much more difficult proposition.
    • the angular velocity of the ball should also be set to zero Body.setAngularVelocity(ball, 0) -- although the spin of the ball does not affect its energy, there is a funny effect of the rotation, that the ball although touches the rectangle in the right position there is no collision and the ball somehow slides off the surface.

    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
    <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()">Stop</button>

    or a jsFiddle.