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>