Matter.js Multiplayer Sync on a Node.js Server
This article provides a comprehensive guide on synchronizing real-time 2D physics across multiple clients using Matter.js and a Node.js server. You will learn how to implement an authoritative server architecture, set up a headless physics loop, optimize state transmission through snapshots and delta compression, and implement client-side prediction, reconciliation, and entity interpolation to ensure smooth, cheat-resistant gameplay.
The Authoritative Server Architecture
In physics-based multiplayer games, relying on clients to calculate physics results in desynchronization and exposes the game to cheating. The standard solution is an Authoritative Server model:
- Clients capture user input (key presses, mouse positions) and send them with sequence numbers to the server.
- The Server validates inputs, applies forces to the physics simulation, and advances the game state.
- The Server broadcasts physics snapshots to all connected clients at a fixed tick rate.
- Clients receive the snapshots, correct local prediction errors, and interpolate passive entities.
Running Headless Matter.js in Node.js
Matter.js is typically used in the browser with its built-in canvas renderer, but its core engine can run headlessly in Node.js.
To run it on the server:
- Omit
Matter.RenderandMatter.MouseConstraint(or substitute with custom math). - Use
Matter.Engine,Matter.Bodies,Matter.Composite, andMatter.Body. - Drive the simulation with a fixed time step using
hrtimerather thansetIntervalto prevent physics drift caused by JavaScript event loop delays.
import Matter from 'matter-js';
const { Engine, Bodies, Composite } = Matter;
const engine = Engine.create();
engine.gravity.y = 1; // standard gravity
const ground = Bodies.rectangle(400, 610, 810, 60, { isStatic: true });
Composite.add(engine.world, [ground]);
const TICK_RATE = 1000 / 30; // 30 Hz
let lastTime = Date.now();
setInterval(() => {
const now = Date.now();
const delta = now - lastTime;
lastTime = now;
// Step the physics engine
Engine.update(engine, delta);
// Broadcast state to clients
broadcastGameState();
}, TICK_RATE);Snapshot Serialization and Bandwidth Optimization
Sending the entire Matter.js body object over the network consumes excessive bandwidth. You must serialize only the minimal properties required to reconstruct the state:
- Entity ID: Unique identifier.
- Position:
xandycoordinates. - Orientation:
angle. - Velocities:
velocity.x,velocity.y, andangularVelocity(crucial for client extrapolation).
To minimize payload sizes:
- Round floating-point numbers to 2 or 3 decimal places.
- Use binary protocols like Buffer, ArrayBuffer, or libraries like Protobuf or Geckos.io instead of raw JSON.
- Implement delta compression: only send updates for bodies whose position, velocity, or angle have changed past a defined threshold.
Client-Side Prediction and Server Reconciliation
If a client waits for the server to process an input before moving, the game will feel unresponsive due to network latency (ping).
- Client-Side Prediction: When a player presses a movement key, apply the force locally immediately, and store the input along with a local sequence number.
- Authoritative Confirmation: When the server processes an input, it includes the latest processed sequence number in its state snapshot.
- Reconciliation: When the client receives a server
snapshot:
- Reset the local player body's position and velocity to match the server snapshot.
- Discard all stored inputs older than the snapshot's acknowledged sequence number.
- Re-simulate remaining unacknowledged inputs sequentially on top of the corrected server state.
If the difference between the predicted position and the server snapshot is negligible, skip the hard reset to avoid visual jitter.
Interpolation for Remote Entities
While the local player benefits from prediction, other entities (remote players, dynamic props) must be displayed smoothly despite receiving discrete snapshots (e.g., 20 or 30 updates per second).
To achieve smooth rendering:
- Maintain a snapshot buffer on the client (typically 100ms of past updates).
- Render entities slightly in the past (interpolation delay).
- Determine the two snapshots that surround the render timestamp (\(t_{render} = t_{now} - \text{delay}\)).
- Calculate the interpolation factor (\(t\) between 0 and 1) and use linear
interpolation (
lerp) for positions and spherical linear interpolation (slerp) for angles:
\[\text{Position} = A + (B - A) \times t\]
Overcoming Matter.js Non-Determinism
Matter.js uses floating-point arithmetic and iterative constraint solvers, meaning two identical simulations on different machines (or Node.js vs. Browser) can diverge over time.
Because Matter.js is not strictly deterministic:
- Do not rely on deterministic lockstep synchronization.
- Always treat the Node.js server state as the single source of truth.
- Use smooth correction (lerping from the current client position toward the authoritative server position) rather than instant snapping when small discrepancies occur, ensuring that physics visual artifacts remain hidden from the player.