The Entity System
Basics of Multiplayer
The game is written with multiplayer in mind, and even single player gameplay uses the same stack as the multiplayer scenario, just with a single client.
The host of a game is always the server, and all players in the game (including the host) are clients.
Generally speaking, the clients are only responsible for rendering and input, whereas the server is responsible for the game logic. For example, when a player grabs an tomato from a crate, its client sends a message to the server that “I will pickup something from the crate”, and the server decides to spawn a tomato and put it in the chef’s hands, and synchronizes that information to the clients. Even if the player is the host, it goes through the same code path, except the networking part just calls local code.
The physics simulation is somewhat of an exception. Both the server and the client run physics simulation. I don’t yet understand how that works.
Basics of the entity system
Each object during the gameplay that requires multiplayer synchronization has an entity ID. The entity IDs
are agreed upon at all times between the server and the clients. The bidirectional mapping between entity IDs
and their corresponding GameObjects are kept by EntitySerialisationRegistry
as static fields. The registry
also keeps track of some other properties.
- The staticness of the fields are relied upon by the deserialization code. When receiving a message over
the network references an object by its the entity ID, the message is deserialized directly into a
corresponding GameObject, by accessing
EntitySerialisationRegistry
statically. This is a questionable design decision, but in the face of Unity (which uses static objects all over the place), it doesn’t stand out.
Synchronizing Entities
Initially, when the level is first launched, the starting objects (such as counters, mixers, plates, chefs, etc.) are assigned entity IDs by iterating through the objects:
- See
EntitySerialisationRegistry.SetupSynchronisation
which then callsLinkAllEntitiesToSynchronisationScripts()
. - This assignment is done independently by both the server and all the (non-server) clients, and the agreement of the IDs depends on the iteration order of the game objects in the scene. This is presumably deterministic.
- The initial state of all the initial objects are not synchronized over the network; rather, the server and clients just assume they all have identical contents in the Unity scene.
Each synchronized entity (i.e. GameObject) can contain multiple components, each of which may provide its own synchronization logic:
- Each component has a base type, a server synchronizer, and a client synchronizer.
- For example, a cutting board typically contains components like
Workstation
andAttachStation
. TheWorkstation
component corresponds to the functionality of a cutting board, whereasAttachStation
provides the ability to place anything on the counter (such as a plate). The two components interact: if the attached object is aWorkableItem
, then theWorkstation
may be used to “work on” the item, which is the game’s way of saying, chopping it. - In this example, when
SetupSynchronisation
is called, the server will attachServerWorkstation
andServerAttachStation
to the same GameObject. A client (including the host’s) will attachClientWorkstation
andClientAttachStation
. - The pair of
ServerWorkstation
andClientWorkstation
work with each other to synchronize the state for just that aspect of the cutting board object. - The synchronization of each type of component uses a different entity message. For
Workstation
the message type isWorkstationMessage
. The message contains all states represented by this component.
Spawning an entity
An entity is spawned by specifying a spawner entity and the prefab to be spawned.
- On the server side this is done via
NetworkUtils.ServerSpawnPrefab(GameObject spawner, GameObject prefab)
.- For example,
ServerPlateReturnStation.CreateStack
callsNetworkUtils.ServerSpawnPrefab(this.gameObject, this.m_returnStation.m_stackPrefab)
, the latter being the prefab for a dirty plate stack.
- For example,
- On initialization, any object that can spawn something calls
NetworkUtils.RegisterSpawnablePrefab
. This will add aSpawnableEntityCollection
onto the GameObject if it’s not already there, and add the prefab to the list of spawnables. InsideNetworkUtils.ServerSpawnPrefab
, theSpawnableEntityCollection
is requested from the spawner GameObject via the interfaceINetworkEntitySpawner
(whose only implementation isSpawnableEntityCollection
). - When calling
NetworkUtils.ServerSpawnPrefab
, the spawned object is automatically registered as a new entity in theEntitySerialisationRegistry
, and it automatically callsEntitySerialisationRegistry.StartSynchronisingEntry
on the resulting GameObject.- This happens before calling
ServerMessenger.SpawnEntity
to notify clients about the spawning of the entity. This implies that StartSynchronisingEntry should not immediately emit any synchronization messages, or else the message will just be dropped at the client side. - The function also checks if the spawned object is a
PhysicalAttachment
, which generally means something that has a container object that has aRigidBody
. If so, it additionally registers the container object as a separate entity, and instead of callingServerMessenger.SpawnEntity
, callsServerMessenger.SpawnPhysicalAttachment
instead - this is a separate network message type. This message includes both entity IDs.- See the client side at
ClientSynchronisationReceiver.OnSpawnPhysicalAttachmentMessageReceived
.
- See the client side at
- This happens before calling
Messaging and Serialization
All the message types and entity message types are registered in MultiplayerController.Awake
.
When are Entities Synchronized
Most entities are synchronized when the corresponding server component calls SendServerData
, which is then
processed by the client side when it arrives.
Some entities are synchronized automatically by the entity system, if they override GetServerUpdate
. Periodically (once every few frames), the ServerSynchronisationScheduler.SynchroniseList
function will
send an EntitySynchronisationMessage
to synchronize all such entities to the clients.
These automatically synchronized entities are ServerWorldObjectSynchroniser
(for physics synchronization), ServerCookingHandler
, ServerIngredientContainer
, and ServerMixingHandler
.