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:

  1. Computes the elapsed time (delta).
  2. Advances the physics engine using Engine.update(engine, delta).
  3. 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: