Client-Side Prediction & Server Reconciliation
⏱️ Client-Side Prediction & Server Reconciliation
In an authoritative multiplayer game, the server is the single source of truth. However, internet latency means any round-trip to the server takes anywhere from 30ms to 200ms.
If a player presses "Move Forward" and has to wait 100ms for the server to reply before moving, the game feels unresponsive, sluggish, and unplayable.
To solve this, modern competitive games (Source engine, Unreal, Quake, Overwatch) use Client-Side Prediction, Server Reconciliation, and Entity Interpolation.
🏛️ The Three Netcode Pillars
+-------------------------------------------------------------+
| 1. Client-Side Prediction (Local Player) |
| - Process input immediately locally (0ms perceived lag) |
| - Store (Input, SequenceID) in local history buffer |
+-------------------------------------------------------------+
│
(Send Inputs via UDP)
▼
+-------------------------------------------------------------+
| 2. Authoritative Server (The Truth) |
| - Runs simulation, validates movement, prevents cheats |
| - Broadcasts (State, LastProcessedSequenceID) to client |
+-------------------------------------------------------------+
│
(State Snapshot back)
▼
+-------------------------------------------------------------+
| 3. Server Reconciliation (Local Correction) |
| - If server state != predicted state: |
| * Snap position to server state |
| * Replay all unacknowledged inputs in history buffer |
+-------------------------------------------------------------+
🏎️ 1. Client-Side Prediction (Local Player)
When the local player presses W:
- Generate an
InputCmdstruct with a monotonicsequence_idand the currentdelta_time. - Apply the physics movement immediately on the local client character. The player feels zero input latency.
- Save the input into a fixed-size Input Ring Buffer (e.g., last 128 inputs).
- Send the
InputCmdto the server over UDP.
Odin Input Command Struct Example
Input_Command :: struct #packed {
sequence_id: u32,
move_dir: [2]f32, // X, Y movement input
view_yaw: f32,
buttons: bit_set[Player_Button; u8], // Jump, Fire, Crouch
delta_time: f32,
}
🔄 2. Server Reconciliation (Handling Mispredictions)
The server receives the client's inputs, executes them inside its authoritative simulation, and periodically sends back a state update containing:
positionandvelocitylast_processed_sequence_id
When the client receives this packet:
- Discard Acknowledged Inputs: Remove all inputs from the ring buffer with
sequence_id <= last_processed_sequence_id. - Check for Divergence: Compare the server's authoritative position against where the client thought it was at that sequence ID.
- Reconcile (Rollback & Replay):
- If the error exceeds an acceptable threshold (e.g., > 2 centimeters, perhaps due to colliding with an enemy the client hadn't seen yet):
- Snap client position back to the server's snapshot position.
- Instantly re-run all remaining unacknowledged inputs in the ring buffer in sequence!
Snapping immediately can cause visual "snapping" or "rubberbanding." Modern engines separate the Simulation Position from the Render Mesh Position, smoothing out corrections over 2–3 visual frames using interpolation.
👥 3. Entity Interpolation (Remote Players)
You cannot predict other players because you cannot predict human intent. If remote player updates arrive from the server at 20Hz (every 50ms), rendering them directly causes them to "jitter" across the screen.
The Solution: Render remote entities slightly in the past (typically
Received Snapshots: [Snap 1 (t=0ms)] ──────> [Snap 2 (t=50ms)] ──────> [Snap 3 (t=100ms)]
▲
Render Time (t=25ms): │
Interpolate between Snap 1 & Snap 2: [Render Entity Here]
By buffering 2 or 3 snapshots, the client always has a "from" and "to" state, allowing smooth linear or Hermite spline interpolation (
🎯 4. Lag Compensation (Hitscan Rewind)
If you shoot at a remote player who is rendered 100ms in the past due to interpolation, aiming directly at their model would cause you to miss on the server!
To solve this, the server performs Lag Compensation:
- When the client clicks "Fire," it sends the command with the exact server timestamp/tick they were looking at.
- The server receives the shot, pauses the current simulation, and temporarily rewinds all other players' collision hitboxes back in time to match that exact timestamp.
- The server traces the ray against the rewound hitboxes.
- The server registers the hit, and returns all hitboxes back to the current server time.
This guarantees that if the target was in your crosshairs when you clicked, the shot connects.
📊 Summary: The Three Player Perspectives
| Perspective | Entity | Technique Used |
|---|---|---|
| Local Player | Yourself | Client-Side Prediction & Reconciliation (Instant response, replayed on error) |
| Remote Players | Enemies & Allies | Snapshot Interpolation (Buffered 50–100ms in the past for buttery smooth movement) |
| Hit Verification | Shooting / Melee | Server Lag Compensation (Server rewinds history to verify raycast) |
🔗 Related Notes
- Basics Of NetWorking — UDP vs TCP, sockets, and reliability layers.
- Network Serialization & Delta Compression — Compressing network packets to fit in MTU.
- Types in Odin —
#packedmemory structures for network payloads. - Optimization — Eliminating CPU hitches during physics replay loops.
- Game Development Hub — Central game development index.