How to Rewind and Replay States in Matter.js
This article explains how to implement a state rewind and replay system in Matter.js by capturing frame-by-frame simulation data and reapplying it to physics bodies. Because Matter.js does not include a native time-travel feature, implementing rewind and replay functionality requires capturing snapshots of critical body properties—such as position, velocity, and rotation—during each update cycle, storing them in a history buffer, and restoring those values when rewinding or playing back.
Key Concepts of State Storage
To accurately reconstruct a physics state in Matter.js, you must record dynamic properties for every active body on each tick:
- Position:
{ x, y } - Velocity:
{ x, y } - Angle: Rotation in radians
- Angular Velocity: Rotational speed
Static bodies (like walls or immobile platforms) do not need per-frame state tracking unless they are moved programmatically.
Step 1: Capturing State Snapshots
Hook into the engine's update loop using the afterUpdate
event. Store the collected state of all dynamic bodies into an array
representing the timeline.
const history = [];
const MAX_FRAMES = 600; // Limit history to prevent memory issues (e.g., 10 seconds at 60fps)
Matter.Events.on(engine, 'afterUpdate', () => {
if (isRewinding || isReplaying) return;
const frameSnapshot = composite.bodies.map(body => ({
id: body.id,
position: { x: body.position.x, y: body.position.y },
velocity: { x: body.velocity.x, y: body.velocity.y },
angle: body.angle,
angularVelocity: body.angularVelocity
}));
history.push(frameSnapshot);
if (history.length > MAX_FRAMES) {
history.shift();
}
});Step 2: Restoring Body States
To apply a saved state back to a body, avoid setting properties
directly through assignments like body.position = ....
Instead, use the built-in Matter.Body methods to maintain
internal engine constraints and bounding boxes properly.
function applyState(body, state) {
Matter.Body.setPosition(body, state.position);
Matter.Body.setVelocity(body, state.velocity);
Matter.Body.setAngle(body, state.angle);
Matter.Body.setAngularVelocity(body, state.angularVelocity);
}Step 3: Implementing Rewind
When rewinding, pause the Matter.Runner to prevent the
engine from calculating forward physics steps. Then, step backward
through the history array inside your own animation loop
(using requestAnimationFrame).
let isRewinding = false;
function startRewind() {
isRewinding = true;
Matter.Runner.stop(runner); // Halt forward physics calculation
rewindLoop();
}
function rewindLoop() {
if (!isRewinding) return;
if (history.length > 0) {
const previousFrame = history.pop();
previousFrame.forEach(state => {
const body = Matter.Composite.allBodies(engine.world).find(b => b.id === state.id);
if (body) {
applyState(body, state);
}
});
requestAnimationFrame(rewindLoop);
} else {
stopRewind();
}
}
function stopRewind() {
isRewinding = false;
Matter.Runner.start(runner, engine); // Resume standard physics simulation
}Step 4: Implementing Replay
Replaying operates similarly to rewinding, but instead of removing
states from the end of the history array using pop(),
iterate forward through the saved snapshots from a given starting
index.
let isReplaying = false;
let replayIndex = 0;
function startReplay() {
isReplaying = true;
replayIndex = 0;
Matter.Runner.stop(runner);
replayLoop();
}
function replayLoop() {
if (!isReplaying) return;
if (replayIndex < history.length) {
const frame = history[replayIndex];
frame.forEach(state => {
const body = Matter.Composite.allBodies(engine.world).find(b => b.id === state.id);
if (body) {
applyState(body, state);
}
});
replayIndex++;
requestAnimationFrame(replayLoop);
} else {
isReplaying = false;
Matter.Runner.start(runner, engine);
}
}Handling Collisions and Sleeping Bodies
- Sleeping Bodies: If using
enableSleeping, wake all bodies up during restoration usingMatter.Sleeping.set(body, false)to prevent visual or physics glitches when normal simulation resumes. - Collision Events: Clear any pending custom collision state or game logic events during rewind to ensure logic does not trigger in reverse unintentionally.