NFnexframe_
logs/025 · netcode

Deterministic Physics for Multiplayer Games, Step by Step

A glowing green oscilloscope waveform in a dark lab

Lockstep and rollback netcode share one hard requirement: every machine must simulate the exact same world from the exact same inputs. Send only the player's button presses across the wire, and each client reconstructs the whole game locally. That only works if the simulation is deterministic — feed it identical inputs and it produces bit-for-bit identical output, forever. This log builds that guarantee from the ground up.

Step 1 — a fixed timestep, always

Variable frame times are the first thing to go. If one machine runs at 144 fps and another at 60, they integrate motion over different intervals and drift apart within seconds. Determinism demands that physics advances in fixed, identical chunks regardless of how fast the screen refreshes.

loop.c — the accumulator
const int DT_MS = 1000 / 60;   // fixed 60 Hz step
accumulator += frame_time_ms;

while (accumulator >= DT_MS) {   // exact integer subtraction
  simulate_tick(&world, inputs_for_tick(tick++));
  accumulator -= DT_MS;
}
render(&world, accumulator);   // interpolate for display only

Rendering can interpolate between ticks for smoothness — that part is allowed to vary per machine because it never feeds back into the simulation. The line between "affects the world" and "just for the eyes" must be absolute.

Step 2 — kill the floats (or pin them)

The subtler enemy is floating-point math. Two CPUs can compute a * b + c and get results that differ in the last bit — different compilers, different SIMD widths, an 80-bit intermediate on one machine and 64-bit on another. Over thousands of ticks those tiny differences compound into two players standing in different places.

The robust answer is fixed-point arithmetic: represent positions and velocities as integers with an implied fractional scale. Integer math is exactly reproducible on every platform, full stop.

fixed.h — 16.16 fixed-point
typedef int32_t fixed;         // 16 integer, 16 fraction bits
#define FX_ONE (1 << 16)

static inline fixed fx_mul(fixed a, fixed b) {
  return (fixed)(((int64_t)a * b) >> 16);
}
static inline fixed fx_from_int(int n) { return n << 16; }

Yes, it's more work than float x. But fixed-point turns "probably in sync" into "provably in sync," and that certainty is worth the ceremony the moment two clients have to agree on where a projectile landed.

Step 3 — a strict, stable update order

Determinism isn't only about numbers; it's about sequence. If you iterate entities in hash-map order, or resolve collisions in whatever order they happen to appear, two machines can process the same set of events differently and diverge. Every loop that touches simulation state must run in an order both machines compute identically — sort by a stable entity ID, never by pointer address or insertion timing.

The single most common desync in the wild: iterating a set or dictionary whose order isn't guaranteed. Store entities in a flat array indexed by a stable ID and iterate that array. Boring, bulletproof.

Step 4 — prove it with a checksum

You don't trust determinism, you verify it. Every N ticks, hash the entire simulation state into a single number and compare it across clients. The instant two checksums disagree, you know the exact tick a desync entered — which turns a vague "it drifts sometimes" bug into a precise, reproducible one.

verify.c — state hash
uint64_t world_checksum(const World *w) {
  uint64_t h = 1469598103934665603ULL;  // FNV offset
  for (int i = 0; i < w->count; ++i) {
    h = (h ^ (uint64_t)w->ent[i].x) * 1099511628211ULL;
    h = (h ^ (uint64_t)w->ent[i].y) * 1099511628211ULL;
  }
  return h;
}

Log the checksum with its tick number on every client. When a report comes in, diff the logs: the first mismatching tick is your crime scene. This one habit has saved me more debugging nights than any other on this list.

Putting it together

Fixed timestep, fixed-point math, a stable update order, and a per-tick checksum — those four steps are the entire foundation. With them in place you can layer lockstep or rollback on top and trust that the hard part underneath holds. Skip any one and you'll spend release week chasing a desync that only appears on someone else's hardware.

The Ship It bundle includes the full netcode track — a working rollback layer built on this deterministic core, and a workshop where we intentionally break sync and hunt it down with the checksum log. Related: hot-reloading game code for fast iteration on the sim.