I want to create a simple client-server real-time multiplayer game as a project for my networking class.

I've read a lot about real-time multiplayer network models and I understand the relationships between the client and the server and lag-compensation techniques.

What I want to do is something similar to the Quake 3 network model: basically, the server stores a snapshot of the whole game state; upon receiving input from the clients, the server creates a new snapshot reflecting the changes. Then, it calculates the differences between the new snapshot and the last one and sends them to the clients, so that they can be in sync.

This approach seems really solid to me - if the client and server have a stable connection, only the minimal necessary amount of data will be sent to keep them in sync. If the client gets out of sync, a full snapshot can be requested as well.

I cannot, however, find a good way to implement the snapshot system. I find it really hard to move away from single-player programming architecture and think about how I could store the game state in such a way that:

  • All the data is separated from the logic
  • Differences can be calculated between snapshot of game states
  • Game entities can be still easily manipulated via code

How is a snapshot class implemented? How are the entities and their data stored? Does every client entity have an ID that matches an ID on the server?

How are snapshot differences calculated?

In general: how would a game-state snapshot system be implemented?

  • 4
    +1. This is a bit too broad for a single question, but IMO it is an interesting topic that can be roughly covered in an answer. – Kromster Nov 20 '14 at 14:02
  • Why don't you just store 1 Snapshot (the actual world), save every incoming changes into this regular world-state AND store the change in a list or something. Then, when its time to send the changes to all the clients just send the content of the list to all of them and clear the list, begin from zero (changes). Maybe this isn't as good as to store 2 snapshots but with this approach you don't need to worry about algorithms about how to fast diff 2 snapshots. – tkausl Dec 2 '14 at 15:04
  • Have you read this: fabiensanglard.net/quake3/network.php - the review of quake 3 network model includes discussion on implemention. – Steven Dec 2 '14 at 16:29
  • What kind of game are attempting to construct? The network setup depends heavily on the type of game you are making. A RTS does not behave like a FPS in terms of networking. – wolfdawn Dec 19 '14 at 8:17
up vote 3 down vote accepted
+50

You can calculate snapshot delta (changes to its previous synced state) by keeping two snapshots instances: current one and last synced one.

When client input arrives you modify current snapshot. Then when it's time to send delta to clients, you calculate last synced snapshot with current one field-by-field (recursively) and calculate and serialise delta. For serialisation you can assign unique ID to each field in scope of its class (as opposed to global state scope). Client and server should share the same data structure for global state so client understands what a particular ID is applied to.

Then, when delta is calculated you clone current state and make it the last synced one, so now you have identical current and last synced state but different instances so you can modify current state and not affect the other.

This approach can be easier to implement, especially with help of reflection (if you have such a luxury), but can be slow, even if you highly opitimise reflection part (by building your data schema to cache most reflection calls). Mainly because you need to compare two copies of potentially large state. Of course it depends how you implement comparison and your language. It can be fast in C++ with hardcoded comparator but not so flexible: any change of your global state structure requires modification of this comparator, and these changes are so frequent on initial project stages.

Another approach is to use dirty flags. Each time client input arrives you apply it to your single copy of global state and flag corresponding field(s) as dirty. Then when it's time to sync clients you serialise dirty fields (recursively) using the same unique IDs. (Minor) drawback is that sometimes you send more data than strictly required: e.g. int field1 was initially 0, then assigned 1 (and flagged dirty) and after that assigned 0 again (but remains dirty). Benefit is that having huge hierarchical data structure you don't need to analyse it completely to calculate delta, only dirty paths.

In general, this task can be quite complicated, depends how flexible should be final solution. E.g. Unity3D 5 (upcoming) will use attributes to specify data that should be auto-synced to clients (very flexible approach, you don't need to do anything except adding an attribute to your field(s)) and then generate code as a post-build step. More details here.

First you need to know how to represent your relevant data in a protocol compliant manner. This depends on the data relevant to the game. I will use an RTS game as an example.

For networking purposes, all entities in the game are enumerated (e.g. pickups, units, buildings, natural resources, destructibles).

The players need to have the data relevant to them (for instance all visible units):

  • Are they alive or dead?
  • What type are they?
  • How much health do they have left?
  • Current position, rotation, velocity (speed + direction), path in the near future...
  • Activity: Attacking, walking, building, fixing, healing, etc...
  • buff/debuff status effects
  • and possibly other stats like mana, shields and what not?

At first the player must receive the full state before she can enter the game (or alternatively all info relevant to that player).

Each unit has an integer ID. Attributes are enumerated and therefore have integral identifiers too. The units IDs don't have to be 32 bits long (it could be if we aren't frugal). It could very well be 20 bits (leaving 10bits for the attributes). The units' ID has to be unique, it could very well be assigned by a counter when the unit is instantiated and/or added to the game world (buildings and resources are considered an immobile unit and resources could be assigned an ID when the map is loaded).

The server stores the current global state. Each player's most recent updated state is represented by a pointer to a list of recent changes (all the changes after the pointer have not been sent to that player yet). Changes are added to the list when they occur. Once the server is done with sending the last update, it can begin to iterate over the list: the server moves the player's pointer along the list to it's tail, collecting all changes along the way and placing them in a buffer that will be sent to the player(i.e the protocol's format can be something like this: unit_id; attr_id; new_value) New units are considered changes as well and are sent with all their attribute values to the receiving players.

If you are not using a language with a garbage collector, you will need to setup a lazy pointer that will be lagging behind and then catching up to the most outdated player pointer in the list, freeing objects along the way. You can remember which player is the most outdated inside a priority heap or simply iterate and free until the lazy pointer equals (i.e points to the same item as one of the players pointers).

Some questions you did not raise and I think are interesting are:

  1. Should the clients receive a snapshot with all the data in the first place? What about items outside their line of vision? What about fog of war in RTS games? If you send all the data, the client could be hacked to display data that should not be available to the player (depending on other security measures you take). If you only send relevant data, the issue is resolved.
  2. When is it vital to send changes instead of sending all the information? Considering the bandwidth available on modern machines, do we gain anything from sending a "delta" instead of sending all the information, if so when?

Your Answer

 
discard

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.