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

Using PlayMode Tools to generate 2 Thin Clients and navigating through ICommandData flows

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).

You are probably sick of our suggestions (pleas?) to watch Timothy Ford's talk if you haven't already watched it...

But if you have gotten this far and STILL don't know what the heck is going on with predicted-clients, do yourself a favor and checkout Timothy Ford's talk:

Watch Timothy Ford's talk from 24:15 to 33:05, seriously

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.

From NetCode's Prediction documentation

Then we will use Auto Command Target to attach ICommandData to our player, and have NetCode automatically send those commands to the Server.

We USED to say (pre v.50)

"We will then update the NCE's CommandTargetComponent's targetEntity to point at that entity in the a new PlayerGhostSpawnClassificationSystem. The server will also update its NCE's CommandTargetComponent's targetEntity field to point at the spawned entity on the server.

The CommandTargetComponent points to where the Commands sent from a client should be stored. We will be storing them in the player entities."

Now NetCode allows us to send multiple command streams just by where we attach the ICommand Data, much easier!

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 stream

The 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 ICommandData interface. To implement that interface you need to provide a property for accessing the Tick.

The serialization and registration code for the ICommandData will be generated automatically, but it is also possible to disable that and write the serialization manually.

If you add your ICommandData component to a ghost which has Has Owner and Support Auto Command Target enabled 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 the ICommandData component has been attached to.

You can have multiple command systems, and NetCode selects the correct one based on the ICommandData type of the entity that points to CommandTargetComponent.

When you need to access inputs on the client and server, it is important to read the data from the ICommandData rather 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> called GetDataAtTick which gets the matching tick for a specific frame. You can also use the AddCommandData utility method which adds more commands to the buffer.

From NetCode's Command stream documentation

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

Player spawn flow implemented in this section (if image is blurry right click and save or open in new tab to be able to zoom in)

Updating the Player prefab

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

Create PlayerEntityComponent
  • 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

Creating Camera prefab
  • 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

Updating Player 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:

Creating CameraAuthoringComponent in Client/Components
  • 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

Adding CameraAuthoringComponent to PrefabCollection
  • Now let's make PlayerSpawnRequestRpc in Mixed/Commands. Paste the code snippet below into PlayerSpawnRequestRpc.cs:

Create PlayerSpawnRequestRpc
  • 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:

Deleting InputSpawnSystem and InputMovementSystem and creating InputSystem in Client/Systems
  • 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:

Creating PlayerGhostSpawnClassificationSystem in Client/Systems
  • Hit play then hit space bar to spawn our player

Hitting play then spawning our player with spacebar using our new server/client flow
  • Great. Now we are able to spawn our player entity and have it set up in NetCode

Updating player movement

Creating PlayerCommands and updating game state flow implemented in this section
  • 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:

Creating PlayerCommand in Mixed/Components
  • 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

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

Adding PlayerCommandBufferAuthoringComponent to our Player prefab
  • Let's do a quick review of NetCode's prediction handling to make sense of ".ShouldPredict()" in

Prediction

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.

Client

The 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 GhostPredictionSystemGroup sets 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 false the 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.

Server

On 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.PredictingTick to make sure the exact same code can be run on both the client and server.

From NetCode's Prediction documentation

Creating VelocityComponent and MovementSystem
  • 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 stream

The 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 ICommandData interface. To implement that interface you need to provide a property for accessing the Tick.

The serialization and registration code for the ICommandData will be generated automatically, but it is also possible to disable that and write the serialization manually.

If you add your ICommandData component to a ghost which has Has Owner and Support Auto Command Target enabled 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 the ICommandData component has been attached to.

You can have multiple command systems, and NetCode selects the correct one based on the ICommandData type of the entity that points to CommandTargetComponent.

When you need to access inputs on the client and server, it is important to read the data from the ICommandData rather 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> called GetDataAtTick which gets the matching tick for a specific frame. You can also use the AddCommandData utility method which adds more commands to the buffer.

From NetCode's Command stream documentation

  • 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

Adding VelocityComponent to Player prefab and re-building NetCode generated code
  • 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"

Updating GameSettings and navigating
  • 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 💪

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!

Github branch link:

git clone https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/ git checkout 'Updating-Players'

Last updated