When I decided to rewrite Warman's multiplayer from the ground up, I went with lockstep simulation. The idea is simple: don't synchronise game state, synchronise inputs. Every client runs the exact same simulation. As long as the inputs are identical and the simulation is deterministic, the game state stays in sync without ever sending it over the wire.
This was a practical decision. Warman can have hundreds of enemies, projectiles, ground effects, and modifiers active at the same time. Synchronising all of that state every frame would be expensive. With lockstep, the network cost is constant regardless of game complexity. Whether there are 10 enemies or 500, the same tiny input packets get sent.
The Input Command
Every tick, each player produces a PlayerInputCommand. This struct contains everything the server needs to know about what a player is doing: a movement velocity vector, a cursor position relative to the player (for aiming), boolean flags for each skill slot, and an interact flag. There is also an array of UI commands for things like buying items, equipping gear, or selling loot.
Simulation Ticks
The host is authoritative. It collects inputs from all connected players into an input buffer, then assembles a SimulationTick, a struct that contains the tick number plus one PlayerInputCommand per player slot (up to 8). Before writing, a bitmask byte marks which slots actually have input, so empty player slots contribute zero bytes. The assembled tick is compressed and broadcast to all clients.
Clients receive these ticks and feed them into their local simulation in order. The simulation advances one tick at a time, applying each player's input to their character. Because the simulation is deterministic and the inputs are identical, all clients compute the same result.
Input Delay
Lockstep has a well-known problem: you can't simulate a tick until all inputs for that tick have arrived. If a player's input is late, everyone waits. The standard solution is input delay: when a player presses a button, that input doesn't take effect immediately, it's scheduled for a future tick.
Warman calculates input delay dynamically based on the measured one-way ping to the host. The formula divides the ping by the tick length (1000ms / tick rate) and rounds up, then adds a configurable jitter buffer. At 20 ticks per second with 50ms ping, that's about 2 ticks of delay, roughly 100ms. Most players don't notice anything under 150ms. For a singleplayer game or a host with no connected peers, the delay drops to 1 tick.
Missing Inputs
Network jitter means inputs sometimes arrive late. Rather than stalling the simulation, the host fills in missing inputs with a "best guess": it copies the movement velocity and cursor position from the player's most recent known input but zeroes out all action buttons. The player keeps walking and aiming in the same direction, but doesn't fire skills or interact with anything. The simulation keeps running while keeping the gameplay safe from phantom button presses.
Transport Abstraction
Warman supports two network transports behind a single IGameTransport interface: LiteNetLib (direct UDP) for non-Steam builds, and Steam P2P for Steam users. The Steam transport uses Valve's relay network, which handles NAT traversal so players don't need to port-forward. The transport choice is automatic. If the player launched through Steam, they get P2P. Otherwise, direct UDP with an explicit IP address.
Both transports support reliable ordered delivery (for simulation ticks and lobby management) and reliable unordered delivery (for chat and player inputs). The abstraction means the networking layer above doesn't care which transport is active. Starting a host, connecting as a client, sending data, and handling disconnections all go through the same interface.
When the Host Disconnects
One nice property of lockstep is that every client already has the full game state. If the host disconnects mid-game, the client doesn't need to tear everything down. Instead, it flips itself into singleplayer mode: the transport shuts down, the client promotes itself to host, other players get removed, and the simulation continues locally. From the player's perspective, their friends disappear and a chat message says "Game continues in singleplayer." No progress is lost.
This only works mid-game. If the host disconnects during the lobby phase (before the simulation has started), there's nothing to continue, so the client disconnects normally.
Determinism
The entire system depends on determinism. If two clients compute a different result from the same inputs, the game desyncs and everything breaks. This means no System.Random, no UnityEngine.Random, no floating-point operations that might differ across platforms. Warman uses a custom PRNG (MasterRandom) seeded from the game seed, and all gameplay code uses it exclusively. The random number generator is a subtractive generator based on Knuth's algorithm, chosen because it's simple, fast, and produces identical output across Windows, macOS, and Linux.