How to Synchronize Two Matter.js Engines
Synchronizing the state between two independent Matter.js physics engines running side by side—such as in a client-server simulation, a Web Worker architecture, or a dual-view setup—requires an authoritative state pipeline, structured data serialization, and accurate state reconciliation. This article details how to extract physics parameters from a primary simulation, transfer the minimal necessary payload to a secondary engine, and apply state corrections without causing visual jitter or simulation instability.
1. Establish the Authority Model
Before sharing data, designate one Matter.js engine as the authoritative source (Master) and the other as the replica (Slave).
- Master Engine: Computes collisions, forces, and canonical body states.
- Slave Engine: Mirrors the master engine, either for rendering purposes or as a client-side prediction sandbox.
Never allow both engines to update each other bidirectionally without conflict-resolution logic; doing so introduces feedback loops and compounding floating-point drift.
2. Extract the Essential State Payload
Matter.js composite trees contain deep object graphs with circular
references. Do not attempt to serialize the entire Engine
or Composite instance. Instead, map each rigid body to a
unique identifier (id) and extract only the dynamic
properties required to reconstruct motion:
function extractEngineState(engine) {
const bodies = Matter.Composite.allBodies(engine.world);
return bodies
.filter(body => !body.isStatic)
.map(body => ({
id: body.id,
position: { x: body.position.x, y: body.position.y },
angle: body.angle,
velocity: { x: body.velocity.x, y: body.velocity.y },
angularVelocity: body.angularVelocity
}));
}If bodies are frequently created or destroyed, also synchronize dynamic body creation and removal events rather than querying full hierarchies every tick.
3. Apply the State to the Target Engine
To synchronize the secondary engine, match incoming state records by
id and apply the values using built-in
Matter.Body manipulation functions rather than direct
object mutations. Direct mutations bypass internal cache recalculations
(such as axes, bounds, and inertia).
function applyEngineState(targetEngine, statePayload) {
const bodies = Matter.Composite.allBodies(targetEngine.world);
const bodyMap = new Map(bodies.map(b => [b.id, b]));
for (const state of statePayload) {
const targetBody = bodyMap.get(state.id);
if (!targetBody) continue;
Matter.Body.setPosition(targetBody, state.position);
Matter.Body.setAngle(targetBody, state.angle);
Matter.Body.setVelocity(targetBody, state.velocity);
Matter.Body.setAngularVelocity(targetBody, state.angularVelocity);
}
}4. Prevent Simulation Drift and Jitter
Directly snapping properties each tick can lead to jarring visual artifacts and physics explosions if the secondary engine has active collision constraints. Use the following techniques to maintain stability:
Step Timing Alignment
Both engines must run on the exact same delta time. Avoid passing
variable frame deltas (Engine.update(engine, delta)) where
frame drops can cause divergent integrations. Run both engines using
fixed time steps:
const FIXED_DELTA = 1000 / 60;
Matter.Engine.update(masterEngine, FIXED_DELTA);
Matter.Engine.update(slaveEngine, FIXED_DELTA);Interpolation Over Hard Snapping
If updates between engines occur over a network or an asynchronous thread boundary (such as Web Workers), the secondary engine should interpolate between the current transform and the target transform:
const LERP_FACTOR = 0.2;
function smoothSyncBody(targetBody, targetState) {
const newX = targetBody.position.x + (targetState.position.x - targetBody.position.x) * LERP_FACTOR;
const newY = targetBody.position.y + (targetState.position.y - targetBody.position.y) * LERP_FACTOR;
const newAngle = targetBody.angle + (targetState.angle - targetBody.angle) * LERP_FACTOR;
Matter.Body.setPosition(targetBody, { x: newX, y: newY });
Matter.Body.setAngle(targetBody, newAngle);
Matter.Body.setVelocity(targetBody, targetState.velocity);
Matter.Body.setAngularVelocity(targetBody, targetState.angularVelocity);
}5. Synchronize Constraints and Sleeping Bodies
- Sleeping Bodies: If
engine.enableSleepingis set totrue, ensure that bodies woken up in the master engine explicitly wake up in the target engine viaMatter.Sleeping.set(targetBody, false). - Composite Constraints: If your physics simulation
uses springs, pins, or joints (
Matter.Constraint), create them with identical IDs on both sides during initialization. Synchronizing body positions and velocities will naturally align constraints without needing to transmit constraint data continuously.