How to Implement Sub-Stepping in Matter.js
Sub-stepping is an essential physics technique used to eliminate
collision tunneling and enhance stability for fast-moving bodies by
dividing a single frame's duration into smaller, discrete time
increments. This article demonstrates how to implement manual
sub-stepping in Matter.js by replacing the default runner with a custom
requestAnimationFrame loop that executes
Matter.Engine.update multiple times per display
refresh.
Why Use Sub-Stepping?
By default, Matter.js processes physics updates once per animation frame (roughly every 16.67 milliseconds at 60Hz). When bodies move at high velocities, they may travel completely through thin barriers or other objects between frames—an issue known as tunneling.
Dividing the frame time into smaller slices (sub-steps) and running the physics solver for each slice ensures that collision detection occurs more frequently, preventing overlap errors and yielding more accurate physical responses.
Disabling the Default Runner
To control engine updates manually, avoid using
Matter.Runner.run(runner, engine). The built-in
Matter.Runner manages its own loop and does not natively
support sub-stepping. Instead, manage the animation loop directly using
standard browser APIs.
Implementing the Custom Update Loop
To execute multiple sub-steps per frame, determine the target frame
delta and divide it by the desired number of iterations. Inside your
requestAnimationFrame callback, run a loop that repeatedly
calls Matter.Engine.update(engine, subDelta).
const Engine = Matter.Engine;
const Render = Matter.Render;
// 1. Create engine and renderer
const engine = Engine.create();
const render = Render.create({
element: document.body,
engine: engine,
options: {
width: 800,
height: 600,
wireframes: false
}
});
// 2. Configure sub-stepping parameters
const subSteps = 4;
const targetFps = 60;
const frameDelta = 1000 / targetFps;
const subDelta = frameDelta / subSteps;
// 3. Define the custom game loop
function loop() {
// Execute the physics engine multiple times per frame
for (let i = 0; i < subSteps; i++) {
Engine.update(engine, subDelta);
}
// Render the updated physics state once per frame
Render.world(render);
requestAnimationFrame(loop);
}
// 4. Start the loop
requestAnimationFrame(loop);Key Considerations
- Rendering Performance: Do not place rendering calls
(such as
Matter.Render.worldor custom canvas drawing routines) inside the sub-step loop. Rendering is computationally expensive and only needs to happen once per screen refresh after all sub-steps have settled. - Choosing Sub-Step Counts: Each additional sub-step
multiplies the CPU work required for collision detection and constraint
solving. Setting
subStepsbetween2and8typically provides sufficient stability for fast objects without causing frame drops on standard hardware. - Fixed Timesteps: Passing a fixed delta (such as
subDelta) rather than variable elapsed wall-clock time ensures deterministic physics behavior across devices with varying refresh rates.