Synced data containers for players and server

I found that sending data back and forth between clients and server got messy and unstructured. I made wrappers to structure the transfers back and forth. Sometimes you want to send a list of positions and velocities for the physics engine, sometimes you want to update the inventory of an actor. It can get very messy when dealing with all these different data operations if you don't structure it. I solved it by wrapping data in containers and connecting them with an identical counterpart on the other side.

DataContainer

A DataContainer is a wrapper for other data. PlayerData, for instance. PlayerData could contain position, name, inventory, etc. The container only wraps a reference of the data that should be synced and tracks when it was last updated, etc.

The container also keeps track if it's stored in the server data layer or the client data layer so it's clear what should happen when it's commited.

I have defined the server layer as the layer where the game logic is running. The client layer is where graphics are being rendered and input is being collected. The client layer also exist on the computer where the server layer is running. All other connected computers only have the client layer.

Targeting

A DataContainer can contain data designated for a specific player or contain global data which is used by the game.

An example of player specific data could be cards in a card game. These should only be sent to the player who owns them if the player is the only one who should see them.

An example of shared data would be all chesspieces on a board.

Groups

A DataContainer is associated with a group identifier. That way you know which container in the server layer corresponds to which containers in the client layer. The container could be targetting a certain player on a machine or a target machine.

A container that has all the enemies would be maintained and owned by the server layer, but should be synced to the client layers on all connected machines (and the local). So the DataContainer would have the group identifier "enemies" and be registered as a global container.

DataContainerHandler

I've taken the approach to create handlers for the different types of data. PlayerData gets a PlayerDataContainerHandler, PhysicsObjectData gets an PhysicsObjectDataHandler and so on. The handler is responsible for collecting the data which are to be sent to the other containers in the group.

For example, the PlayerData should only send it's Inventory and Stats and not it's name and identifiers. In the server layer, the DataContainer is marked as pending sync. The PlayerDataContainerHandler reads these values and composes a message. The message is sent over to the client layer. In the client layer, the PlayerDataContainerHandler parses this message and updates it's own PlayerData.

Partial updates

Sometimes you don't need to update all of the data in a container. This can almost always be applied to lists of data. Like a list of enemies / npcs. Some are updated by the AI because they're within range and some are inert because they're not within the simulated area. Then it might make sense to only send out data concerning those objects if there's an bandwidth concern or if reacting to a sync costs a lot of clock cycles.

Events and reactions

Lists are subject to removal of objects and addition of new ones. If all objects in the list have an id, it's easy to find and remove them. Events connected to these events can be triggered so the layer reacts accordingly. I found it better to place the events on the object which are enclosed in the DataContainer and trigger it from the DataContainerHandler. It gave the freedom I needed.

Direction

My containers usually only have one direction. They are transferred from the server layer to the client layer. The input data and client actions are sent back as messages to reduce any potential lag.