Network Serialization & Delta Compression

πŸ“¦ Network Serialization & Delta Compression

In multiplayer game development, bandwidth is a strict physical budget. The standard Ethernet Maximum Transmission Unit (MTU) is 1,500 bytes. To avoid IP-level packet fragmentation (which causes catastrophic packet loss over UDP), game developers keep UDP packet sizes under 1,200 to 1,400 bytes.

If you send raw uncompressed C++ or Odin structs with 32-bit floats for every position, rotation, and velocity for 64 players at 60Hz, you will quickly saturate the player's connection.

Efficient netcode uses Bit-Packing, Float Quantization, and Delta Compression.


πŸ—œοΈ 1. Bit-Packing (Working at the Bit Level)

Standard programming languages align variables to byte or 4-byte boundaries. In networking, we throw byte alignment away and treat packets as continuous bit streams:

Data Standard Size Bit-Packed Size Savings
Boolean (is_crouching) 1 byte (8 bits) 1 bit 87.5%
Weapon Slot (1 of 4 weapons) 4 bytes (32 bits) 2 bits (22=4) 93.7%
Health Points (0 to 100) 4 bytes (32 bits) 7 bits (27=128) 78.1%
Yaw Facing (0Β° to 360Β°) 4 bytes (32 bits) 9 bits (360/512β‰ˆ0.7∘) 71.8%

Using a custom BitWriter and BitReader, multiple fields are packed consecutively into a byte buffer without padding.


πŸ“ 2. Float Quantization (Compressing 3D Math)

Standard f32 floats allocate 1 sign bit, 8 exponent bits, and 23 mantissa bits (see Odin Precision Limits). Most game positions do not need sub-millimeter precision across infinite ranges.

The Quantization Formula

If a world coordinate is bounded between a minimum and maximum range [Min,Max]:

Encoded=round(Valueβˆ’MinMaxβˆ’MinΓ—(2Bitsβˆ’1))Decoded=Min+(Encoded2Bitsβˆ’1)Γ—(Maxβˆ’Min)

The "Smallest Three" Quaternion Compression

A full quaternion has 4 floats (x, y, z, w), totaling 128 bits.
Because normalized quaternions satisfy x2+y2+z2+w2=1, we only need to transmit three of the componentsβ€”the sign and magnitude of the fourth can be reconstructed on the receiver!

  1. Find the component with the largest absolute value (0, 1, 2, or 3).
  2. Store its index in 2 bits.
  3. Negate the quaternion if the largest component is negative (ensuring the largest is always positive).
  4. Quantize the remaining 3 components into 9 bits each (range [βˆ’0.7071,+0.7071]).
  5. Total size: 2+(3Γ—9)=29Β bits (down from 128 bitsβ€”a 77% reduction!).

πŸ”„ 3. Delta Compression (Only Send What Changed)

Instead of sending the full game state every tick (Keyframe / Baseline), the server sends only the differences (Deltas) relative to the last state that the client confirmed receiving.

Client ACK history: "I received Server Snapshot #40"
Server prepares:    Snapshot #45

Delta Packet = Difference between (Snapshot #45) and (Snapshot #40)

The Change Mask (Bitmask)

A single entity might have 20 networked properties (health, ammo, position, velocity, weapon, stance).
At the start of the entity stream, the server writes a Bitmask indicating which properties changed:

[ Change Mask: 0b00000101 ]  -> Bits 0 and 2 are active
- Bit 0 active: Read Quantized Position (32 bits)
- Bit 1 inactive: Position Z unchanged (skip)
- Bit 2 active: Read Health (7 bits)
- Remaining bits inactive: All other properties remain identical to Snapshot #40!

If an entity is standing still, its update cost drops to a few bits instead of dozens of bytes.


πŸ› οΈ Engine Implementations & Best Practices

In Odin

Use #packed structs and explicit sized types for deterministic packet layouts (see Types in Odin):

Packet_Header :: struct #packed {
    protocol_id: u32,
    sequence:    u16,
    ack:         u16,
    ack_bitfield:u32, // Up to 32 previous acks in a single integer
}

In Unreal Engine 5

Unreal provides high-level tools to control replication bandwidth: