How to Implement Fixed Time Stepping in Matter.js
This article explains the concept of fixed time stepping in physics engines and provides a direct, step-by-step guide to implementing it using Matter.js. You will learn the difference between variable and fixed timesteps, why fixed steps are essential for physics stability, and how to replace the default Matter.js game loop with an accumulator-based loop to ensure consistent behavior across different hardware and frame rates.
What is Fixed Time Stepping?
In game development and physics simulations, time stepping refers to the interval (\(\Delta t\) or delta time) passed to the physics engine to calculate the next state of bodies, including velocity, position, and collisions.
There are two primary ways to handle this:
- Variable Time Stepping: The engine advances physics using the exact time elapsed since the previous frame. If a frame drops or stutters, \(\Delta t\) spikes. A large \(\Delta t\) causes objects to move huge distances in a single step, often resulting in "tunneling" (objects clipping through walls) or explosive force calculations.
- Fixed Time Stepping: The engine updates physics in precise, constant increments (such as 16.66ms for 60Hz), independent of the display refresh rate. If rendering slows down, the physics engine runs multiple fixed steps to catch up. If rendering is faster than the physics rate, the engine waits until enough real time has accumulated.
Fixed time stepping provides deterministic results, prevents physics instability caused by frame drops, and ensures consistent gameplay across high-refresh-rate monitors (such as 144Hz or 240Hz) and lower-end mobile devices.
The Problem with Matter.js Default Runner
Matter.js provides a built-in Matter.Runner module. By
default, Matter.Runner dynamically adjusts delta time to
match the browser's refresh rate via
requestAnimationFrame.
While convenient for simple setups, this default approach can cause
objects to behave differently depending on the user's screen refresh
rate or during sudden performance dips. To achieve completely stable,
predictable physics, you must bypass Matter.Runner and
control Matter.Engine.update() manually.
Implementing Fixed Time Stepping with an Accumulator
The industry-standard approach for fixed time stepping is the
time accumulator pattern. It measures the real elapsed
time between render frames, adds it to an accumulator, and consumes it
in fixed chunks via a while loop.
Complete Implementation
// 1. Initialize Engine and World
const { Engine, Render, World, Bodies } = Matter;
const engine = Engine.create();
const render = Render.create({
element: document.body,
engine: engine,
options: {
width: 800,
height: 600,
wireframes: false
}
});
// Add a test body and ground
const box = Bodies.rectangle(400, 200, 80, 80);
const ground = Bodies.rectangle(400, 590, 810, 60, { isStatic: true });
World.add(engine.world, [box, ground]);
// 2. Fixed Time Step Configuration
const fixedDelta = 1000 / 60; // 60 updates per second (~16.666ms)
const maxSubSteps = 5; // Prevents the "spiral of death"
let lastTime = performance.now();
let accumulator = 0;
// 3. Custom Game Loop
function gameLoop(currentTime) {
// Calculate elapsed real time in milliseconds
let frameTime = currentTime - lastTime;
lastTime = currentTime;
// Cap the maximum frame time to avoid huge catch-up loops after a lag spike
if (frameTime > 250) {
frameTime = 250;
}
accumulator += frameTime;
// Step the physics engine in fixed increments
let steps = 0;
while (accumulator >= fixedDelta && steps < maxSubSteps) {
Engine.update(engine, fixedDelta);
accumulator -= fixedDelta;
steps++;
}
// Render the updated physics state
Render.world(render);
requestAnimationFrame(gameLoop);
}
// Start the loop
requestAnimationFrame(gameLoop);Key Considerations
Preventing the Spiral of Death
If an update takes longer to compute than the fixedDelta
itself, the accumulator continues to grow, forcing more physics updates
in subsequent frames. This feedback loop freezes the browser. Using a
cap (like maxSubSteps or limiting frameTime to
250ms) discards excess accumulated time and prevents
simulation lockups during major lag spikes.
Decoupling Physics from Rendering
In the loop above, Engine.update(engine, fixedDelta)
runs at a strict 60Hz rate, while Render.world(render) runs
at the native display refresh rate. This ensures that physics
calculations remain identical whether the user has a 30Hz, 60Hz, or
144Hz monitor.