DOTS NetCode and Player Prefabs
Code and workflows to turn the Player prefab into a NetCode ghost and spawn Thin Clients
What you'll develop on this page

We will update our Player prefab by "turning it into" a client-predicted ghost which spawns and moves by commands sent from the client.
Github branch link: https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/tree/Updating-Players
NetCode client-predicted model background
Our player will be client-predicted. This means we will be able to move and shoot with immediate feedback because the client will predict what will happen when it issues commands.
How is that possible? I thought the server was the authority, clients can't do what they want!
True (and way to go!) This is why the clients are only "predicting" what will happen based on the user's input (commands, like up, down, left, right arrow keys on a keyboard). The server makes the ultimate decision of what actually happened (by ingesting commands from all clients and deciding the truth).
Spawning
Although eventually movement and shooting will be "instant" on the client (predicted), the first step, Spawning a player entity, happens as a result of the client sending an RPC to the server.
Similar to how we updated ServerSendGameSystem in the last Section to send the newly connected client an RPC to load the game, we will do same here with Player; the client will send the server an RPC to spawn it a player. Once the server spawns the client's player entity, NetCode will send the entity to all clients, but the client that requested it will have a special version of the player entity that has a "PredictedGhostComponent" attached. This is a special NetCode component that we can use to know which ghosted entities are "owned" (predicted) by the clients. So of all the player entities in ClientWorld (as many as there are connected clients) only 1 entity will have the PredictedGhostComponent (the client's player entity).
Prediction in a multiplayer games means that the client is running the same simulation as the server for the local player. The purpose of running the simulation on the client is so it can predictively apply inputs to the local player right away to reduce the input latency.
Prediction should only run for entities which have the PredictedGhostComponent. Unity adds this component to all predicted ghosts on the client and to all ghosts on the server. On the client, the component also contains some data it needs for the prediction - such as which snapshot has been applied to the ghost.
The prediction is based on a GhostPredictionSystemGroup which always runs at a fixed timestep to get the same results on the client and server.
Then we will use Auto Command Target to attach ICommandData to our player, and have NetCode automatically send those commands to the Server.
We will also need to update our player's camera. Currently the camera is part of the Player prefab. If we leave our Player prefab like this every time a remote client appears in ClientWorld the camera will change to that new remote client's camera (because Unity switches to the last activated camera automatically). Instead we will remove the camera from the Player prefab and instead add it to the player during PlayerGhostSpawnClassification. We will store a reference to the camera in PrefabCollection.
Movement
InputSpawnSystem and InputMovementSystem will no longer capture input and update state based on that input. Instead, client inputs will be stored as "PlayerCommands" in a new "InputSystem." The Commands are then sent to the server to playback. The server will use InputSpawnSystem and InputMovementSystem to play pack commands and update the game state. The systems will also be run by the client to "predict" what will happen. If there are any "disagreements" about what happened between the client and the server NetCode updates the state on the client to match the server.
Command streamThe client continuously sends a command stream to the server. This stream includes all inputs and acknowledgements of the last received snapshot. When no commands are sent a NullCommandSendSystem sends acknowledgements for received snapshots without any inputs. This is an automatic system to make sure the flow works automatically when the game does not need to send any inputs.
To create a new input type, create a struct that implements the
ICommandDatainterface. To implement that interface you need to provide a property for accessing theTick.The serialization and registration code for the
ICommandDatawill be generated automatically, but it is also possible to disable that and write the serialization manually.If you add your
ICommandDatacomponent to a ghost which hasHas OwnerandSupport Auto Command Targetenabled in the autoring component the commands for that ghost will automatically be sent if the ghost is owned by you, is predicted, and AutoCommandTarget.Enabled has not been set to false.If you are not using
Auto Command Target, your game code must set the CommandTargetComponent on the connection entity to reference the entity that theICommandDatacomponent has been attached to.You can have multiple command systems, and NetCode selects the correct one based on the
ICommandDatatype of the entity that points toCommandTargetComponent.When you need to access inputs on the client and server, it is important to read the data from the
ICommandDatarather than reading it directly from the system. If you read the data from the system the inputs won’t match between the client and server so your game will not behave as expected.When you need to access the inputs from the buffer, you can use an extension method for
DynamicBuffer<ICommandData>calledGetDataAtTickwhich gets the matching tick for a specific frame. You can also use theAddCommandDatautility method which adds more commands to the buffer.
Thin Clients
This is an experimental feature in NetCode's Multiplayer PlayMode Tools.
Previously in the "Create a Socket Connection" section, you saw how we could add "Thin Clients," which produced more ClientWorlds and NCEs. This is part of Unity's effort to to build more tools to help developers build multiplayer games (nice!)
Currently this functionality is not well-documented and still being ironed out by Unity, so we at Moetsi have read in-between the lines from Unity sample projects and have broken down the explanation as follows:
A "Thin Client" will contain a Singleton "ThinClientComponent" in its ClientWorld. NetCode automatically adds this Singleton when Multiplayer PlayMode Tools has Num Thin Clients > 0.
When creating input systems you must check for the Singleton ThinClientComponent, and if it exists you can create mock inputs to simulate client behavior.
As mentioned here by the DOTS NetCode team: "Thin clients just send the input stream to server, They don't have ghosts and they don't decompress snapshots. They can send RPC as normal client does (in case of asteroid, for the initial spawn and level loading)."
Because Thin clients do not get sent Ghosts, then we will not be able to use our "normal client" approach of sending ICommandData to the server (using the new, and awesome, Auto Command Target Approach). We will need to use the "old version" (setting the Command Target Component on the NCE).
So to keep your head on straight we will first implement spawning and commands for a "normal client". Then we will update our systems to account for thin clients.
Updating Player spawn with NetCode

