All tutorials Mighty Professional
Build a Game Engine · Networking

Networking: Netcode & Rollback

A player in New York presses jump. A server in London can't hear about it for at least 27 ms, because that's how long light takes through fiber, and real networks add delay, lose packets and deliver them out of order on top. No netcode beats those constraints; it hides them. This page builds the standard tools so your own character responds instantly, other players move smoothly, and shots land where the shooter aimed: client-side prediction and reconciliation, entity interpolation, lag compensation and rollback.

Time~25 min LevelSenior PrereqsThe Game Loop (the fixed timestep), Floating Point (cross-machine determinism), and Compression (packing packets). StackC++ & Rust
◂ Build a Game Engine Phase 12 · Networking Next · Debugging in Release ▸
What you'll be able to do by the end

01Why it's hard

Two players' machines need to agree on one game, but every message between them takes time and some never arrive. Netcode works against five constraints: latency (the delay before a packet arrives), jitter (variance in latency), packet loss, bandwidth, and cheating. Latency has a floor you cannot beat: light in fiber travels at roughly 200,000 km/s, so New York to London (~5,500 km great-circle) is about 55 ms round-trip of pure propagation, before cable detours, routing, or queuing add more. The network feeds each peer the inputs or state for the fixed tick it's about to run.

Worked example

New York to London is about 5,500 km. At 200,000 km/s in fiber, one way takes 5,500 / 200,000 = 0.0275 s, so 27.5 ms, and the round trip 55 ms. At 60 frames per second a frame lasts 16.7 ms, so even a perfect network puts the reply 3.3 frames after the press. Every technique on this page is a way of not making the player feel those frames.

02UDP vs TCP

A shooter sends the world's state 20 or more times a second, and a state that arrives late is already useless. Fast-paced games run that realtime stream over UDP, fire-and-forget packets with no delivery or ordering guarantee, not TCP, a reliable, in-order byte stream. TCP guarantees reliable, in-order delivery, and that guarantee is the problem: a single lost packet causes head-of-line blocking. TCP won't hand over any later data that already arrived until the retransmit lands (≥1 RTT later), freezing the stream in whichever direction lost the packet[2].

"Never use TCP" is wrong; "never for time-critical data" is right

