Entity Interpolation in Matter.js for Smooth Movement

This article provides a step-by-step technical guide to implementing client-side entity interpolation for networked Matter.js bodies. You will learn how to buffer incoming server state updates, calculate an interpolation timestamp, and smoothly blend positions and rotations between updates to eliminate jitter caused by network latency and variable packet delivery rates.

Why Entity Interpolation is Necessary

In multiplayer games using physics engines like Matter.js, servers typically broadcast world states at fixed tick rates (for example, 20 or 30 Hz). If a client renders at 60 Hz or higher, directly snapping Matter.js bodies to the latest received coordinates creates stuttering and visual jitter.

Entity interpolation resolves this issue by rendering remote bodies slightly in the past (typically delayed by 50ms to 100ms). This intentional delay ensures the client always has at least two server states to transition between smoothly.

Step 1: Configure the Remote Matter.js Body

Remote bodies should not be simulated actively by the local physics engine. If the client runs physics calculations on them, local predictions will fight incoming server corrections.

Set the remote body to static or disable its collision responses so it acts solely as a visual proxy:

const remoteBody = Matter.Bodies.rectangle(x, y, width, height, {
    isStatic: true,
    isSensor: true // Prevents collision conflicts locally
});
Matter.Composite.add(engine.world, remoteBody);

Step 2: Buffer Incoming Network Snapshots

Maintain an array of snapshots received from the server. Each snapshot must contain the server timestamp, position coordinates, and the body's rotation.

const snapshotBuffer = [];

function onServerUpdate(packet) {
    // packet: { timestamp, x, y, angle }
    snapshotBuffer.push(packet);

    // Keep the buffer clean by dropping snapshots older than 1 second
    const maxAge = 1000;
    while (snapshotBuffer.length > 2 && snapshotBuffer[0].timestamp < packet.timestamp - maxAge) {
        snapshotBuffer.shift();
    }
}

Step 3: Calculate the Render Time

Define an interpolation offset (the delay). On every local animation or update frame, determine the target render timestamp:

const INTERPOLATION_OFFSET = 100; // milliseconds

function getRenderTime() {
    return Date.now() - INTERPOLATION_OFFSET;
}

Step 4: Locate Bracketing Snapshots

Iterate through the buffer to find the two snapshots that bracket the current renderTime—one snapshot just before (p0) and one snapshot just after (p1):

function getBoundingSnapshots(renderTime) {
    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) {
            return { p0, p1 };
        }
    }
    return null;
}

If the buffer does not yet contain two snapshots, or if network lag causes renderTime to exceed the newest snapshot, you can either extrapolate forward or lock the body to the latest known state.

Step 5: Interpolate and Apply to the Matter.js Body

Calculate the interpolation factor t, which ranges from 0 to 1. Use linear interpolation (LERP) for positional coordinates and shortest-path angular interpolation for rotation.

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

function lerpAngle(a, b, t) {
    const diff = ((b - a + Math.PI) % (Math.PI * 2)) - Math.PI;
    return a + diff * t;
}

function updateRemoteBody() {
    const renderTime = getRenderTime();
    const bounds = getBoundingSnapshots(renderTime);

    if (!bounds) {
        // Fallback: Snap to latest state if waiting for packets
        if (snapshotBuffer.length > 0) {
            const latest = snapshotBuffer[snapshotBuffer.length - 1];
            Matter.Body.setPosition(remoteBody, { x: latest.x, y: latest.y });
            Matter.Body.setAngle(remoteBody, latest.angle);
        }
        return;
    }

    const { p0, p1 } = bounds;
    const timeDelta = p1.timestamp - p0.timestamp;
    const t = timeDelta > 0 ? (renderTime - p0.timestamp) / timeDelta : 0;

    const interpolatedX = lerp(p0.x, p1.x, t);
    const interpolatedY = lerp(p0.y, p1.y, t);
    const interpolatedAngle = lerpAngle(p0.angle, p1.angle, t);

    // Apply transformed values to the Matter.js body
    Matter.Body.setPosition(remoteBody, { x: interpolatedX, y: interpolatedY });
    Matter.Body.setAngle(remoteBody, interpolatedAngle);
}

Step 6: Hook into the Render Loop

Execute updateRemoteBody() right before Matter.js renders the frame. If using the default Matter.Render module, listen to the beforeRender event on the renderer instance:

Matter.Events.on(render, 'beforeRender', () => {
    updateRemoteBody();
});

This implementation guarantees that the rendered transformation of remote bodies remains fluid and detached from client-side frame rate variations or incoming network packet irregularities.