State Interpolation for Remote Players in Matter.js

This article explains how to implement client-side state interpolation for remote players using the Matter.js 2D physics engine. In multiplayer games, network latency and packet jitter make directly setting player positions from network packets look jerky and erratic. By creating a snapshot buffer, running remote entities slightly behind real-time, and linearly interpolating between received states, you can achieve smooth, reliable movement for remote bodies without fighting the local physics simulation.

1. Configure the Remote Physics Body

By default, Matter.js dynamic bodies respond to gravity, forces, and collisions, which will conflict with positional updates arriving from a server. Remote bodies should be treated as kinematic proxies:

const remoteBody = Matter.Bodies.rectangle(x, y, width, height, {
  isSensor: true, // Prevents local collision response if server handles physics
  frictionAir: 0,
  mass: 1
});

Matter.Composite.add(engine.world, remoteBody);

Using isSensor: true allows the body to trigger collision detection events if needed, but prevents Matter.js from applying impulses that fight the incoming network positions.

2. Store Server Updates in a Snapshot Buffer

Whenever the client receives a state update from the server (containing a timestamp, x, y, and angle), push it into an array. Ensure snapshots are kept sorted chronologically and discard states older than necessary.

const snapshotBuffer = [];
const BUFFER_CAPACITY = 20;

function onServerUpdate(data) {
  // data = { timestamp, x, y, angle }
  snapshotBuffer.push(data);
  
  if (snapshotBuffer.length > BUFFER_CAPACITY) {
    snapshotBuffer.shift();
  }
}

3. Establish an Interpolation Delay

To interpolate smoothly between two known points, the client must render remote players slightly in the past (typically 100ms to 150ms behind actual server time). This delay covers network transit fluctuations and packet arrival intervals.

const INTERPOLATION_OFFSET_MS = 100;

function getRenderTimestamp() {
  return Date.now() - INTERPOLATION_OFFSET_MS;
}

4. Locate Surrounding Snapshots and Calculate Progress

In every frame before updating the Matter.js engine, determine the target render time, find the two snapshots that encapsulate this time, and calculate the interpolation factor (\(\alpha\)).

function getInterpolatedState(renderTime) {
  if (snapshotBuffer.length < 2) {
    return snapshotBuffer[0] || null;
  }

  // If the render time is older than our oldest snapshot, use the oldest
  if (renderTime <= snapshotBuffer[0].timestamp) {
    return snapshotBuffer[0];
  }

  // If the render time is ahead of the newest snapshot, extrapolate or use newest
  if (renderTime >= snapshotBuffer[snapshotBuffer.length - 1].timestamp) {
    return snapshotBuffer[snapshotBuffer.length - 1];
  }

  // Find the two surrounding snapshots
  for (let i = 0; i < snapshotBuffer.length - 1; i++) {
    const p0 = snapshotBuffer[i];
    const p1 = snapshotBuffer[i + 1];

    if (renderTime >= p0.timestamp && renderTime <= p1.timestamp) {
      const alpha = (renderTime - p0.timestamp) / (p1.timestamp - p0.timestamp);
      
      return {
        x: lerp(p0.x, p1.x, alpha),
        y: lerp(p0.y, p1.y, alpha),
        angle: lerpAngle(p0.angle, p1.angle, alpha)
      };
    }
  }

  return null;
}

function lerp(start, end, alpha) {
  return start + (end - start) * alpha;
}

function lerpAngle(start, end, alpha) {
  // Shortest path angle interpolation
  const difference = (end - start) % (2 * Math.PI);
  const shortestAngle = ((2 * difference) % (2 * Math.PI)) - difference;
  return start + shortestAngle * alpha;
}

5. Apply the Interpolated State to Matter.js

Apply the calculated position and angle directly to the body using Matter.js helper functions rather than modifying properties directly. This ensures internal properties, bounding boxes, and velocity approximations update correctly.

function updateRemotePlayers() {
  const renderTime = getRenderTimestamp();
  const state = getInterpolatedState(renderTime);

  if (state) {
    Matter.Body.setPosition(remoteBody, { x: state.x, y: state.y });
    Matter.Body.setAngle(remoteBody, state.angle);
  }
}

// Hook into the animation loop before Matter.Engine.update
function gameLoop() {
  updateRemotePlayers();
  Matter.Engine.update(engine, 1000 / 60);
  requestAnimationFrame(gameLoop);
}

requestAnimationFrame(gameLoop);

By decoupling remote player positioning from local physics calculations and buffering states by a set delay, remote players will glide smoothly across the screen even when network packets arrive irregularly.