2

How can I move a circular body to another position with an animation? When I use translate(), the object disappears and reappears instantly at the new position.

I'm developing simple game. The player has to avoid incoming obstacles from the top. They can only move to left or right at three fields. I want to animate the motion on these fields. Currently, I have something like this:

public move = (entities, {touches, time}) => {
  touches.filter(t => t.type === 'press').forEach(t => {
    const player = entities['player']
    const direction = this.getPlayerMoveDirection(t.event.pageX)
    const nextFieldId = this.getNextFieldIdByDirection(direction)
    if (nextFieldId !== this.fieldId) {
      this.setFieldId(nextFieldId)
      const nextField = this.game.fields[nextFieldId]
      const nextFieldXPosition = nextField.getCenter()
      const newXPosition = direction === 'left' ? (player.body.position.x - nextFieldXPosition) *-1 : nextFieldXPosition - player.body.position.x
      Matter.Body.translate( player.body, {x: newXPosition, y:  0});
      this.playMoveSound()
    }
  })
  return entities
}

This works very well but the sprite is not animated. It just appears at the new position. I want to animate its movement.

I'm using Matter.js in react-native with react-native-game-engine.

ggorlen
  • 44,755
  • 7
  • 76
  • 106
Skodsgn
  • 940
  • 1
  • 10
  • 33
  • 2
    [Apply force to the body](https://stackoverflow.com/questions/35827012/matter-js-calculating-force-needed) rather than simply translating it to the new position. This code snippet out of context isn't really conducive to offering working code in response, so I recommend sharing a runnable [mcve]. Then again, Matter.js might be overkill for simple physics and collision like this. See [this project](https://codepen.io/ggorlen/pen/GvJvxq) which is just vanilla JS. – ggorlen May 23 '21 at 05:10
  • 1
    But how stop player on fixed position using Force? – Skodsgn May 24 '21 at 09:21
  • 1
    Whoops, I missed your comment. You could use friction, apply force in the opposite direction or maybe something a bit hacky like creating an invisible obstacle if you want a dead stop, I suppose -- it seems pretty dependent on the effect you want. Again, I'm not sure if Matter.js is appropriate here if you're just doing tweening left-right movement and circle/box-style collision. When you call `translate` and begin overriding MJS' physics engine, it's like buying a bike just to carry it. – ggorlen Jun 29 '21 at 16:24

1 Answers1

0

This is a good question--it's not entirely obvious how to achieve this in MJS as there is no goto(body, x, y) function.

There are at least a few different approaches available, with each approach involving many potentially fussy details and available tweaks. Which is best seems to depend heavily on your use case.

One option is to use Matter.Body.applyForce(body, position, force) to boost the body towards the destination. See Matter.js calculating force needed for the typical way to set up these calls. Given a destination, you could scale the force by the distance to the destination and apply it once. This would give a large force that diminishes as the body approaches its destination.

Another approach is to apply force gradually on a frame-by-frame basis. This is how the example below works. The catch is that it's a bit tricky to slow and stop the body as it approaches the destination. Without some care, it's easy to overshoot the target, creating a probably undesirable spring or rubber band effect. I checked the distance to determine when to slow the force as the target nears the destination. There is room for improvement here.

Either way, if you want to rotate the body to point in the direction it's headed, more work is needed. I used a hand-spun approach based on my answer in Rotate an object gradually to face a point? to figure out which way to turn, then Matter.Body.setAngularVelocity(body, velocity) to inform MJS of the turning velocity. As with everything else here, the numbers are pretty sensitive and took tweaking to get right, so treat them as a proof of concept.

Rather than applyForce, you could use Body.setPosition and Body.setVelocity calls to reposition the moving body by hand. This would probably be inappropriate for the common case, because it'd cause the body to barge through other bodies and ignore most of its relevant physical properties, but for certain use-cases, it might be appropriate.

Relevant to the question, you can skip applying force on the y-axis to constrain movement to the horizontal axis if desired (specifcally, set y: 0 in the example below in the call to applyForce).

Here's a proof of concept of a top-down game with turning and frame-by-frame forces that push the body toward the destination point.

const {PI} = Math;
const TAU = PI * 2;
const canvas = document.createElement("canvas");
document.body.appendChild(canvas);
const ctx = canvas.getContext("2d");
canvas.height = 180;
canvas.width = 400;

const engine = Matter.Engine.create();
engine.gravity.y = 0; // enable top-down
const ship = {
  body: Matter.Bodies.rectangle(
    canvas.width / 2,
    canvas.height / 2,
    20,
    20,
    {
      frictionAir: 0.03,
      density: 0.3,
      friction: 0.8,
    }
  ),
  size: 20,
  destX: canvas.width / 2,
  destY: canvas.height / 2,
  color: "#eaf",
  setDestination(x, y) {
    this.destX = x;
    this.destY = y;
  },
  draw(ctx) {
    ctx.save();
    ctx.translate(this.body.position.x, this.body.position.y);
    ctx.rotate(this.body.angle);
    ctx.lineWidth = 3;
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(this.size / 1.2, 0);
    ctx.stroke();
    ctx.fillStyle = this.color;
    ctx.fillRect(
      this.size / -2,
      this.size / -2,
      this.size,
      this.size
    );
    ctx.strokeRect(
      this.size / -2,
      this.size / -2,
      this.size,
      this.size
    );
    ctx.restore();
  },
};

Matter.Composite.add(engine.world, [ship.body]);

canvas.addEventListener("click", (e) => {
  ship.setDestination(e.offsetX, e.offsetY);
});

(function update() {
  requestAnimationFrame(update);
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  const {x, y} = ship.body.position;
  const dist = Math.sqrt(
    (ship.destX - x) ** 2 + (ship.destY - y) ** 2
  );

  if (dist > 10) {
    const dx = ship.destX - ship.body.position.x;
    const dy = ship.destY - ship.body.position.y;

    let theta = Math.atan2(dy, dx);
    const a =
      ship.body.angle > 0
        ? ((ship.body.angle + TAU) % TAU) - TAU
        : -(((-ship.body.angle + TAU) % TAU) - TAU);
    let diff = a - theta;
    diff = diff > PI ? diff - TAU : diff;
    diff = diff < -PI ? diff + TAU : diff;
    const f = dist < 70 ? Math.min(0.02, dist / 10000) : 0.03;
    Matter.Body.applyForce(
      ship.body,
      {x, y},
      {
        x: Math.cos(theta) * f,
        y: Math.sin(theta) * f,
      }
    );

    if (dist > 15) {
      Matter.Body.setAngularVelocity(ship.body, diff / -8);
    } else {
      Matter.Body.setAngularVelocity(ship.body, 0);
    }
  }

  ship.draw(ctx);
  Matter.Engine.update(engine);
})();
body { 
  margin: 0;
  font-family: monospace;
  display: flex; 
  align-items: center; 
}

html, body { 
  height: 100%; 
}

canvas { 
  background: #eee;
  margin: 1em; 
  border: 4px solid #222; 
}

#instructions {
  transform: rotate(-90deg);
  background: #222;
  color: #fff;
  padding: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>
<div id="instructions">click to move</div>
ggorlen
  • 44,755
  • 7
  • 76
  • 106