Updating the Player prefab
First let's create PlayerEntityComponent in Mixed/Components. Paste this code snippet in the file

Open the Player prefab and move the Camera GameObject from Hierarchy into Scripts and Prefabs. Once moved into the folder, delete the Camera GameObject from the Player prefab in Hierarchy

Next add a GhostAuthoringComponent to the Player prefab
Name = Player
Importance = 90
Supported Ghost Modes = All
Default Ghost Mode = Owner Predicted
Optimization Mode = Dynamic
Check "Has Owner"
Check "Support Auto Command Target"

Finally add the PlayerEntityComponent to the prefab

Spawning a client-predicted player
Create a new component called "CameraAuthoringComponent" and put it in a new folder Client/Components
Paste the code snippet below into CameraAuthoringComponent.cs:

Navigate to PrefabCollection in ConvertedSubScene and add CameraAuthoringComponent
Drag the Camera prefab in Scripts and Prefabs onto the Prefab field in the CameraAuthoringComponent
Save, return to SampleScene and reimport ConvertedSubScene

Now let's make PlayerSpawnRequestRpc in Mixed/Commands. Paste the code snippet below into PlayerSpawnRequestRpc.cs:

In a coming section, we will update InputSpawnSystem and InputMovementSystem to InputResponseSpawnSystem and InputResponseMovementSystem
For now let's delete InputSpawnSystem and InputMovementSystem so they do not interfere with our new work flows
Create a new system in Client/Systems named InputSystem
Paste the code snippet below into InputSystem.cs:

Now let's create the system that will respond to the PlayerSpawnRequestRpc
Create a new system named PlayerSpawnSystem in the Server/Systems folder
Paste the code snippet below into PlayerSpawnSystem.cs:
This file actually contains 2 systems:
PlayerSpawnSystem
PlayerCompleteSpawnSystem
PlayerSpawnSystem will instantiate the prefab and set the components on the Player prefab and add a PlayerSpawnInProgressTag
It does not fully commit to updating because first it will ensure that the entity made it over to the client without issues
PlayerCompleteSpawnSystem will check for any entities with a PlayerSpawnInProgressTag (which means the entity was created) and if they exist they will remove the tag and add it to the linked entity group (used for house keeping when a player disconnects)

