Who Needs This and What Goes Wrong Without It
If you are building a competitive multiplayer game—whether a real-time strategy title, a fighting game, or an arcade racing sim—deterministic networking is the foundation that ensures every player sees the same world. Without it, replays become useless, rollback netcode produces ghostly desyncs, and tournament fairness collapses. We have seen teams pour months into netcode only to discover that a single floating-point rounding difference between CPUs causes units to drift apart after 30 seconds of play.
The core problem is simple: each client must compute the same game state from the same inputs. But modern hardware is not naturally deterministic. GPUs reorder floating-point operations, compilers optimize expressions in subtle ways, and even the order of addition can change results. When a competitive match involves hundreds of thousands of state updates, tiny errors cascade into visible desyncs. Players notice when their replay shows a different outcome than the live game, or when a tournament observer sees a unit survive that was killed on screen.
Deterministic networking solves this by enforcing a strict computation model: all clients simulate the same tick with the same inputs and produce identical outputs. This is not optional for games that rely on replays, spectator modes, or deterministic rollback. Without it, you are left with state-synchronization approaches that send full world snapshots every frame—bandwidth-heavy and vulnerable to latency spikes. For competitive titles where every millisecond matters, a deterministic layer reduces bandwidth to input packets (a few bytes per tick) and offloads simulation to each client.
What Breaks First
Most teams encounter determinism failures in three areas: floating-point math, non-deterministic RNG, and unordered network delivery. Floating-point operations that produce slightly different results on different architectures (x86 vs. ARM, or even different compiler flags) are the most common culprit. We have seen a project where a simple sin() call on an Intel CPU gave a result that differed by 1e-12 from an AMD CPU, and after 10,000 ticks that error grew into a visible positional difference. Non-deterministic RNG—using system entropy sources or time-based seeds without explicit synchronization—causes desyncs that appear random and are nearly impossible to reproduce. Unordered or dropped inputs, if not handled with a consistent tick buffer, produce divergent state even when the simulation code is otherwise correct.
The audience for this guide is not beginners. We assume you have built a multiplayer loop before and are now facing the hard problem of making it deterministic across diverse hardware. If you are still debating UDP vs. TCP, start there first. Here we focus on the architecture that makes determinism possible.
Prerequisites and Context Readers Should Settle First
Before writing any network code, you must establish a deterministic simulation environment. This means controlling every source of non-determinism in your engine. The most critical step is to replace all floating-point arithmetic with fixed-point math or, if you must use floats, to ensure strict IEEE 754 compliance and disable fast-math compiler flags. Many engine teams adopt a fixed-point library (like libfixmath or a custom integer-based system) to guarantee identical results across platforms. The trade-off is reduced precision and more complex code for trigonometric functions, but for most game physics the range is sufficient.
Fixed-Point vs. Floating-Point
Fixed-point arithmetic uses integers with an implicit scaling factor. For example, a 32.32 fixed-point number stores 32 bits of integer part and 32 bits of fractional part. All operations are integer operations, which are deterministic across all compliant hardware. The downside is that multiplication and division require careful overflow handling, and trigonometric functions must be implemented via lookup tables or polynomial approximations. We recommend starting with a 16.16 fixed-point format for simplicity, then scaling up if precision is insufficient.
If you decide to stick with floating-point, you must enforce strict IEEE 754 behavior. This means compiling with -ffloat-store (on GCC/Clang) to avoid extra precision registers, disabling auto-vectorization that may reorder operations, and using #pragma STDC FENV_ACCESS ON to ensure rounding modes are consistent. Even then, different GPU architectures may produce different results for the same shader code. For this reason, many competitive games offload all simulation to the CPU and use the GPU only for rendering.
Tick Rate and Input Buffering
Deterministic networking relies on a fixed tick rate. Common choices are 60 Hz for fighting games and 30 Hz for RTS titles. Each tick consumes a set of inputs from all players, simulates one step, and produces a new state. The tick rate must be high enough to feel responsive but low enough to keep simulation load manageable. For a 60 Hz tick, each tick lasts 16.67 ms; your simulation must complete within that window on the weakest client.
Input buffering is essential. Clients send input packets for each tick, but packets may arrive late or out of order. The deterministic layer must buffer inputs and only advance the simulation when all inputs for a given tick are received (lockstep) or use prediction with later reconciliation (if you are building a more advanced rollback system). For pure determinism, lockstep is the simplest: all clients wait for the slowest network link. This introduces latency equal to the worst round-trip time, which can be unacceptable for fast-paced games. Rollback netcode relaxes determinism by allowing clients to predict inputs and then reconcile when the true inputs arrive, but the simulation itself must still be deterministic so that the rollback produces the same state as if the inputs had been applied in order.
Core Workflow: Building a Deterministic Network Layer
We now present a step-by-step workflow to implement a deterministic layer. This assumes you have a fixed-point math library and a fixed tick rate. The steps are ordered from foundation to optimization.
Step 1: Define a Deterministic Simulation Interface
Your game simulation must expose a pure function: State simulate(State previous, Inputs inputs). The state must be a flat structure (no pointers, no dynamic allocation) that can be serialized and hashed. Inputs are a packed structure of player actions. The function must have no side effects: no file I/O, no system time calls, no random numbers from external sources. If you need random numbers, use a deterministic PRNG seeded from the initial state (e.g., a simple LCG or xorshift).
Step 2: Implement Lockstep Input Delivery
Each client sends input packets for the current tick to a server (or directly to peers in P2P). The server collects all inputs for tick N and broadcasts them to all clients. Clients only simulate tick N when they have received the full input set. This guarantees that all clients simulate the same inputs in the same order. The downside is that the simulation stalls if one client's packet is delayed. To mitigate this, you can use a timeout and skip inputs (marking them as “no action”) or implement a prediction layer, but that introduces non-determinism unless you carefully reconcile.
Step 3: Add a Checksum for Debugging
After every tick, compute a checksum of the game state (e.g., CRC32 of the serialized state) and include it in the next input packet. The server can compare checksums from all clients. If they diverge, the server logs the tick number and the differing checksums. This is invaluable for debugging desyncs. In production, you may want to turn off checksum logging to save bandwidth, but keep it available for tournament modes.
Step 4: Implement Replay Recording and Playback
Since simulation is deterministic, a replay is just a sequence of input packets. Record all inputs from all players along with the initial state seed. To play back, simply simulate from the initial state using the recorded inputs. This works across different hardware because the simulation is deterministic. Ensure that your replay format includes the exact game version and any parameter changes (like map seed).
Tools, Setup, and Environment Realities
Choosing the right tools can make or break your deterministic layer. We compare three common stacks: C++ with manual FPU control, Rust with fixed-point crates, and Unity DOTS with custom burst-compiled jobs.
C++ with Manual FPU Control
C++ gives you full control over floating-point behavior. Use _controlfp on Windows or fesetround on POSIX to set rounding modes. Compile with /fp:strict on MSVC or -frounding-math on GCC. Consider using a fixed-point library like libfixmath or writing your own using int64_t. The main cost is development time: every math operation must be carefully reviewed.
Rust with Fixed-Point Crates
Rust's type system helps enforce determinism at compile time. Crates like fixed provide fixed-point types with configurable bit widths. The no_std support makes it suitable for console development. Rust's lack of undefined behavior in safe code reduces the risk of compiler-introduced non-determinism. However, you may need to write unsafe code for performance-critical sections.
Unity DOTS with Burst Compiler
Unity's Data-Oriented Tech Stack (DOTS) uses the Burst compiler, which can produce deterministic code if you avoid certain patterns. Burst compiles to native code with strict IEEE 754 semantics by default, but you must avoid using Mathf (which uses floats) and instead use fixed-point math via custom jobs. The entity-component-system architecture naturally fits the flat state requirement. The downside is that debugging is harder due to the job system and the lack of a REPL.
Environment Considerations
When building for multiple platforms, test determinism on every target early. We have seen a case where the same code produced different results on an Xbox Series X and a PC with an AMD GPU due to differences in the sqrt implementation. Always include a test suite that runs the same input sequence on all platforms and compares state checksums. Use continuous integration to run these tests nightly.
Variations for Different Constraints
Not every game needs pure lockstep determinism. We outline three common variations and their trade-offs.
Peer-to-Peer with Rollback
For fighting games and fast-paced shooters, rollback netcode reduces perceived latency. Each client predicts future inputs and simulates ahead. When the true input arrives, it rolls back to the tick where the input was actually applied and re-simulates. This requires deterministic simulation so that the rollback produces the correct state. The challenge is that rollback can be expensive if many ticks must be re-simulated. Implement a state cache that stores snapshots every few ticks to limit the rollback distance.
Dedicated Server with Authority
In this model, the server is the single authority. Clients send inputs, the server simulates the world, and sends state snapshots back. The server must still be deterministic for replays and anticheat, but clients do not need to simulate deterministically. This reduces client CPU load but increases bandwidth because state snapshots are larger than input packets. For competitive play, the server can run at a higher tick rate and use interpolation for client rendering.
Turn-Based with Lockstep
Turn-based games (like chess or card games) can use lockstep with no prediction. The server collects all inputs for a turn, broadcasts them, and each client simulates the turn. Latency is not critical because players wait for the turn to complete. Determinism is easier because there are no real-time constraints, and you can use strong checksums to verify. The main pitfall is handling disconnections: if a player drops, you need a reconnection protocol that resends missing inputs.
Pitfalls, Debugging, and What to Check When It Fails
Even with careful design, desyncs happen. The most common causes are subtle non-determinism in third-party libraries, incorrect serialization, and timing bugs. Here is a systematic debugging approach.
Checksum Drift Logging
Log the checksum of the game state at every tick on every client. When a desync is reported, compare the logs. The first tick where checksums differ is the point of divergence. Then examine the inputs for that tick: are they identical? If yes, the simulation code is non-deterministic. If no, the input delivery system is broken. We have seen cases where a client sent an extra input packet due to a timer bug, causing the server to include it in the wrong tick.
Replay Divergence Analysis
If replays desync, record the full input stream and the initial state. Then simulate the replay on two different machines with the same binary. If they diverge, the binary is non-deterministic. Use a binary diff tool to find the exact instruction that produces different results. This is easier with fixed-point math because integer operations are deterministic.
Common Pitfalls
- Non-deterministic RNG: Always use a deterministic PRNG seeded from the game state. Avoid
rand()orstd::mt19937without a fixed seed. - Uninitialized memory: Ensure all state is initialized to zero or a known value. Use a memory allocator that zeroes memory.
- Compiler optimizations: Disable auto-vectorization and fast-math. Test with different optimization levels.
- Threading: If you use multiple threads, ensure that the simulation runs on a single thread or that the thread scheduling is deterministic. Use a deterministic task scheduler.
- FPU control word: Some libraries change the FPU rounding mode. Save and restore the control word around library calls.
Network Jitter Buffers
Jitter buffers can introduce non-determinism if they reorder packets. Use a buffer that delivers packets in tick order, not arrival order. If a packet arrives late, either skip it (marking inputs as “no action”) or stall the simulation. Stalling is safer for determinism but increases latency.
FAQ and Checklist
Q: Can I use floating-point math if I only target one platform?
A: Even on the same platform, different CPUs (Intel vs. AMD) or different compiler versions can produce different results. We strongly recommend fixed-point for any competitive game.
Q: How do I handle hash collisions in checksums?
A: Use a strong hash like CRC64 or SHA-256. The probability of collision is negligible for game state sizes. If you are paranoid, compare the full state when checksums match but a desync is suspected.
Q: Is there a way to make GPU shaders deterministic?
A: Not reliably. GPU architectures have non-deterministic scheduling and floating-point behavior. Keep simulation on the CPU.
Q: What about deterministic physics engines?
A: Most physics engines (like Box2D) are not deterministic out of the box. You must use fixed-point or a special deterministic build. Box2D has a deterministic mode using fixed-point math.
Q: How do I handle player disconnection in lockstep?
A: The server can pause the game until the player reconnects, or replace missing inputs with a “no action” or an AI prediction. The latter breaks determinism unless you define a deterministic AI.
Quick Checklist
- Replace all floats with fixed-point or enforce strict IEEE 754.
- Use a deterministic PRNG seeded from the initial state.
- Implement a flat serializable state structure.
- Use lockstep input delivery or rollback with deterministic reconciliation.
- Add per-tick checksum logging for debugging.
- Test determinism on all target platforms in CI.
- Disable compiler optimizations that reorder floating-point operations.
- Ensure all memory is initialized.
Next moves: If you are starting fresh, prototype with a simple lockstep loop and a fixed-point math library. Write a test that simulates 10,000 ticks with random inputs on two different machines and verifies checksums match. Once that passes, add rollback. Then integrate with your game's simulation. Finally, set up automated desync detection in your live game to catch regressions early.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!