How to Create a Custom Matter.js Render Loop
This article provides a step-by-step guide to replacing the default
Matter.js renderer with a custom rendering pipeline. While the built-in
Matter.Render module is convenient for prototyping,
building production games or integrating visual libraries like Pixi.js,
Three.js, or raw HTML5 Canvas requires manual control over the update
cycle. You will learn how to initialize the physics engine
independently, drive state updates using
requestAnimationFrame, and render physics bodies to an
HTML5 Canvas context.
1. Decoupling Engine and Renderer
By default, Matter.js pairs Matter.Render with
Matter.Runner to automatically step the physics simulation
and draw it to a canvas. To take full control, omit both modules
entirely. You only need Matter.Engine,
Matter.Bodies, and Matter.Composite.
const { Engine, Bodies, Composite } = Matter;
// Create an engine instance
const engine = Engine.create();
// Add bodies to the world
const box = Bodies.rectangle(400, 200, 80, 80);
const ground = Bodies.rectangle(400, 580, 810, 40, { isStatic: true });
Composite.add(engine.world, [box, ground]);2. Setting Up the Target Canvas
Create an HTML canvas element and acquire its 2D rendering context.
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
canvas.width = 800;
canvas.height = 600;3. Writing the Game Loop
A custom loop relies on the browser's
requestAnimationFrame API. The loop performs three primary
tasks on every frame:
- Computes the elapsed time (
delta). - Advances the physics engine using
Engine.update(engine, delta). - Clears the screen and draws every body using its updated coordinates.
let lastTime = performance.now();
function loop(currentTime) {
const delta = currentTime - lastTime;
lastTime = currentTime;
// 1. Advance the physics simulation
Engine.update(engine, delta);
// 2. Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 3. Render all bodies in the world
const bodies = Composite.allBodies(engine.world);
bodies.forEach(body => {
const vertices = body.vertices;
ctx.beginPath();
ctx.moveTo(vertices[0].x, vertices[0].y);
for (let i = 1; i < vertices.length; i += 1) {
ctx.lineTo(vertices[i].x, vertices[i].y);
}
ctx.closePath();
ctx.fillStyle = body.isStatic ? '#888' : '#2ecc71';
ctx.fill();
ctx.lineWidth = 1;
ctx.strokeStyle = '#27ae60';
ctx.stroke();
});
// Request the next frame
requestAnimationFrame(loop);
}
// Start the loop
requestAnimationFrame(loop);4. Handling Variable vs. Fixed Timesteps
Engine.update(engine, delta) supports a dynamic
timestep, but variable frame rates can cause non-deterministic physics
behavior or tunneling at low frame rates. To maintain stable simulation,
clamp the maximum delta value or step the engine using a fixed interval
(e.g., 16.66ms for 60Hz):
const fixedDelta = 1000 / 60;
function loop(currentTime) {
// Run fixed updates to prevent instability on frame drops
Engine.update(engine, fixedDelta);
ctx.clearRect(0, 0, canvas.width, canvas.height);
renderBodies(Composite.allBodies(engine.world));
requestAnimationFrame(loop);
}5. Benefits of Custom Rendering
Bypassing Matter.Render enables:
- Sprite and Texture Mapping: Translate and rotate
images using
body.position.x,body.position.y, andbody.angle. - Third-Party Integrations: Bind Matter.js physics coordinates directly to WebGL frameworks (Pixi.js, Three.js, Babylon.js).
- Layering and Post-Processing: Render custom UI, background layers, lighting passes, or particle systems alongside physics objects without engine constraints.