Finally let's create PlayerGhostSpawnClassificationSystem in Client/Systems. Paste the code snippet below into the file:

Hit play then hit space bar to spawn our player

Great. Now we are able to spawn our player entity and have it set up in NetCode
WARNING!
Sometimes you can get an error here where the camera stops working after updating PlayerGhostSpawnClassificationSystem (when we added the camera on the client). Instead of switching from the main camera to the newly-added player camera (that has been added by PlayerGhostSpawnClassificationSystem), the active camera stays as the Main Camera.
From our testing this has a 50% of happening. These are the steps we recommend you take to fix this issue:
Reimport all assets (this sometimes fixes it, if not go to step 2)
Quit out of Unity entirely, then reopen the Project (this should fix it most of the time, if not go to step 3)
Restart your computer (yup that's right, somehow restarting the computer has been known to fix this camera issue 😔)
If you are still having issues with the new camera flow, ask us a question on Discord.
We can now spawn a client-predicted Player prefab
We updated our Player prefab by
Removing the Camera GameObject
Adding a GhostAuthoringComponent
Adding a PlayerEntityComponent
Created a CameraAuthoringComponent and adding it to the PrefabCollection
Created PlayerSpawnRequestRpc
Deleted InputSpawnSystem and InputMovementSystem
Created InputSystem
Created PlayerSpawnSystem
Created PlayerGhostSpawnClassificationSystem
Updating player movement

Let's start by creating the ICommandData component that will store our input Commands in the Mixed/Components folder
Name it PlayerCommand
Paste the code snippet below into PlayerCommand.cs:

Next let's create an authoring component in the Mixed/Components folder that will add a buffer of PlayerCommands to whatever prefab we add it to
Name it PlayerCommandBufferAuthoringComponent

We need to add our PlayerCommandBufferAuthoringComponent to our Player prefab (navigate to Player prefab and click "Add Component" in the Inspector)

Let's do a quick review of NetCode's prediction handling to make sense of ".ShouldPredict()" in
PredictionPrediction in a multiplayer games means that the client is running the same simulation as the server for the local player. The purpose of running the simulation on the client is so it can predictively apply inputs to the local player right away to reduce the input latency.
Prediction should only run for entities which have the PredictedGhostComponent. Unity adds this component to all predicted ghosts on the client and to all ghosts on the server. On the client, the component also contains some data it needs for the prediction - such as which snapshot has been applied to the ghost.
The prediction is based on a GhostPredictionSystemGroup which always runs at a fixed timestep to get the same results on the client and server.
ClientThe basic flow on the client is:
NetCode applies the latest snapshot it received from the server to all predicted entities.
While applying the snapshots, NetCode also finds the oldest snapshot it applied to any entity.
Once NetCode applies the snapshots, the GhostPredictionSystemGroup runs from the oldest tick applied to any entity, to the tick the prediction is targeting.
When the prediction runs, the
GhostPredictionSystemGroupsets the correct time for the current prediction tick in the ECS TimeData struct. It also sets GhostPredictionSystemGroup.PredictingTick to the tick being predicted.Because the prediction loop runs from the oldest tick applied to any entity, and some entities might already have newer data, you must check whether each entity needs to be simulated or not. To perform these checks, call the static method GhostPredictionSystemGroup.ShouldPredict before updating an entity. If it returns
falsethe update should not run for that entity.If an entity did not receive any new data from the network since the last prediction ran, and it ended with simulating a full tick (which is not always true when you use a dynamic timestep), the prediction continues from where it finished last time, rather than applying the network data.
ServerOn the server the prediction loop always runs exactly once, and does not update the TimeData struct because it is already correct. It still sets
GhostPredictionSystemGroup.PredictingTickto make sure the exact same code can be run on both the client and server.

Drag the InputSystem file into the Client/Systems folder
Next let's update InputSystem by pasting the code snippet below into InputSystem.cs:
This updated InputSystem is pretty intense, so take another look at the Command documentation to get a better sense of what's going on
Command streamThe client continuously sends a command stream to the server. This stream includes all inputs and acknowledgements of the last received snapshot. When no commands are sent a NullCommandSendSystem sends acknowledgements for received snapshots without any inputs. This is an automatic system to make sure the flow works automatically when the game does not need to send any inputs.
To create a new input type, create a struct that implements the
ICommandDatainterface. To implement that interface you need to provide a property for accessing theTick.The serialization and registration code for the
ICommandDatawill be generated automatically, but it is also possible to disable that and write the serialization manually.If you add your
ICommandDatacomponent to a ghost which hasHas OwnerandSupport Auto Command Targetenabled in the autoring component the commands for that ghost will automatically be sent if the ghost is owned by you, is predicted, and AutoCommandTarget.Enabled has not been set to false.If you are not using
Auto Command Target, your game code must set the CommandTargetComponent on the connection entity to reference the entity that theICommandDatacomponent has been attached to.You can have multiple command systems, and NetCode selects the correct one based on the
ICommandDatatype of the entity that points toCommandTargetComponent.When you need to access inputs on the client and server, it is important to read the data from the
ICommandDatarather than reading it directly from the system. If you read the data from the system the inputs won’t match between the client and server so your game will not behave as expected.When you need to access the inputs from the buffer, you can use an extension method for
DynamicBuffer<ICommandData>calledGetDataAtTickwhich gets the matching tick for a specific frame. You can also use theAddCommandDatautility method which adds more commands to the buffer.
You can see why we need to add tick data in ICommandData
This is how NetCode knows "when" the Command came

We need to create InputResponseMovementSystem
Both the server and the client use this system so put the file in Mixed/Systems folder
Paste the code snippet below into InputResponseMovementSystem.cs:

The client "predicts" the movement but the server ultimately decides game state by sending back ghost Snapshots of correct game state

Navigate to GameSettings in ConvertedSubScene increase the Player Force to 20 to make the player controls feel a bit more "zippy"
Reimport ConvertedSubScene and hit "play"

We are able to spawn and move around through ICommandData
Now let's do some clean up
Move PlayerTag into Mixed/Components
Move PlayerAuthoringComponent to Server/Components
You will likely need to update the prefab with these scripts because it will lose track of them
No gif here, we believe in you 💪
We can now spawn a client-predicted Player prefab and move it through commands
We added a GhostAuthoring component on our Player prefab
We created PlayerSpawnRequest
We created PlayerCommand
We merged InputSpawnSystem and InputMovementSystem into InputSystem
We created InputResponseMovementSystem system
Updating Systems to Handle Thin Clients
As mentioned earlier:
As mentioned here by the DOTS NetCode team: "Thin clients just send the input stream to server, They don't have ghosts and they don't decompress snapshots. They can send RPC as normal client does (in case of asteroid, for the initial spawn and level loading)."
Because Thin clients do not get sent Ghosts, then we will not be able to use our "normal client" approach of sending ICommandData to the server (using the new, and awesome, Auto Command Target Approach). We will need to use the "old version" (setting the Command Target Component on the NCE).
Let's update InputSystem.cs to generate mock data if we are a thin client
Next let's update the PlayerSpawnSystem.cs
Note that the PlayerSpawnSystem checks the NCE to see if the CommandTargetComponent targetEntity has been set to see if there is already an active player for a Network Connection
In this way the server makes sure it doesn't spawn more than 1 player per Network Connection
Now we can support Thin Clients, update PlayMode tools and add Thin Clients, hit play, and checkout the Players go!
We can now spawn and move thin clients by generated mock data
We updated InputSystem
We updated PlayerSpawnSystem
Github branch link:
git clone https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/
git checkout 'Updating-Players'
Last updated