Integrate Matter.js with requestAnimationFrame

This article explains how to manually drive the Matter.js physics engine using the browser's native requestAnimationFrame API instead of the default Matter.Runner. You will learn how to bypass the automated runner, decouple your physics updates from the display refresh rate, and implement both a basic game loop and an advanced fixed-timestep loop with an accumulator to ensure deterministic, smooth physics behavior across various screen refresh rates.

Why Use requestAnimationFrame Instead of Matter.Runner?

By default, Matter.js provides Matter.Runner to automate engine updates. However, managing your own game loop with requestAnimationFrame is necessary when:

Basic Implementation

To integrate Matter.js directly with requestAnimationFrame, call Engine.update(engine, delta) inside your animation callback function instead of launching Matter.Runner.run().

import Matter from 'matter-js';

const { Engine, World, Bodies } = Matter;

// 1. Create the engine
const engine = Engine.create();

// Add bodies to the world
const box = Bodies.rectangle(400, 200, 80, 80);
const ground = Bodies.rectangle(400, 610, 810, 60, { isStatic: true });
World.add(engine.world, [box, ground]);

// 2. Setup the loop variables
let lastTime = performance.now();
let animationFrameId;

// 3. Define the update loop
function loop(currentTime) {
  // Calculate time elapsed since last frame (in milliseconds)
  const deltaTime = currentTime - lastTime;
  lastTime = currentTime;

  // Update Matter.js engine
  Matter.Engine.update(engine, deltaTime);

  // Custom render logic goes here (e.g., drawing bodies to canvas)

  // Request next frame
  animationFrameId = requestAnimationFrame(loop);
}

// 4. Start the loop
animationFrameId = requestAnimationFrame(loop);

To stop the loop at any time:

cancelAnimationFrame(animationFrameId);

Advanced Implementation: Fixed Timestep with an Accumulator

Passing a variable deltaTime directly to Engine.update can lead to unstable physics, clipping through geometry, or inconsistent behavior if the frame rate drops.

A production-grade approach uses a fixed timestep with an accumulator. This decouples the physics calculation frequency from the monitor's display refresh rate.

import Matter from 'matter-js';

const engine = Matter.Engine.create();

let lastTime = performance.now();
let accumulator = 0;
const fixedDelta = 1000 / 60; // Fixed 60 updates per second (~16.67ms)
let animationFrameId;

function loop(currentTime) {
  // Calculate time passed since last tick
  let frameTime = currentTime - lastTime;
  lastTime = currentTime;

  // Prevent spiral of death on lag spikes or tab switching
  if (frameTime > 250) {
    frameTime = 250;
  }

  accumulator += frameTime;

  // Consume accumulated time in fixed slices
  while (accumulator >= fixedDelta) {
    Matter.Engine.update(engine, fixedDelta);
    accumulator -= fixedDelta;
  }

  // Render your scene after all physics steps for this frame are complete
  render();

  animationFrameId = requestAnimationFrame(loop);
}

function render() {
  // Clear canvas and draw updated body positions here
}

animationFrameId = requestAnimationFrame(loop);

Summary Checklist for Integration

  1. Omit Matter.Runner: Do not invoke Matter.Runner.run(), as running both will cause the engine to update twice per frame.
  2. Handle Large Deltas: Always clamp frameTime when tabs lose focus to prevent the accumulator from running hundreds of updates simultaneously upon refocusing.
  3. Separate Physics and Rendering: Run all physics steps before drawing to the screen to ensure visuals always reflect the latest physics state.