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:

  1. Generate an InputCmd struct with a monotonic sequence_id and the current delta_time.
  2. Apply the physics movement immediately on the local client character. The player feels zero input latency.
  3. Save the input into a fixed-size Input Ring Buffer (e.g., last 128 inputs).
  4. Send the InputCmd to 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:

When the client receives this packet:

  1. Discard Acknowledged Inputs: Remove all inputs from the ring buffer with sequence_id <= last_processed_sequence_id.
  2. Check for Divergence: Compare the server's authoritative position against where the client thought it was at that sequence ID.
  3. 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!
Smooth Correction

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 t−100ms, known as the Interpolation Delay).

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 (LERP) between updates.


🎯 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:

  1. When the client clicks "Fire," it sends the command with the exact server timestamp/tick they were looking at.
  2. 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.
  3. The server traces the ray against the rewound hitboxes.
  4. 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)