Deterministic Input Replay in Matter.js
Debugging sporadic physics anomalies in Matter.js can be exceptionally difficult when relying on manual reproduction. By decoupling the physics update from the browser's variable render loop and recording user interactions relative to discrete physics ticks, you can capture the exact sequence of events leading to a glitch. This article explains how to configure a fixed timestep simulation, record user inputs per frame, and accurately replay those inputs to inspect and resolve physics bugs.
1. Enforce a Fixed Timestep
Matter.js instances powered by Matter.Runner dynamically
adjust their delta time by default based on monitor refresh rates and
performance dips. Because floating-point operations in physics engines
depend on the exact integration step size, any variation in delta will
alter collision outcomes.
To achieve determinism, discard Matter.Runner or
configure manual updates using a fixed timestep:
const engine = Matter.Engine.create();
const FIXED_DELTA = 1000 / 60; // 60 Hz fixed timestep
function physicsTick() {
Matter.Engine.update(engine, FIXED_DELTA);
}Ensure this fixed update is called predictably. During debugging or
replay, you can advance the simulation one frame at a time manually
rather than letting requestAnimationFrame run
continuously.
2. Capture Inputs Relative to Engine Ticks
Do not apply user inputs (such as forces, impulses, or position
overrides) directly inside browser event listeners
(keydown, mousemove, etc.). Instead, queue
user actions and bind them to the current physics tick.
Maintain a monotonic tick counter and an input log:
let currentTick = 0;
const inputLog = [];
let pendingInputs = [];
// Event listener stores raw intent
window.addEventListener('keydown', (e) => {
pendingInputs.push({ type: 'keydown', key: e.key });
});
// Run this right before updating the physics
function processInputsForTick() {
if (pendingInputs.length > 0) {
inputLog.push({
tick: currentTick,
actions: [...pendingInputs]
});
pendingInputs = [];
}
}3. Apply Actions Uniformly
Map the captured actions to physical bodies through a centralized dispatcher. This guarantees that both live gameplay and replays manipulate the simulation identically:
function applyAction(action) {
if (action.type === 'keydown' && action.key === 'Space') {
Matter.Body.applyForce(playerBody, playerBody.position, { x: 0, y: -0.05 });
}
}
function stepSimulation() {
processInputsForTick();
// Apply any actions meant for this tick
const entry = inputLog.find(log => log.tick === currentTick);
if (entry) {
entry.actions.forEach(applyAction);
}
Matter.Engine.update(engine, FIXED_DELTA);
currentTick++;
}4. Replay and Isolate Glitches
To replay a captured session, export inputLog alongside
the initial world state (body dimensions, initial coordinates, and
collision filters).
To execute the replay:
- Reset the
Matter.Engineinstance and reconstruct all bodies in their exact starting positions. - Reset
currentTickto zero. - Advance the engine frame-by-frame, applying actions from the
recorded
inputLogwhenlog.tick === currentTick.
function replayStep(recordedLog) {
const entry = recordedLog.find(item => item.tick === currentTick);
if (entry) {
entry.actions.forEach(applyAction);
}
Matter.Engine.update(engine, FIXED_DELTA);
currentTick++;
}5. Inspecting the Root Cause
Once an input sequence consistently reproduces the glitch, take advantage of the deterministic environment:
- Step-by-Step Traversal: Pause the simulation a few ticks before the glitch occurs and advance tick-by-tick to observe body overlap and impulse spikes.
- Logging Collision Pairs: Log
engine.pairs.liston the exact tick of the failure to evaluate active contacts, penetration depths, and collision normals. - Constraint and Tunneling Analysis: If bodies clip
through geometry, verify whether high velocities require increasing
engine.positionIterationsandengine.velocityIterations, or if the body requires continuous collision detection (CCD) alternatives such as subdivision of the movement vector across smaller substeps.