Manual Physics Steps in Matter.js

Turn-based simulations require strict control over physics calculations rather than relying on a continuous, real-time animation loop. This guide explains how to disable the default runner in Matter.js and manually advance the physics engine using the Engine.update method. By decoupling the physics calculation from the browser's refresh rate, you can execute discrete, deterministic physics steps, fast-forward actions, and synchronize your rendering strictly when a player executes a turn.

1. Avoid Using Matter.Runner

By default, Matter.js applications use Matter.Runner.run(runner, engine) to tie updates to the browser's requestAnimationFrame. In a turn-based game, you must omit Matter.Runner completely, or stop it using:

Matter.Runner.stop(runner);

Without the runner, the simulation remains paused until you explicitly tell the engine to advance.

2. Advance the Engine Using Engine.update

To advance the physics world manually, call Matter.Engine.update(). This method takes two primary arguments: your engine instance and a fixed time step (delta) in milliseconds.

const delta = 1000 / 60; // Represents one frame at 60 FPS (~16.66ms)

// Advance the simulation by exactly one step
Matter.Engine.update(engine, delta);

Using a fixed delta value ensures that physics calculations remain deterministic and consistent across different devices and hardware capabilities.

3. Synchronize the Renderer

If you are using Matter.Render for visuals, the canvas will not update automatically without the continuous runner. You must manually call Render.world after your physics updates:

Matter.Engine.update(engine, delta);
Matter.Render.world(render);

4. Executing Multi-Step Turns

In turn-based games (such as artillery games, billiards, or bowling), a turn often requires the physics to run until all moving bodies come to a complete stop.

You can run multiple steps synchronously in a loop, or iteratively with an animation frame if you want the player to see the turn play out:

function stepTurn(engine, render, maxSteps = 300) {
  const delta = 1000 / 60;

  for (let i = 0; i < maxSteps; i++) {
    Matter.Engine.update(engine, delta);

    // Check if all bodies have settled
    const allSleeping = engine.world.bodies.every(body => body.isSleeping || body.isStatic);
    if (allSleeping) {
      break;
    }
  }

  // Draw the final state of the turn
  Matter.Render.world(render);
}

To enable the isSleeping check, ensure you set enableSleeping: true in your engine configuration:

const engine = Matter.Engine.create({
  enableSleeping: true
});

Complete Implementation Example

const { Engine, Render, Bodies, Composite } = Matter;

// 1. Initialize Engine and Renderer
const engine = Engine.create({ enableSleeping: true });
const render = Render.create({
  element: document.body,
  engine: engine,
  options: { width: 800, height: 600, wireframes: false }
});

// 2. Add sample bodies
const ball = Bodies.circle(100, 100, 20, { restitution: 0.8 });
const ground = Bodies.rectangle(400, 580, 810, 40, { isStatic: true });
Composite.add(engine.world, [ball, ground]);

// Initial render
Render.world(render);

// 3. Trigger manual step on demand (e.g., button click or game action)
function triggerTurnStep() {
  const fixedDelta = 1000 / 60;
  
  // Step the simulation once
  Engine.update(engine, fixedDelta);
  
  // Update the visual frame
  Render.world(render);
}

document.getElementById('turnButton').addEventListener('click', triggerTurnStep);