How to Reconcile Local Matter.js State with Server
In networked multiplayer physics simulations, client-side prediction keeps gameplay responsive, but network latency and floating-point non-determinism inevitably cause client and server states to drift apart. Server reconciliation solves this by treating the server as the ultimate authority, rolling back the local Matter.js physics engine to the authoritative server state when a mismatch occurs, and then reapplying all pending, unacknowledged local inputs to catch back up to the present. This article explains the exact architecture and mechanics required to implement robust server reconciliation using Matter.js.
The Prediction and History Architecture
To reconcile state deviations, the client must track history over time. Real-time inputs cannot simply be applied and discarded; they must be stamped and stored alongside state snapshots.
- Sequence Tagging: Every user input (such as
directional impulses or torque applications) is tagged with a
monotonically increasing
sequenceNumberand sent to the server. - Input History Buffer: The client stores these inputs in a circular buffer along with the exact frame or tick they were generated.
- State History Buffer: The client periodically captures physical snapshots of its Matter.js bodies (position, angle, linear velocity, and angular velocity) indexed by input sequence number.
Detecting State Desynchronization
The server processes incoming inputs sequentially, updates its authoritative Matter.js instance, and periodically broadcasts an authoritative snapshot back to the client. This snapshot contains:
- The authoritative transform and velocity of the entities.
- The
lastProcessedInputsequence number acknowledged by the server.
Upon receiving this payload, the client searches its history buffer
for the local state snapshot matching lastProcessedInput.
It calculates the error vector:
\[\text{Error} = |\vec{P}_{\text{local}} - \vec{P}_{\text{server}}|\]
If the deviation exceeds a defined threshold (for instance, a few pixels or units to prevent unnecessary resets due to harmless floating-point noise), a reconciliation is triggered.
The Reconciliation and Resimulation Loop
When reconciliation is necessary, the client must fast-forward the physics simulation from the server's confirmed state to the current local frame.
1. Reset State to Authority
Directly override the target body's physical properties in Matter.js using the server snapshot values:
Matter.Body.setPosition(clientBody, serverState.position);
Matter.Body.setAngle(clientBody, serverState.angle);
Matter.Body.setVelocity(clientBody, serverState.velocity);
Matter.Body.setAngularVelocity(clientBody, serverState.angularVelocity);2. Discard Acknowledged Inputs
Remove all inputs from the input history buffer whose
sequenceNumber is less than or equal to
lastProcessedInput, as the server has already executed
them.
3. Re-simulate Remaining Inputs
Loop through all remaining unacknowledged inputs in chronological order. For each input:
- Apply the corresponding forces or impulses to the Matter.js body.
- Step the engine forward manually using a fixed delta time via
Matter.Engine.update(engine, fixedDelta).
for (const input of pendingInputs) {
applyInputForces(clientBody, input);
Matter.Engine.update(engine, FIXED_TIMESTEP);
}Once the loop completes, the client body has recovered the divergence and sits at the correct, re-predicted present state.
Mitigating Visual Popping with Error Smoothing
Instantaneously snapping the physics body to the reconciled state produces visible stutter or "popping." To maintain visual continuity while strictly reconciling the underlying physics:
- Calculate the spatial difference between the old (erroneous) client position and the newly reconciled position: \[\vec{O}_{\text{error}} = \vec{P}_{\text{pre-reconciliation}} - \vec{P}_{\text{post-reconciliation}}\]
- Decouple the Matter.js rigid body from the rendering layer. Keep the rigid body at the true, simulated physics position, but render the visual sprite at: \[\vec{P}_{\text{render}} = \vec{P}_{\text{body}} + \vec{O}_{\text{error}}\]
- Exponentially decay \(\vec{O}_{\text{error}}\) to zero over a short window (e.g., 50–100ms) using linear interpolation: \[\vec{O}_{\text{error}} \leftarrow \vec{O}_{\text{error}} \times (1 - \lambda \cdot \Delta t)\]
This technique conceals physical corrections without compromising the deterministic replay required for authoritative synchronization.