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:

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