TCP is fine for non-realtime channels: login, matchmaking, chat, patch downloads, REST calls. Slower genres (MMOs, turn-based games) ship whole gameplay streams on it. The precise rule (Fiedler's) is never use TCP for time-critical data. For the realtime stream, fast-paced games build a thin partial-reliability layer on UDP: sequence numbers plus an ack bitfield (each packet carries the latest received sequence and a 32-bit field of the prior 32, so every ack is effectively sent 32 times for redundancy), with unacked reliable messages re-included in outgoing packets until they are acked, so a lost packet never blocks the realtime stream[3].

Worked example

A client receives 60 state packets a second over a 100 ms round trip. Over TCP, one lost packet holds back everything behind it until the resend lands, at least 100 ms later: 6 packets' worth of newer state sits in the receive buffer, and the game freezes, then jumps. Over UDP the game uses packet 1,001 as soon as it arrives and never misses 1,000. For the acks, a packet carrying "latest received: 1,000" plus a 32-bit field for 999 down to 968 acknowledges up to 33 packets at once, so the sender learns about packet 1,000 from any of the next 32 packets that get through.

03The three models

An RTS with 1,500 units, a 64-player shooter and a two-player fighting game have very different problems: too much state to send, players who might cheat, and timing tight enough that a few frames decide a match. Three archetypes, each suited to a different genre. They differ in what crosses the wire (inputs vs state), who is authoritative (peers vs a server), and whether a peer waits for remote inputs or predicts them.

ModelWire / authorityGenre & tradeoff
Deterministic lockstepInputs only; peers (P2P)RTS. Tiny bandwidth, but needs perfect determinism + waits for the slowest peer
Client-server authoritativeState; server is truthFPS. Cheat-resistant; needs prediction to feel responsive
RollbackInputs only; peersFighting games. Lockstep that predicts remote inputs instead of waiting; needs determinism + cheap state save/restore

A fourth model sits between the first two: state synchronization runs the sim on both sides and sends inputs plus state, so the state corrects drift and perfect determinism isn't required[14].

04Lockstep & determinism

In , every peer runs the identical simulation and only inputs (commands) cross the wire, so bandwidth is proportional to input size, not world-object count[4]. Age of Empires passed commands rather than per-unit state to hit its 1,500-unit target on a 28.8k modem; passing per-unit position and status would have capped it at about 250 moving units[5]. Commands are scheduled a couple of turns ahead so transmission overlaps simulation.

Worked example

A 28.8 kbit/s modem moves 28,800 / 8 = 3,600 bytes a second. Say each unit's position and status take 8 bytes: 1,500 units are 12,000 bytes, more than 3 seconds of modem time for a single update. A command such as "these 40 units attack that building" is a few dozen bytes, whatever the units then do for the next minute.

Two hard costs: the slowest peer, and determinism

Lockstep runs at the speed of the slowest, laggiest peer (everyone needs turn N's inputs before simulating turn N), and a single non-deterministic divergence desyncs everyone, permanently, and compounds over time[1]. Across machines, the classic culprit is floating point: transcendentals not correctly rounded, non-associativity under compiler/SIMD reordering, -ffast-math, x87 vs SSE in legacy 32-bit builds, and FMA contraction and libm differences across x86/ARM (the Floating Point tutorial covers the mechanism)[6]. The fixes are fixed-point math or tightly controlled float. Floats aren't the only source: Age of Empires' programmers had to keep the number of calls to the (seeded, synchronized) random generator identical on every machine[5], and uninitialized memory or iteration over pointer-keyed containers diverge the same way. A fixed timestep is necessary but not sufficient, and using doubles doesn't fix it. Those two costs keep many engines off lockstep.

05Prediction & reconciliation

You press forward in a shooter with a 120 ms round trip to the server. If your character waits for the server's answer, it starts moving 120 ms after your finger did, which feels like steering through mud. Under an authoritative server, waiting for the round-trip means input lag of at least the full RTT (server processing and tick alignment add more). applies your own input immediately to your local copy, tagging each input with a sequence number and storing it in a pending buffer[7]. Each server update then describes a moment the client has already predicted past, so the two disagree until the client reconciles.

Reconciliation replays; it does not just snap

The server's state update carries the sequence number of the last input it processed. On receipt the client: (1) snaps to the authoritative state, (2) discards pending inputs up to that ack, (3) replays the still-unacknowledged inputs on top. Replaying is what keeps your predicted position correct when the server agrees. A correction/rubber-band appears only when it genuinely disagreed (a misprediction, e.g. you got shoved); some engines blend that correction over a short window instead of popping, as Source does with cl_smoothtime[9]. Prediction makes your character responsive; it does nothing for other players (that's interpolation, §6).

Worked example

At a 120 ms round trip and one input per 60 Hz frame, about 0.12 × 60 ≈ 7 inputs are in flight at any moment. A server update arrives acking input 500, and the client has inputs 501 to 507 pending. It snaps to the server's position for input 500, drops 500 and everything older from its buffer, and re-applies 501 to 507. If the server agreed, that lands exactly where the client already was and nothing moves. If another player shoved you at input 498, the replay starts from the shoved position and you see the correction.

The character follows a moving input; raise the latency. With no prediction it lags behind your input by the full round trip. Prediction with no reconciliation feels instant but never corrects its mispredictions, so it drifts off the authoritative position and never recovers. Reconciliation is the half that makes prediction usable: it snaps the character back onto authority and replays your unacked inputs:

Pick a mode and drag the round-trip slider. The blue ring is where the server says you actually are; the green dot is what your client draws; the colored bar between them is the prediction error. No prediction: the character renders the late server state, so it trails your input by the full round trip (the demo lumps both legs of the trip into the snapshot's return path). Prediction only: your own input is applied instantly, but the server keeps applying input the client could not predict (another player shoved you, or the server resolved a collision you had not seen), and with no reconciliation that error accumulates: the green dot drifts off the blue ring and never recovers. Prediction + reconciliation: same instant response, but every server update the client snaps to authority and replays your unacked inputs, so the error collapses back toward zero. The snap is invisible when the server agreed and pops as a rubber-band only when it genuinely disagreed. The input buffer is keyed by timestamp here; the code panel below keys the same snap-and-replay by sequence number.
Client prediction + reconciliation (replay, not snap)
#include <cstdint>
#include <deque>

std::deque<Input> pendingInputs;          // unacknowledged inputs, in order
uint32_t inputSequence = 0;
State predictedState;                       // what we render locally

void onLocalInput(float dx, float dt) {
    Input input{ ++inputSequence, dx, dt };
    applyInput(predictedState, input);      // PREDICT: apply now, don't wait for the server
    sendToServer(input);                    // the server will ack it by sequence number
    pendingInputs.push_back(input);          // keep it until acked
}
void onServerState(const State& serverState, uint32_t lastProcessedInput) {
    predictedState = serverState;            // RECONCILE 1: snap to authority
    while (!pendingInputs.empty() && pendingInputs.front().sequence <= lastProcessedInput)
        pendingInputs.pop_front();           // drop what the server already ran
    for (const Input& input : pendingInputs)
        applyInput(predictedState, input);  // RECONCILE 2: REPLAY unacked inputs
}
fn on_local_input(&mut self, dx: f32, dt: f32) {
    self.input_sequence += 1;
    let input = Input { sequence: self.input_sequence, dx, dt };
    apply_input(&mut self.predicted_state, &input);  // PREDICT immediately
    send_to_server(&input);                          // send before the buffer takes ownership
    self.pending_inputs.push_back(input);            // keep it until acked
}
fn on_server_state(&mut self, server_state: State, last_processed: u32) {
    self.predicted_state = server_state;             // RECONCILE: snap to authority
    while self.pending_inputs.front().is_some_and(|i| i.sequence <= last_processed) {
        self.pending_inputs.pop_front();             // drop acked
    }
    for input in &self.pending_inputs {
        apply_input(&mut self.predicted_state, input);  // REPLAY unacked
    }
}

06Interpolation

Other players' positions arrive 20 times a second, but your screen draws 60 or more frames a second. Draw each update as it arrives and they hop. The snapshot model doesn't predict other players: they stop, turn, and accelerate unpredictably, so every misprediction would pop (rollback in §9 and dead reckoning accept that cost). Instead, buffer their timestamped snapshots and render them at now − interpolationDelay, between the two snapshots that bracket that render time[8]. Source defaults to cl_interp 0.1 = 100 ms of view delay, derived from its 20 snapshot/s default and sized so a single lost snapshot still leaves two to interpolate between. The effective period is max(cl_interp, cl_interp_ratio / cl_updaterate), so with cl_interp set to 0, raising the update rate shrinks the delay well below 100 ms[9].

You see others in the past, deliberately

Interpolation delay is latency added on purpose, bought in exchange for smooth motion. It's interpolation between two real snapshots, not extrapolation, so it only coasts/extrapolates (and can overshoot) when snapshots are lost or arrive later than the buffer allows; Source caps that extrapolation at 0.25 s[9]. And it's coupled to lag compensation: the server subtracts this same delay when it rewinds time to validate your shots (§8).

Worked example

At Source's default 20 snapshots a second, snapshots are 50 ms apart, say stamped 0, 50, 100 and 150 ms (ignore travel time for the moment). With 100 ms of delay, at 175 ms the client draws the world as it was at 75 ms, halfway between the 50 and 100 snapshots. If the 100 ms snapshot is lost, the client blends between 50 and 150 instead, a 100 ms gap but still two real positions. Lose two in a row and it has nothing newer to blend toward, so it coasts.

A remote entity moving: compare raw snapshots (teleporting) to interpolation (smooth, but lagging), and drop packets to see it coast:

Switch between raw and interpolated, then drag the packet-loss slider. Raw snapshots arrive ~10 times a second, so the entity teleports between them. Interpolated is smooth but visibly trails the true position (the faint ring) by the interpolation delay, 250 ms here, exaggerated for visibility. Raise packet loss and the interpolated entity coasts past gaps, the failure mode when too many snapshots drop.

07Snapshots & delta

A 64-player match has hundreds of moving objects, and most of them didn't change since the last packet. Sending the whole world 20 times a second to every client wastes most of the bandwidth. A client-server engine takes a snapshot of world state and sends each client the delta against the last snapshot that client acknowledged[10]. The snapshot rate can sit below the tick rate: Counter-Strike: Source and TF2 simulate at 66 ticks/s, but Source clients request 20 snapshots/s by default (cl_updaterate 20)[9]. Quake 3 keeps the last 32 snapshots per client and deltas against the client's last acked one; if none is acked (heavy loss), it deltas against a zeroed baseline, which is just a full update. A lost ack self-heals: the next delta is computed from an older baseline (bigger, but correct) rather than a forced full resend.

Quantize and bit-pack the fields

Then quantize each field to the bits it needs (cross-ref Bit Shifting and Compression): map a position float over a known range to an integer (Fiedler's example packs x,y,z into 18/18/14 bits, ~2 mm precision, instead of 96) and orientation into a smallest-three quaternion (2 bits for the largest-component index + 3×9 bits = 29 bits vs 128)[11]. Quantization is lossy but bounded: picking the range and precision is the design choice, and out-of-range values clamp.

Worked example

At 1/512 m steps (about 2 mm), 18 bits give 218 = 262,144 steps, which covers 262,144 / 512 = 512 m; 14 bits cover 16,384 / 512 = 32 m, enough for height. That's 18 + 18 + 14 = 50 bits for a position instead of 96 for three floats, and 29 bits for an orientation instead of 128, so 79 bits per object instead of 224, about 35% of the size, before delta encoding drops the objects that didn't move at all.

08Lag compensation

You line up a headshot on a player running past a doorway and click. Your screen showed them in the doorway, but by the time your shot reaches the server they're behind the wall. Should it hit? When validating a hit, the authoritative server rewinds the other players to where the shooter saw them. To do that it needs the moment the shooter was looking at, and Valve estimates it as:

Command Execution Time = Current Server Time − Packet Latency − Client View Interpolation

In words: start from the server's clock now, go back by the time the shot's packet took to arrive, then go back again by the interpolation delay the shooter was seeing other players with.

The formula subtracts the interpolation delay from §6: the two systems are coupled[9]. The server keeps about 1 second of position history, moves the candidates back, tests the hit, and restores them.

Worked example

The shot arrives at server time 10.000 s. It took 40 ms to get there, and the shooter renders other players 100 ms in the past, so the server rewinds everyone to 10.000 − 0.040 − 0.100 = 9.860 s. The target stepped behind the wall at 9.900 s, but at 9.860 s they were still in the doorway, so the hit counts, even though on the server the target had been behind the wall for 100 ms.

It favors the shooter, by design

Lag compensation makes you hit what your screen showed, at the cost of the target: you can be killed after you have already ducked behind cover, because on the shooter's machine you were still exposed. Valve is explicit that this "can't be solved in general because of the relatively slow packet speeds." The trade is shooter feel against target fairness. Source rewinds only players; every other entity stays at its current position[9].

09Rollback

In a fighting game a punch can be active for only three frames, so a few frames of delay change which hits land. Waiting for the opponent's input before simulating each frame adds that delay to every frame. (GGPO, the fighting-game standard) runs a deterministic sim and predicts the remote player's input (assume they keep doing what you last heard, "carry-forward"), simulating forward immediately so the game feels offline-responsive. When the real input arrives and differs, it rolls back to the saved state at that frame and re-simulates forward to the present with the corrected input[12].

Not lag-free, and it demands two things at once

Rollback hides remote latency but doesn't erase it: a misprediction produces a visible correction/teleport. It requires both a fully deterministic sim and the ability to save/restore the entire game state cheaply every frame (ideally one contiguous block you can memcpy). The cost scales with the misprediction window (latency in frames, minus any input delay): at 60 fps with three frames of input delay, supporting 300 ms of one-way latency means re-simulating up to 15 frames inside one 16.6 ms display frame; overrun that budget and the resim becomes its own spiral of death[13]. It predicts the remote input; your own is applied directly. Full-state rollback best fits 2-player P2P with small state. In a 64-player shooter the state to save is large, each resim steps every player, and with 63 remote inputs to guess, some guess is wrong on most frames, so a near-full-window resim runs almost every frame. The idea survives there in narrower form: §5's snap-and-replay reconciliation is a rollback of your own predicted state.

Worked example

At 60 fps, 300 ms of one-way latency is 0.3 × 60 = 18 frames. Three frames of input delay cover 3 of them, leaving a 15-frame window: when a late input turns out to differ from the guess, the game restores the state from 15 frames ago and steps the simulation 15 times, plus the current frame, inside one 16.7 ms display frame. That's about 1 ms per simulation step, before rendering gets any time at all.

The widget predicts the remote input; when a real input arrives that differs, the sim rolls back N frames and re-simulates. Raise latency to grow the rollback window:

Drag the latency and mispredict sliders and watch the frame timeline and the remote character. Each frame the remote input is predicted and the sim runs ahead. When a real input differs, the timeline rolls back to the saved frame and re-simulates forward (fast but visible), and the remote character pops to its corrected spot. Raising latency widens the window: more frames to re-simulate per correction, and a bigger pop because more frames ran on the wrong input. The faint ring is the remote's actual position, which the local sim only learns about one latency window later; the fading red outline marks where the mispredicted present had been drawn before the correction. Rollbacks still fire with the mispredict slider at zero: every turn at a track edge is an input the predictor could not know in advance, so it mispredicts by construction.
The rollback core (predict remote, roll back, re-simulate)
constexpr int MAX_ROLLBACK = 8;             // prediction window; a real implementation stalls the sim past this
GameState savedStates[MAX_ROLLBACK];        // ring of per-frame saves (must be cheap to copy)
Input remoteInputs[MAX_ROLLBACK];            // remote input each frame was SIMULATED with (predicted or real)
GameState currentState;                     // the live simulation state
int confirmedFrame = -1;                    // last frame with a REAL remote input
int presentFrame = 0;                       // newest frame advance() has simulated

Input predictRemote() {                       // carry-forward: keep doing what we last heard
    return remoteInputs[(confirmedFrame >= 0 ? confirmedFrame : 0) % MAX_ROLLBACK];
}
void advance(int frame, Input local) {
    presentFrame = frame;
    savedStates[frame % MAX_ROLLBACK] = currentState;    // SAVE before stepping
    Input predicted = predictRemote();
    remoteInputs[frame % MAX_ROLLBACK] = predicted;      // RECORD the guess so onRemoteInput can compare
    simulate(currentState, local, predicted);            // step with the PREDICTED remote input
}
void onRemoteInput(int frame, Input real) {
    if (real == remoteInputs[frame % MAX_ROLLBACK]) { confirmedFrame = frame; return; } // guessed right
    remoteInputs[frame % MAX_ROLLBACK] = real;
    confirmedFrame = frame;                              // move BEFORE resim: carry-forward uses the new input
    currentState = savedStates[frame % MAX_ROLLBACK];    // ROLL BACK to that saved frame
    for (int f = frame; f <= presentFrame; ++f) {        // RE-SIMULATE forward to now
        savedStates[f % MAX_ROLLBACK] = currentState;    // RE-SAVE: a later rollback needs corrected states
        Input remote = (f == frame) ? real : predictRemote();
        remoteInputs[f % MAX_ROLLBACK] = remote;         // re-record what this frame actually ran with
        simulate(currentState, localInputAt(f), remote);
    }                                                    // the re-simulated present renders next: the visible pop
}
const MAX_ROLLBACK: usize = 8;                   // prediction window; a real implementation stalls the sim past this

struct Rollback {
    saved_states: [GameState; MAX_ROLLBACK],     // ring of per-frame saves (must be cheap to copy)
    remote_inputs: [Input; MAX_ROLLBACK],        // remote input each frame was SIMULATED with (predicted or real)
    current_state: GameState,                    // the live simulation state
    confirmed_frame: i32,                        // last frame with a REAL remote input (-1: none yet)
    present_frame: usize,                        // newest frame advance() has simulated
}

impl Rollback {
    fn predict_remote(&self) -> Input {          // carry-forward: keep doing what we last heard
        self.remote_inputs[self.confirmed_frame.max(0) as usize % MAX_ROLLBACK]
    }
    fn advance(&mut self, frame: usize, local: Input) {
        self.present_frame = frame;
        self.saved_states[frame % MAX_ROLLBACK] = self.current_state;  // SAVE before stepping
        let predicted = self.predict_remote();
        self.remote_inputs[frame % MAX_ROLLBACK] = predicted;  // RECORD the guess so on_remote_input can compare
        simulate(&mut self.current_state, local, predicted);   // step with the PREDICTED remote input
    }
    fn on_remote_input(&mut self, frame: usize, real: Input) {
        if real == self.remote_inputs[frame % MAX_ROLLBACK] { self.confirmed_frame = frame as i32; return; }  // guessed right
        self.remote_inputs[frame % MAX_ROLLBACK] = real;
        self.confirmed_frame = frame as i32;                    // move BEFORE resim: carry-forward uses the new input
        self.current_state = self.saved_states[frame % MAX_ROLLBACK];  // ROLL BACK to that saved frame
        for f in frame..=self.present_frame {                   // RE-SIMULATE forward to now
            self.saved_states[f % MAX_ROLLBACK] = self.current_state;  // RE-SAVE: a later rollback needs corrected states
            let remote = if f == frame { real } else { self.predict_remote() };
            self.remote_inputs[f % MAX_ROLLBACK] = remote;      // re-record what this frame actually ran with
            simulate(&mut self.current_state, local_input_at(f), remote);
        }                                                       // the re-simulated present renders next: the visible pop
    }
}

10Choosing a model

You're starting a multiplayer game and have to commit to a model before writing the simulation, because lockstep and rollback both demand determinism from day one. Each model fits a genre's constraints:

The engine seam

One common arrangement: a network thread receives and parses packets and hands inputs/snapshots to the simulation thread over a lock-free SPSC queue, so wire I/O never stalls the fixed-timestep loop; plenty of shipped games instead poll non-blocking sockets at the top of each frame. Either way, every model here sits on top of that fixed tick and the determinism discipline from the Floating Point tutorial.

11Pitfalls

"Never use TCP"Never for time-critical data. TCP is fine for login/chat/downloads.
Lockstep sending stateIt sends inputs; that's the bandwidth win. State is the snapshot model.
"Fixed timestep gives determinism"Necessary, not sufficient. Floats diverge across machines.
Reconciliation that only snapsReplay the unacked inputs, or you stutter every packet.
Predicting other playersIn the snapshot model, interpolate them (render the past); rollback (§9) is the model that predicts them.
Delta vs the latest sent snapshotDelta against the last ACKED snapshot, not the latest sent.
"Rollback is lag-free"It hides latency; mispredictions pop. Needs determinism + cheap save/load.
One model for everythingLockstep/RTS, client-server/FPS, rollback/fighting. Pick per constraints.

12What's next

Two tooling modules remain before the finale. Debugging in Release covers reading the optimized binary, sanitizers and crash dumps; Tooling & Profiling adds the in-engine dev UI and profilers to see and tune everything you've built. Then the 3D-game capstone assembles the whole engine into a game. The full path is on the series hub.

  1. Glenn Fiedler. "What Every Programmer Needs To Know About Game Networking." gafferongames.com. The P2P→client-server history, the lockstep-waits-for-the-slowest-peer point, and that one tiny difference desyncs everyone.
  2. Glenn Fiedler. "UDP vs. TCP." gafferongames.com. Head-of-line blocking; "never use TCP for time-critical data"; TCP for non-critical services.
  3. Glenn Fiedler. "Reliable Ordered Messages." gafferongames.com. Sequence numbers + the 32-bit ack bitfield, and priority-based partial reliability over UDP.
  4. Glenn Fiedler. "Deterministic Lockstep." gafferongames.com. Inputs-only, bandwidth proportional to input not object count, and the cross-platform determinism warning.
  5. Paul Bettner and Mark Terrano. "1500 Archers on a 28.8: Network Programming in Age of Empires and Beyond." GDC 2001. gamedeveloper.com. The lockstep RTS case: pass commands, 1,500 units vs a 250-unit cap for passing state, commands scheduled two turns ahead, identical random-call counts on every machine, and running as fast as the slowest machine.
  6. Glenn Fiedler. "Floating Point Determinism." gafferongames.com. Why cross-machine float results diverge and the forced-precision / wrapped-transcendental fixes (also cited in the Floating Point tutorial).
  7. Gabriel Gambetta. "Client-Side Prediction and Server Reconciliation." gabrielgambetta.com. Per-input sequence numbers, the server echoing the last-processed input, and replaying unacked inputs.
  8. Gabriel Gambetta. "Entity Interpolation." gabrielgambetta.com. Rendering other entities in the past by interpolating between two bracketing snapshots.
  9. Valve. "Source Multiplayer Networking" / "Lag Compensation." developer.valvesoftware.com. The 100 ms cl_interp default and the max(cl_interp, cl_interp_ratio/cl_updaterate) period, the 20 snapshot/s default vs tick 66, the Command-Execution-Time formula, the 1 s history, the favor-the-shooter "behind cover" tradeoff, prediction-error smoothing (cl_smoothtime), and the 0.25 s extrapolation cap.
  10. Fabien Sanglard. "Quake 3 Source Code Review: Network." fabiensanglard.net. The 32-snapshot ring, delta against the last acked snapshot, and the zeroed baseline as a full update.
  11. Glenn Fiedler. "Snapshot Compression." gafferongames.com. Position quantization over a bounded range and the smallest-three quaternion.
  12. GGPO. ggpo.net (open source: github.com/pond3r/ggpo). Input prediction + speculative execution and re-simulation from the point of divergence; the deterministic-sim + save/load requirement.
  13. SnapNet. "Netcode Architectures Part 2: Rollback." snapnet.dev. State ideally one contiguous block you can memcpy, carry-forward prediction, input decay, and the 15-frames-in-16.6 ms resim budget.
  14. Glenn Fiedler. "State Synchronization." gafferongames.com. Sending input and state together, so the simulation needn't be perfectly deterministic to stay in sync.

See also