Netcode Series Part 2: Data Channels (Snapshots and RPCs)
By: Nicola Geretti
Tags: Multiplayer, Netcode, Scalability, Unity

Unreliable, Unordered: Core Gameplay Snapshots

All core gameplay is built on the unreliable, unordered channel. Before jumping onto what that means, it’s important to first define what Core Gameplay data is.

Core gameplay data is any data necessary to define the state of the game at any point in time.

The easiest way to think about it is, let’s say you connect to a game already in progress. You need to know who’s playing, the score, if any projectiles are flying by, etc.. Basically, the data required to build the scene to display.

Core gameplay answers a resounding Yes to both our golden questions:

Is the data latency important? Yes — Is the data essential to game state? Yes

All entities such as Game mode, Players, Projectiles, etc. make up the Gamestate. Entities can spawn, carry changing properties and despawn. Entities carry a limited amount of pre-determined properties called the Net State, which is the contract needed to run our bit-packing (yes, bits, not bytes!).

Net states are written out as protobuffer definition files, a format we created to quickly spell out variables and their serialization format. They’re then automatically parsed and exported into C# files with utility functions. This is what a paired down projectile net state looks like:

class ProjectileEntityNetState : PropEntityNetState

{

var InstigatorID : ushort;

var VelX : float<0.001>;

var VelY : float<0.001>;

var IsWallStuck : bool;

var FireTime : double;

var WeaponID : ushort;

}

These support inheritance, so the PropEntityNetState also carries Position and Angle properties. All the entity net states put together are the Gamestate.

Here’s where it gets interesting.

The Gamestate is saved on the server at every simulation tick. Tickrates in popular shooters are known to be as low as 20hz (Overwatch) and as high as 128hz (Valorant). Every tick stores the Gamestate into a circular buffer of Snapshots. Delta Snapshots are the key to effective data compression.

Delta snapshots are messages that are custom-tailored to each player, and they works like so:

The server sends snapshots periodically to all clients connected. The first snapshot is the Gamestate in its entirety , including all entity net states (that’s data pertaining to game mode, player entity, projectile entity, etc.).

, including all entity net states (that’s data pertaining to game mode, player entity, projectile entity, etc.). Since we can’t rely on the protocol ordering packets for us, each snapshot has a sequence number. If the client receives a snapshot that has an older sequence number than the last received, it is discarded , as it contains old data we don’t need (like a previous player’s position).

, as it contains old data we don’t need (like a previous player’s position). When the client receives a snapshot, it also acknowledges the receipt to the server by sending the sequence number.

to the server by sending the sequence number. The server now knows what the client knows . It can generate custom-tailored snapshots based on the last acknowledged sequence number, sending the client only the data that has changed. This is what’s called a delta snapshot.

. It can generate custom-tailored snapshots based on the last acknowledged sequence number, sending the client This is what’s called a delta snapshot. If there is packet loss or jitter, there will be a delay in snapshot acknowledgement. This means the server will send increasingly larger snapshots, including more and more changed data that was never received by the client.

What’s important to note is that no matter which delta client snapshot is the latest, a full game state can be reconstructed, meaning core gameplay state is maintained and the simulation can continue running.

You can find out more about the technical implementation of Delta Snapshots on Fabien Sanglard’s deep-dive of Quake3’s source code here.

I should note that Snapshots aren’t suitable for all games. They’re great for non-deterministic physics-based games that require frequent synchronization on a limited amount of entities. If you have a fully deterministic game (meaning the same inputs will produce the exact same result each time) such as a fighting game, input rollback systems are a better, more efficient way to go, with no need for positional deltas. If you’re making an RTS with hundreds or thousands of entities, you’ll need a different network model altogether.