I am working on a top-down physics based game set in space. I would like the rotate to the view to always show the player's ship facing up even though the ship can rotate. I've searched through the docs, but didn't find anything about rotating the world or renderer, but it's possible that I don't know the right terminology to look for. Is this even possible with matter.js?
For starters, according to the docs the MJS renderer is "mostly intended for development and debugging purposes". As such, for complex rendering involving canvas transformations like this, I'd use some dedicated renderer appropriate for your project such as the DOM, HTML5 canvas or p5.js. Regardless of which one you choose, the procedure is mostly the same: run MJS headlessly as a physics engine and extract body positions per frame and render them how you like.
The architecture is like this:
[asynchronous DOM events] [library calls to MJS]
| |
| |
| +-----------------------------+
| |
v v
.-----------. .-----------.
| matter.js |---[body positions]-->| rendering |
| engine | [ per frame ] | engine |
`-----------` `-----------`
Since MJS handles physics but doesn't know or care how you choose to render its bodies when run headlessly, the viewport concept is basically an unrelated, mostly decoupled module--whether you show the entire map on screen or a tiny, rotated portion of it has no bearing on MJS, with at least two caveats that are out of scope for this proof-of-concept thread:
- If you have input that relies on x/y coordinates, you'll need to ensure that events going into MJS match its understanding of the world.
- If your world is large, you may want to cull physics and rendering updates to improve performance.
For this post, I'll use HTML5 canvas and show how to integrate MJS into a rotating, player-centered viewport from the canonical thread HTML5 Canvas camera/viewport - how to actually do it?. I recommend reading this post before going further regardless of whether you're using HTML5 canvas or not--the underlying viewport math is the same.
Next, let's look at running Matter.js headlessly using canvas as a rendering front-end. A minimal example is:
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
canvas.width = canvas.height = 180;
const engine = Matter.Engine.create();
const size = 50;
const bodies = [
Matter.Bodies.rectangle(
canvas.width / 2, 0, size, size
),
Matter.Bodies.rectangle(
canvas.width / 2, 120,
size, size, {isStatic: true}
),
];
const mouseConstraint = Matter.MouseConstraint.create(
engine, {element: canvas}
);
Matter.Composite.add(engine.world, [...bodies, mouseConstraint]);
(function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
bodies.forEach((e, i) => {
const {x, y} = e.position;
ctx.save();
ctx.translate(x, y);
ctx.rotate(e.angle);
ctx.fillStyle = `rgb(${i * 200}, 100, 100)`;
ctx.fillRect(size / -2, size / -2, size, size);
ctx.restore();
});
Matter.Engine.update(engine);
requestAnimationFrame(render);
})();
canvas {
border: 4px solid black;
background: #eee;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.20.0/matter.min.js"></script>
<canvas></canvas>
Note that MJS teats x/y coordinates as the center of rectangles by default whereas canvas uses the top-left. ctx.fillRect(size / -2, size / -2, size, size);
is a typical normalization step needed to ensure canvas and MJS are in sync. Matter.Engine.update(engine);
is used to step the engine forward a tick.
Armed with these examples, we (just) need to hook them together. In the following example, all of the MJS code is pretty much standard-issue. The rest of the code is geared towards setting up state, running the update loop and drawing the MJS bodies to canvas in the correct positions.
const rnd = Math.random;
const canvas = document.createElement("canvas");
document.body.appendChild(canvas);
const ctx = canvas.getContext("2d");
canvas.height = canvas.width = 180;
const map = {height: 1000, width: 1000};
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.02, density: 0.3}
),
size: 20,
color: "#eee",
accelForce: 0.03,
rotationAmt: 0.03,
rotationAngVel: 0.01,
accelerate() {
Matter.Body.applyForce(
this.body,
this.body.position,
{
x: Math.cos(this.body.angle) * this.accelForce,
y: Math.sin(this.body.angle) * this.accelForce
}
);
},
decelerate() {
Matter.Body.applyForce(
this.body,
this.body.position,
{
x: Math.cos(this.body.angle) * -this.accelForce,
y: Math.sin(this.body.angle) * -this.accelForce
}
);
},
rotateLeft() {
Matter.Body.rotate(this.body, -this.rotationAmt);
Matter.Body.setAngularVelocity(
this.body, -this.rotationAngVel
);
},
rotateRight() {
Matter.Body.rotate(this.body, this.rotationAmt);
Matter.Body.setAngularVelocity(
this.body, this.rotationAngVel
);
},
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();
},
};
const obsSize = 50;
const makeObstacle = () => ({
body: (() => {
const body = Matter.Bodies.fromVertices(
obsSize + rnd() * (map.width - obsSize * 2),
obsSize + rnd() * (map.height - obsSize * 2),
[...Array(3)].map(() => ({
x: rnd() * obsSize,
y: rnd() * obsSize
})),
{frictionAir: 0.02}
);
Matter.Body.rotate(body, rnd() * Math.PI * 2);
return body;
})(),
color: `hsl(${Math.random() * 30 + 200}, 80%, 70%)`,
});
const obstacles = [
...[...Array(100)].map(makeObstacle),
{
body: Matter.Bodies.rectangle(
-10, map.height / 2,
20, map.height, {isStatic: true}
),
color: "#333",
},
{
body: Matter.Bodies.rectangle(
map.width / 2, -10,
map.width, 20, {isStatic: true}
),
color: "#333",
},
{
body: Matter.Bodies.rectangle(
map.width / 2, map.height + 10,
map.width, 20, {isStatic: true}
),
color: "#333",
},
{
body: Matter.Bodies.rectangle(
map.width + 10, map.height / 2,
20, map.width, {isStatic: true}
),
color: "#333",
},
];
Matter.Composite.add(engine.world, [
ship.body, ...obstacles.map(e => e.body),
]);
const keyCodesToActions = {
ArrowUp: () => ship.accelerate(),
ArrowLeft: () => ship.rotateLeft(),
ArrowRight: () => ship.rotateRight(),
ArrowDown: () => ship.decelerate(),
};
const validKeys = new Set(
Object.keys(keyCodesToActions)
);
const keysPressed = new Set();
document.addEventListener("keydown", e => {
if (validKeys.has(e.code)) {
e.preventDefault();
keysPressed.add(e.code);
}
});
document.addEventListener("keyup", e => {
if (validKeys.has(e.code)) {
e.preventDefault();
keysPressed.delete(e.code);
}
});
(function update() {
requestAnimationFrame(update);
keysPressed.forEach(k => {
if (k in keyCodesToActions) {
keyCodesToActions[k]();
}
});
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(canvas.width / 2, canvas.height / 1.4);
// ^^^ optionally offset y a bit
// so the player can see better
ctx.rotate(-90 * Math.PI / 180 - ship.body.angle);
ctx.translate(-ship.body.position.x, -ship.body.position.y);
/* draw everything as normal */
const tileSize = 50;
for (let x = 0; x < map.width; x += tileSize) {
for (let y = 0; y < map.height; y += tileSize) {
// simple culling
if (x > ship.x + canvas.width || y > ship.y + canvas.height ||
x < ship.x - canvas.width || y < ship.y - canvas.height) {
continue;
}
const light = ((x / tileSize + y / tileSize) & 1) * 5 + 70;
ctx.fillStyle = `hsl(${360 - (x + y) / 10}, 50%, ${light}%)`;
ctx.fillRect(x, y, tileSize + 1, tileSize + 1);
}
}
obstacles.forEach(({body: {vertices}, color}) => {
ctx.beginPath();
ctx.fillStyle = color;
ctx.strokeStyle = "#000";
vertices.forEach(({x, y}) => ctx.lineTo(x, y));
ctx.lineWidth = 5;
ctx.closePath();
ctx.stroke();
ctx.fill();
});
ship.draw(ctx);
ctx.restore();
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;
}
div {
transform: rotate(-90deg);
background: #222;
color: #fff;
padding: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.20.0/matter.min.js"></script>
<div>arrow keys to move</div>
As seen in the above code, the following pattern is typical for rendering n-sided MJS bodies:
ctx.beginPath();
vertices.forEach(({x, y}) => ctx.lineTo(x, y));
ctx.closePath();
ctx.fill();
Additionally, this post shows a variety of techniques for creating top-down games. engine.gravity.y = 0;
was used here as well to disable gravity globally. The linked post discusses Matter.Body.applyForce
(covered in depth in this thread) and rotation; I did it slightly differently here with a combination of Matter.Body.rotate
and Matter.Body.setAngularVelocity
but this is use-case specific and pretty much immaterial to the viewport.
I'm not sure how to do this for the built in renderer. I used a custom renderer and I used canvas transformations to move the camera around.
http://www.w3schools.com/tags/canvas_rotate.asp
ctx.save();
ctx.translate(transX, transY);
drawBody();
ctx.restore();
Alternatively, add code to the Render.startViewTransform method:
// OVERLOAD THIS METHOD
Matter.Render.startViewTransform = function(render) {
var boundsWidth = render.bounds.max.x - render.bounds.min.x,
boundsHeight = render.bounds.max.y - render.bounds.min.y,
boundsScaleX = boundsWidth / render.options.width,
boundsScaleY = boundsHeight / render.options.height;
// add lines:
var w2 = render.canvas.width / 2;
var h2 = render.canvas.height / 2;
render.context.translate(w2, h2);
render.context.rotate(angle_target);
render.context.translate(-w2, -h2);
// /add lines.
render.context.scale(1 / boundsScaleX, 1 / boundsScaleY);
render.context.translate(-render.bounds.min.x, -render.bounds.min.y);
};
But you still need to override the calculation of render.bounds, which now always considers a rectangular area for angle = 0!
You can rotate the canvas in the HTML with element.style.transform = 'rotate('+rotation+')'
, as long as the boat stays in the center of the screen. I know this is similar to lilgreenland's answer, but this way, you don't have to use a custom renderer and can just use a function updating with requestAnimationFrame
.
© 2022 - 2024 — McMap. All rights reserved.