Fix Floating-Point Math in Multiplayer Matter.js
This article explains how to resolve floating-point divergence and simulation desynchronization in multiplayer games using Matter.js. You will learn why cross-platform JavaScript engines produce inconsistent physics states, how to enforce deterministic execution, and which networking architectures—such as authoritative servers with reconciliation or fixed-point math implementations—effectively eliminate drift across connected clients.
Why Floating-Point Differences Occur in Matter.js
Matter.js relies on JavaScript’s native 64-bit IEEE 754 floating-point numbers. While the IEEE 754 standard defines how numbers are represented, execution variance across different hardware architectures (e.g., x86 vs. ARM), operating systems, and JavaScript engines (V8, SpiderMonkey, JavaScriptCore) creates subtle rounding discrepancies.
In a rigid-body physics engine, these micro-discrepancies compound exponentially through iterative constraint solving, collision resolution, and continuous integration. Within a few seconds of gameplay, identical inputs fed to two different clients will produce entirely divergent game states.
Solution 1: Use an Authoritative Server Architecture
The most robust method to solve floating-point desynchronization in Matter.js is to avoid relying on peer-to-peer determinism entirely. Instead, use an authoritative server model:
- Run the Physics Engine on the Server: Execute the canonical instance of Matter.js inside Node.js on your game server.
- Send Player Inputs: Clients send timestamped control inputs (e.g., jump, move left) to the server rather than sending body positions or velocities.
- Run Client-Side Prediction: To eliminate input lag, clients simulate Matter.js locally using the same inputs immediately.
- Server Reconciliation: The server periodically broadcasts snapshots of the authoritative bodies (position, velocity, angle, angular velocity). The client compares its past predicted state to the server state, snaps to the authoritative values if an error exceeds a threshold, and resimulates forward to the current tick.
Solution 2: Enforce Strict, Fixed Timesteps
Matter.js defaults to using variable frame rates if tied to
requestAnimationFrame. Variable delta times guarantee
immediate desynchronization. You must decouple physics from rendering
using a fixed accumulation loop.
const fixedDelta = 1000 / 60; // Exactly 60Hz
let accumulator = 0;
let lastTime = performance.now();
function gameLoop(currentTime) {
const frameTime = currentTime - lastTime;
lastTime = currentTime;
accumulator += frameTime;
while (accumulator >= fixedDelta) {
Matter.Engine.update(engine, fixedDelta);
accumulator -= fixedDelta;
}
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);Ensure engine.timing.isFixed = true is configured and
that custom iteration counts (engine.positionIterations and
engine.velocityIterations) are identical on all
machines.
Solution 3: Quantize and Truncate Floating-Point State
If you must run simulations in parallel, you can mitigate lower-bit floating-point variance by rounding physics states at the end of each simulation tick. Truncating values strips off the micro-variations produced by different CPU arithmetic units before they can propagate to the next step.
function quantize(value, precision = 10000) {
return Math.round(value * precision) / precision;
}
Matter.Events.on(engine, 'afterUpdate', () => {
const bodies = Matter.Composite.allBodies(engine.world);
for (let i = 0; i < bodies.length; i++) {
const body = bodies[i];
body.position.x = quantize(body.position.x);
body.position.y = quantize(body.position.y);
body.velocity.x = quantize(body.velocity.x);
body.velocity.y = quantize(body.velocity.y);
body.angle = quantize(body.angle);
body.angularVelocity = quantize(body.angularVelocity);
}
});Solution 4: Replace Non-Deterministic Math Functions
Different browser engines calculate transcendental functions
(Math.sin, Math.cos, Math.sqrt)
with slight differences in the least significant bits. For lockstep
peer-to-peer setups:
- Lookup Tables: Replace trigonometric functions with precomputed integer or quantized lookup tables.
- Math Polyfills: Monkey-patch standard
Mathmethods with deterministic software implementations (such as polynomial approximations) to ensure every client computes identical values down to the last bit.
Solution 5: Use Integer/Fixed-Point Mathematics
For true lockstep determinism (e.g., standard RTS style lockstep networking), floating-point types cannot be used. While Matter.js is natively coupled to standard JavaScript numbers, you can fork or adapt its vector and collision modules to work with fixed-point math libraries (representing numbers as integers scaled by a fixed factor, such as 16.16 or 32.32 fixed-point numbers). By executing all physics integration through pure integer arithmetic, cross-platform execution becomes mathematically deterministic.