Snapshot Delta Compression in Multiplayer Games
In multiplayer game development, transmitting full world states every tick rapidly saturates network bandwidth, leading to high packet sizes and server congestion. Snapshot delta compression addresses this by sending only the differences (deltas) between the current game state and a previously acknowledged baseline state rather than full snapshots. This article explains how developers implement this technique through state history buffers, bit-level diffing, variable-rate quantization, and robust packet loss handling to significantly decrease per-tick network consumption.
Maintaining State Buffers and Baselines
To calculate state differences, both the server and the client must maintain a circular buffer of historical world snapshots indexed by a sequence number or tick ID.
- Client Acknowledgment: When the client receives and applies a snapshot, it returns an acknowledgment (ACK) containing that snapshot’s sequence ID in its regular user-command packets.
- Baseline Selection: When the server prepares a packet for the current tick (\(T_{current}\)), it checks the client's most recently acknowledged tick (\(T_{ack}\)). The snapshot at \(T_{ack}\) becomes the baseline.
- Buffer Management: The server stores snapshots for a sliding window of time (typically 1 to 2 seconds). If a client encounters extreme packet loss and fails to acknowledge a tick within this window, the server falls back to transmitting a full baseline snapshot to reset synchronization.
Entity and Property Diffing
Once a baseline is chosen, the server compares the current state to the baseline state across all game objects:
- Entity Lifecycle Flags: The server evaluates which entities were created, modified, or destroyed. A set of ID lists or bit flags indicates new entities (requiring full baseline data), deleted entities (requiring only an ID to destroy), and continuous entities (requiring property diffing).
- Change Masks (Dirty Bits): For modified entities,
developers use a bitmask where each bit represents a specific replicated
property (such as position, health, or animation state). The server
compares the current value against the baseline value. If the values
match, the bit is set to
0. If they differ, the bit is set to1, and the updated value is staged for serialization.
Quantization and Bit-Level Packing
Sending raw data types for modified values is inefficient. Developers compress the surviving delta fields using quantization and bit-packing:
- Fixed-Point Quantization: Floating-point vectors (e.g., world coordinates) are mapped to lower-precision integers based on the game world's bounds and required accuracy. For example, a 32-bit float position can often be compressed into a 16-bit integer without perceptible loss in gameplay precision.
- Rotation Compression: Quaternions are compressed using techniques like the "smallest three" algorithm, dropping the largest component and storing the remaining three in 9 to 11 bits each, along with a 2-bit sign indicator.
- Bitstreams: The serialized data is written sequentially to a bitstream rather than aligned to byte boundaries. If a property requires only 3 bits, only 3 bits are written, preventing byte-padding overhead.
Managing Packet Loss Without Resends
Standard delta compression eliminates the need for TCP-style packet retransmissions. If a delta packet is lost in transit, the server does not resend that specific packet.
Because the client never ACKs the lost packet, the server's reference baseline (\(T_{ack}\)) remains unchanged. On the subsequent tick, the server simply generates a new delta comparing \(T_{current+1}\) against the older, still-valid \(T_{ack}\). The resulting delta will contain a larger set of changes, but it automatically brings the client fully up to date in a single transmission, ensuring eventual consistency while preventing head-of-line blocking.