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.

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:

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.
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.
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.
As well as setting the input buffer on an entity, your game code must also 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.

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. There is some additional configuration that needs to be done with the NCE to handle this behavior.
There are comments in InputSystem to specify which portions are meant for handling thin clients. We will include the thin client controls in the update of InputSystem in the subsequent "movement" section down below, instead of immediately here in this "spawning" section, because the spawn flow is complicated enough without immediately introducing thin client code.

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:
1
using Unity.Entities;
2
using Unity.NetCode;
3
4
[GenerateAuthoringComponent]
5
public struct PlayerEntityComponent : IComponentData
6
{
7
public Entity PlayerEntity;
8
}
Copied!
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
    Then add a "GhostOwnerComponent" to the prefab
      This is necessary for predicted ghosts as you might have noticed in the little error that pops up in Inspector
      (the Bullet prefab will also have one)
    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:
1
using Unity.Entities;
2
using UnityEngine;
3
4
[GenerateAuthoringComponent]
5
public struct CameraAuthoringComponent : IComponentData
6
{
7
public Entity Prefab;
8
}
9
Copied!
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:
1
using Unity.NetCode;
2
using Unity.Entities;
3
4
public struct PlayerSpawnRequestRpc : IRpcCommand
5
{
6
}
Copied!
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:
1
using UnityEngine;
2
using Unity.Entities;
3
using Unity.NetCode;
4
5
//This is a special SystemGroup introduced in NetCode 0.5
6
//This group only exists on the client and is meant to be used when commands are being created
7
[UpdateInGroup(typeof(GhostInputSystemGroup))]
8
public class InputSystem : SystemBase
9
{
10
//We will use the BeginSimulationEntityCommandBufferSystem for our structural changes
11
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
12
13
protected override void OnCreate()
14
{
15
16
//This will grab the BeginSimulationEntityCommandBuffer system to be used in OnUpdate
17
m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
18
19
//The client must have loaded the game to spawn a player so we wait for the
20
//NetworkStreamInGame component added during the load game flow
21
RequireSingletonForUpdate<NetworkStreamInGame>();
22
}
23
24
protected override void OnUpdate()
25
{
26
27
//We have removed the other inputs for now and will add them in the movement section
28
byte shoot;
29
shoot = 0;
30
31
if (Input.GetKey("space"))
32
{
33
shoot = 1;
34
}
35
36
//Must declare local variables before using them in the .ForEach()
37
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer().AsParallelWriter();
38
39
//We query for the NCE by checking for entities with a NetworkIdComponent
40
//We also do not want to send an RPC if the NCE has become disconnected
41
Entities
42
.WithAll<NetworkIdComponent>()
43
.WithNone<NetworkStreamDisconnected>()
44
.ForEach((Entity entity, int nativeThreadIndex, in CommandTargetComponent commandTargetComponent) =>
45
{
46
//If the CommandTargetComponent.targetEntity is null that means we need to spawn a player
47
if (commandTargetComponent.targetEntity == Entity.Null)
48
{
49
if (shoot != 0)
50
{
51
var req = commandBuffer.CreateEntity(nativeThreadIndex);
52
commandBuffer.AddComponent<PlayerSpawnRequestRpc>(nativeThreadIndex, req);
53
commandBuffer.AddComponent(nativeThreadIndex, req, new SendRpcCommandRequestComponent {TargetConnection = entity});
54
}
55
}
56
}).Schedule();
57
58
//We need to add the jobs dependency to the command buffer
59
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
60
}
61
}
Copied!
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:
1
using System.Diagnostics;
2
using Unity.Entities;
3
using Unity.Collections;
4
using Unity.Jobs;
5
using Unity.Mathematics;
6
using Unity.Transforms;
7
using Unity.NetCode;
8
using UnityEngine;
9
10
//This tag is only used by the systems in this file so we define it here
11
public struct PlayerSpawnInProgressTag : IComponentData
12
{
13
}
14
15
//Only the server will be running this system to spawn the player
16
[UpdateInGroup(typeof(ServerSimulationSystemGroup))]
17
public class PlayerSpawnSystem : SystemBase
18
{
19
20
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
21
private Entity m_Prefab;
22
23
protected override void OnCreate()
24
{
25
m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
26
27
28
//We check to ensure GameSettingsComponent exists to know if the SubScene has been streamed in
29
//We need the SubScene for actions in our OnUpdate()
30
RequireSingletonForUpdate<GameSettingsComponent>();
31
}
32
33
protected override void OnUpdate()
34
{
35
//Here we set the prefab we will use
36
if (m_Prefab == Entity.Null)
37
{
38
//We grab the converted PrefabCollection Entity's PlayerAuthoringComponent
39
//and set m_Prefab to its Prefab value
40
m_Prefab = GetSingleton<PlayerAuthoringComponent>().Prefab;
41
//we must "return" after setting this prefab because if we were to continue into the Job
42
//we would run into errors because the variable was JUST set (ECS funny business)
43
//comment out return and see the error
44
return;
45
}
46
47
//Because of how ECS works we must declare local variables that will be used within the job
48
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer();
49
var playerPrefab = m_Prefab;
50
var rand = new Unity.Mathematics.Random((uint) Stopwatch.GetTimestamp());
51
var gameSettings = GetSingleton<GameSettingsComponent>();
52
53
//GetComponentDataFromEntity allows us to grab data from an entity that we don't have access to
54
//until we are within a job
55
//We know we will need to get the PlayerSpawningStateComponent from an NCE but we don't know which one yet
56
//So we create a variable that will get PlayerSpawningStateComponent from an entity
57
var playerStateFromEntity = GetComponentDataFromEntity<PlayerSpawningStateComponent>();
58
59
//Similar to playerStateFromEntity, these variables WILL get data from an entity (in the job below)
60
//but do not have it currently
61
var commandTargetFromEntity = GetComponentDataFromEntity<CommandTargetComponent>();
62
var networkIdFromEntity = GetComponentDataFromEntity<NetworkIdComponent>();
63
64
//We are looking for NCEs with a PlayerSpawnRequestRpc
65
//That means the client associated with that NCE wants a player to be spawned for them
66
Entities
67
.ForEach((Entity entity, in PlayerSpawnRequestRpc request,
68
in ReceiveRpcCommandRequestComponent requestSource) =>
69
{
70
//We immediately destroy the request so we act on it once
71
commandBuffer.DestroyEntity(entity);
72
73
//These are checks to see if the NCE has disconnected or if there are any other issues
74
//These checks are pulled from Unity samples and we have left them in even though they seem
75
//Is there a PlayerSpawningState on the NCE
76
//Is there a CommandTargetComponent on the NCE
77
//Is the CommandTargetComponent targetEntity == Entity.Null
78
//Is the PlayerSpawningState == 0
79
//If all those are true we continue with spawning, otherwise we don't
80
81
if (!playerStateFromEntity.HasComponent(requestSource.SourceConnection) ||
82
!commandTargetFromEntity.HasComponent(requestSource.SourceConnection) ||
83
commandTargetFromEntity[requestSource.SourceConnection].targetEntity != Entity.Null ||
84
playerStateFromEntity[requestSource.SourceConnection].IsSpawning != 0)
85
return;
86
87
//We create our player prefab
88
var player = commandBuffer.Instantiate(playerPrefab);
89
90
//We will spawn our player in the center-ish of our game
91
var width = gameSettings.levelWidth * .2f;
92
var height = gameSettings.levelHeight * .2f;
93
var depth = gameSettings.levelDepth * .2f;
94
95
96
var pos = new Translation
97
{
98
Value = new float3(rand.NextFloat(-width, width),
99
rand.NextFloat(-height, height), rand.NextFloat(-depth, depth))
100
};
101
102
//We will not spawn a random rotation for simplicity but include
103
//setting rotation for you to be able to update in your own projects if you like
104
var rot = new Rotation {Value = Quaternion.identity};
105
106
//Here we set the componets that already exist on the Player prefab
107
commandBuffer.SetComponent(player, pos);
108
commandBuffer.SetComponent(player, rot);
109
//This sets the GhostOwnerComponent value to the NCE NetworkId
110
commandBuffer.SetComponent(player, new GhostOwnerComponent {NetworkId = networkIdFromEntity[requestSource.SourceConnection].Value});
111
//This sets the PlayerEntity value in PlayerEntityComponent to the NCE
112
commandBuffer.SetComponent(player, new PlayerEntityComponent {PlayerEntity = requestSource.SourceConnection});
113
114
//Here we add a component that was not included in the Player prefab, PlayerSpawnInProgressTag
115
//This is a temporary tag used to make sure the entity was able to be created and will be removed
116
//in PlayerCompleteSpawnSystem below
117
commandBuffer.AddComponent(player, new PlayerSpawnInProgressTag());
118
119
//We update the PlayerSpawningStateComponent tag on the NCE to "currently spawning" (1)
120
playerStateFromEntity[requestSource.SourceConnection] = new PlayerSpawningStateComponent {IsSpawning = 1};
121
}).Schedule();
122
123
124
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
125
}
126
}
127
128
//We want to complete the spawn before ghosts are sent on the server
129
[UpdateInGroup(typeof(ServerSimulationSystemGroup))]
130
[UpdateBefore(typeof(GhostSendSystem))]
131
public class PlayerCompleteSpawnSystem : SystemBase
132
{
133
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
134
135
protected override void OnCreate()
136
{
137
m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
138
}
139
140
protected override void OnUpdate()
141
{
142
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer();
143
144
//GetComponentDataFromEntity allows us to grab data from an entity that we don't have access to
145
//until we are within a job
146
//We don't know exactly which NCE we currently want to grab data from, but we do know we will want to
147
//so we use GetComponentDataFromEntity to prepare ECS that we will be grabbing this data from an entity
148
var playerStateFromEntity = GetComponentDataFromEntity<PlayerSpawningStateComponent>();
149
var commandTargetFromEntity = GetComponentDataFromEntity<CommandTargetComponent>();
150
var connectionFromEntity = GetComponentDataFromEntity<NetworkStreamConnection>();
151
152
Entities.WithAll<PlayerSpawnInProgressTag>().
153
ForEach((Entity entity, in PlayerEntityComponent player) =>
154
{
155
//This is another check from Unity samples
156
//This ensures there was no disconnect
157
if (!playerStateFromEntity.HasComponent(player.PlayerEntity) ||
158
!connectionFromEntity[player.PlayerEntity].Value.IsCreated)
159
{
160
//Player was disconnected during spawn, or other error so delete
161
commandBuffer.DestroyEntity(entity);
162
return;
163
}
164
165
//If there was no error with spawning the player we can remove the PlayerSpawnInProgressTag
166
commandBuffer.RemoveComponent<PlayerSpawnInProgressTag>(entity);
167
168
//We now update the NCE to point at our player entity
169
commandTargetFromEntity[player.PlayerEntity] = new CommandTargetComponent {targetEntity = entity};
170
//We can now say that our player is no longer spawning so we set IsSpawning = 0 on the NCE
171
playerStateFromEntity[player.PlayerEntity] = new PlayerSpawningStateComponent {IsSpawning = 0};
172
}).Schedule();
173
174
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
175
}
176
}
Copied!
    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 the NCE's CommandTargetComponent as well 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 update the NCE's CommandTargetComponent
    Finally let's create PlayerGhostSpawnClassificationSystem in Client/Systems. Paste the code snippet below into the file:
1
using Unity.Collections;
2
using Unity.Entities;
3
using Unity.Jobs;
4
using Unity.NetCode;
5
using UnityEngine;
6
using Unity.Transforms;
7
using Unity.Mathematics;
8
9
//We are updating only in the client world because only the client must specify exactly which player entity it "owns"
10
[UpdateInWorld(UpdateInWorld.TargetWorld.Client)]
11
//We will be updating after NetCode's GhostSpawnClassificationSystem because we want
12
//to ensure that the PredictedGhostComponent (which it adds) is available on the player entity to identify it
13
[UpdateInGroup(typeof(GhostSimulationSystemGroup))]
14
[UpdateAfter(typeof(GhostSpawnClassificationSystem))]
15
public class PlayerGhostSpawnClassificationSystem : SystemBase
16
{
17
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
18
19
//We will store the Camera prefab here which we will attach when we identify our player entity
20
private Entity m_CameraPrefab;
21
22
protected override void OnCreate()
23
{
24
m_BeginSimEcb = World.GetExistingSystem<BeginSimulationEntityCommandBufferSystem>();
25
26
//We need to make sure we have NCE before we start the update loop (otherwise it's unnecessary)
27
RequireSingletonForUpdate<NetworkIdComponent>();
28
RequireSingletonForUpdate<CameraAuthoringComponent>();
29
}
30
31
struct GhostPlayerState : ISystemStateComponentData
32
{
33
}
34
35
36
protected override void OnUpdate()
37
{
38
//Here we set the prefab we will use
39
if (m_CameraPrefab == Entity.Null)
40
{
41
//We grab our camera and set our variable
42
m_CameraPrefab = GetSingleton<CameraAuthoringComponent>().Prefab;
43
return;
44
}
45
46
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer().AsParallelWriter();
47
48
//We must declare our local variables before using them
49
var camera = m_CameraPrefab;
50
//The "playerEntity" is the NCE
51
var playerEntity = GetSingletonEntity<NetworkIdComponent>();
52
//The false is to signify that the data will NOT be read-only
53
var commandTargetFromEntity = GetComponentDataFromEntity<CommandTargetComponent>(false);
54
55
//We search for player entities with a PredictedGhostComponent (which means it is ours)
56
//In order to write over a variable that we pass through to a job we must include "WithNativeDisableParallelForRestricion"
57
//It means "yes we know what we are doing, allow us to write over this variable"
58
Entities
59
.WithAll<PlayerTag, PredictedGhostComponent>()
60
.WithNone<GhostPlayerState>()
61
.WithNativeDisableParallelForRestriction(commandTargetFromEntity)
62
.ForEach((Entity entity, int entityInQueryIndex) =>
63
{
64
//Here is where we update the NCE's CommandTargetComponent targetEntity to point at our player entity
65
var state = commandTargetFromEntity[playerEntity];
66
state.targetEntity = entity;
67
commandTargetFromEntity[playerEntity] = state;
68
69
//Here we add a special "ISystemStateComponentData" component
70
//This component does NOT get deleted with the rest of the entity
71
//Unity provided this special type of component to provide us a way to do "clean up" when important entities are destroyed
72
//In our case we will use GhostPlayerState to know when our player entity has been destroyed and will
73
//allow us to set our NCE CommandTargetComponent's targetEntity field back to null
74
//It also helps us ensure that we only set up our player entity once because we have a
75
//.WithNone<GhostPlayerState>() on our entity query
76
commandBuffer.AddComponent(entityInQueryIndex, entity, new GhostPlayerState());
77
78
//This creates our camera
79
var cameraEntity = commandBuffer.Instantiate(entityInQueryIndex, camera);
80
//This is how you "attach" a prefab entity to another
81
commandBuffer.AddComponent(entityInQueryIndex, cameraEntity, new Parent { Value = entity });
82
commandBuffer.AddComponent(entityInQueryIndex, cameraEntity, new LocalToParent() );
83
84
}).ScheduleParallel();
85
86
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
87
}
88
}
89
Copied!
    In here you'll notice a new type of component we have not seen before in this system "ISystemStateComponent"

System State Components

You can use SystemStateComponentData to track resources internal to a system and create and destroy those resources as needed without relying on individual callbacks.
SystemStateComponentData and SystemStateSharedComponentData are similar toComponentData and SharedComponentData, but ECS does not delete SystemStateComponentData when an entity is destroyed.
When an entity is destroyed, ECS usually:
    1.
    Finds all components which reference the particular entity's ID.
    2.
    Deletes those components.
    3.
    Recycles the entity ID for reuse.
However, if SystemStateComponentData is present, ECS does not recycle the ID. This gives the system the opportunity to clean up any resources or states associated with the entity ID. ECS only reuses the entity ID once SystemStateComponentData is removed.
    In the subsequent sections of this gitbook we will need to clean up our NCE and set targetEntity back to null if the server destroys our prefab
      Adding this component acts as a callback to trigger our clean-up system
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
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:
    1.
    Reimport all assets (this sometimes fixes it, if not go to step 2)
    2.
    Quit out of Unity entirely, then reopen the Project (this should fix it most of the time, if not go to step 3)
    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
      Adding a GhostOwnerComponent
    Created a CameraAuthoringComponent and adding it to the PrefabCollection
    Created PlayerSpawnRequestRpc
    Deleted InputSpawnSystem and InputMovementSystem
    Created InputSystem
    Created PlayerSpawnSystem
    Created PlayerGhostSpawnClassificationSystem

Updating player movement

First, some background:

DOTS Physics simulation and client NetCode prediction is not currently supported. What this means is that you cannot keep updating PhysicsVelocity.Linear to get instant response as the client.
Previously, before NetCode, we were able to instantly update our PhysicsVelocity.Linear when thrusting. Because the client world does not run "prediction" (aka the physics to move the player according to the velocity update) we cannot currently depend on PhysicsVelocity.Linear. We COULD "tell" the server we want a change in Physics.Velocity and see the response once the SnapShot returns (basically like we did with spawning a player). Instead, we are going to bring back VelocityComponent and MovementSystem to get immediate response.
It is also possible to use a kinematic character controller based on ray-casts for prediction (recommended), but our gitbook is more of an introductory gitbook, not one that goes super in-depth on a certain aspect. If you really want to know about kinematic character controller based on ray-casts for prediction, join our Discord and let us know! We can add more pages to this gitbook if the community finds it really helpful.
PhysicsVelocity still works even when it does not need to be updated because the server is handling all of it. This is why the Asteroids will bounce off each other just fine. We will set up our systems in such a way so that it is a trivial task to back to using PhysicsVelocity from VelocityComponent when Physics actually supports it.
The only downside is what happens when a player collides with an asteroid at quick speeds: the player will overlap the asteroid for a bit, but we can just imagine the asteroids have a nice cushy exterior... 😬
We are also going to implement code to generate data for "Thin Clients" in "Multiplayer PlayMode Tools." The updates will be in the updated InputSystem where you will see the use of HasSingleton<ThinClientComponent>();

Now, let's implement:

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:
1
using Unity.Networking.Transport;
2
using Unity.NetCode;
3
using Unity.Burst;
4
using Unity.Entities;
5
using Unity. Transforms;
6
using Unity.Mathematics;
7
8
9
[GhostComponent(PrefabType = GhostPrefabType.AllPredicted)]
10
public struct PlayerCommand : ICommandData
11
{
12
public uint Tick {get; set;}
13
public byte right;
14
public byte left;
15
public byte thrust;
16
public byte reverseThrust;
17
public byte selfDestruct;
18
public byte shoot;
19
public float mouseX;
20
public float mouseY;
21
}
22
Copied!
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
1
using Unity.Entities;
2
using UnityEngine;
3
4
public class PlayerCommandBufferAuthoringComponent : MonoBehaviour, IConvertGameObjectToEntity
5
{
6
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
7
{
8
dstManager.AddBuffer<PlayerCommand>(entity);
9
}
10
}
Copied!
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
    Now, let's navigate to the VelocityComponent file and drag it into Mixed/Components
    Let's also paste the code snippet below into the VelocityComponent.cs:
1
using Unity.Entities;
2
using Unity.Mathematics;
3
using Unity.NetCode;
4
5
[GenerateAuthoringComponent]
6
public struct VelocityComponent : IComponentData
7
{
8
[GhostField]
9
public float3 Linear;
10
}
Copied!
    Let's drag the MovementSystem file in a new folder Mixed/Systems (make a new folder named "Systems" within the "Mixed" folder)
    Paste the code snippet below in MovementSystem.cs (this new code adds a few updates so that this system runs in GhostPredictionSystemGroup for proper NetCode prediction)
1
using Unity.Entities;
2
using Unity.Mathematics;
3
using Unity.Transforms;
4
using UnityEngine;
5
using Unity.Burst;
6
using Unity.NetCode;
7
8
[UpdateInWorld(UpdateInWorld.TargetWorld.ClientAndServer)]
9
[UpdateInGroup(typeof(GhostPredictionSystemGroup))]
10
public class MovementSystem : SystemBase
11
{
12
private GhostPredictionSystemGroup m_PredictionGroup;
13
14
protected override void OnCreate()
15
{
16
m_PredictionGroup = World.GetOrCreateSystem<GhostPredictionSystemGroup>();
17
}
18
19
protected override void OnUpdate()
20
{
21
var deltaTime = m_PredictionGroup.Time.DeltaTime;
22
var currentTick = m_PredictionGroup.PredictingTick;
23
24
Entities
25
.ForEach((ref Translation position, in VelocityComponent velocity, in PredictedGhostComponent prediction) =>
26
{
27
if (!GhostPredictionSystemGroup.ShouldPredict(currentTick, prediction))
28
return;
29
30
position.Value.xyz += velocity.Linear * deltaTime;
31
}).ScheduleParallel();
32
}
33
}
Copied!
    This system runs on both the client and server
    The client "predicts" the movement but the server ultimately decides game state by sending back ghost Snapshots of correct game state
    Let's do a quick review of NetCode's prediction handling to make sense of ".ShouldPredict()" in MovementSystem

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.
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:
1
using UnityEngine;
2
using Unity.Entities;
3
using Unity.NetCode;
4
5
//This is a special SystemGroup introduced in NetCode 0.5
6
//This group only exists on the client is meant to be used when commands are being created
7
[UpdateInGroup(typeof(GhostInputSystemGroup))]
8
public class InputSystem : SystemBase
9
{
10
//We will use the BeginSimulationEntityCommandBufferSystem for our structural changes
11
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
12
//We need this sytem group so we can grab its "ServerTick" for prediction when we respond to Commands
13
private ClientSimulationSystemGroup m_ClientSimulationSystemGroup;
14
15
//We are going to use this to rate limit bullets per second
16
//We could have included this in the game settings, no "ECS reason" not to
17
private float m_PerSecond = 10f;
18
private float m_NextTime = 0;
19
20
//We will use this for simulating inputs with our ThinClients
21
private int m_FrameCount;
22
23
protected override void OnCreate()
24
{
25
26
//This will grab the BeginSimulationEntityCommandBuffer system to be used in OnUpdate
27
m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
28
29
//We set our ClientSimulationSystemGroup who will provide its ServerTick needed for the Commands
30
m_ClientSimulationSystemGroup = World.GetOrCreateSystem<ClientSimulationSystemGroup>();
31
32
//The client must have loaded the game to spawn a player so we wait for the
33
//NetworkStreamInGame component added during the load game flow
34
RequireSingletonForUpdate<NetworkStreamInGame>();
35
}
36
37
protected override void OnUpdate()
38
{
39
//Because players aren't spawned immediatly for Thin clients we still need a place to store the commands
40
bool isThinClient = HasSingleton<ThinClientComponent>();
41
if (HasSingleton<CommandTargetComponent>() && GetSingleton<CommandTargetComponent>().targetEntity == Entity.Null)
42
{
43
if (isThinClient)
44
{
45
// No ghosts are spawned, so create a placeholder struct to store the commands in
46
var ent = EntityManager.CreateEntity();
47
EntityManager.AddBuffer<PlayerCommand>(ent);
48
SetSingleton(new CommandTargetComponent{targetEntity = ent});
49
}
50
}
51
52
//We now have all our inputs
53
byte right, left, thrust, reverseThrust, selfDestruct, shoot;
54
right = left = thrust = reverseThrust = selfDestruct = shoot = 0;
55
56
//for looking around with mouse
57
float mouseX = 0;
58
float mouseY = 0;
59
60
//We are adding this difference so we can use "Num Thin Client" in "Multiplayer Mode Tools"
61
//These are the instructions if we are NOT a thin client
62
if (!isThinClient)
63
{
64
if (Input.GetKey("d"))
65
{
66
right = 1;
67
}
68
if (Input.GetKey("a"))
69
{
70
left = 1;
71
}
72
if (Input.GetKey("w"))
73
{
74
thrust = 1;
75
}
76
if (Input.GetKey("s"))
77
{
78
reverseThrust = 1;
79
}
80
if (Input.GetKey("p"))
81
{
82
selfDestruct = 1;
83
}
84
if (Input.GetKey("space"))
85
{
86
shoot = 1;
87
}
88
if (Input.GetMouseButton(1))
89
{
90
mouseX = Input.GetAxis("Mouse X");
91
mouseY = Input.GetAxis("Mouse Y");
92
93
}
94
}
95
96
//Here are the instructions if we ARE a thin client
97
else
98
{
99
// Spawn and generate some random inputs
100
var state = (int) Time.ElapsedTime % 3;
101
shoot = 1;
102
if (state == 0)
103
left = 1;
104
else
105
thrust = 1;
106
++m_FrameCount;
107
if (m_FrameCount % 100 == 0)
108
{
109
shoot = 1;
110
m_FrameCount = 0;
111
}
112
}
113
114
//Must declare local variables before using them in the .ForEach()
115
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer().AsParallelWriter();
116
//We grab the PlayerCommand buffer from the player so we can add to it
117
var inputFromEntity = GetBufferFromEntity<PlayerCommand>();
118
//We are sending the simulationsystemgroup tick so the server can playback our commands appropriately
119
var inputTargetTick = m_ClientSimulationSystemGroup.ServerTick;
120
121
//we are going to implement rate limiting for shooting
122
var canShoot = false;
123
if (UnityEngine.Time.time >= m_NextTime)
124
{
125
canShoot = true;
126
m_NextTime += (1/m_PerSecond);
127
}
128
129
//We query for the NCE by checking for entities with a NetworkIdComponent
130
//We also do not want to send an RPC if the NCE has become disconnected
131
Entities
132
.WithAll<NetworkIdComponent>()
133
.WithNone<NetworkStreamDisconnected>()
134
.ForEach((Entity entity, int nativeThreadIndex, in CommandTargetComponent commandTargetComponent) =>
135
{
136
// Special handling for thin clients since we can't tell if the ship is spawned or not
137
//This code only runs for thin clients
138
if (isThinClient && shoot != 0)
139
{
140
var req = commandBuffer.CreateEntity(nativeThreadIndex);
141
commandBuffer.AddComponent<PlayerSpawnRequestRpc>(nativeThreadIndex, req);
142
commandBuffer.AddComponent(nativeThreadIndex, req, new SendRpcCommandRequestComponent {TargetConnection = entity});
143
}
144
145
//If the targetEntity is null we want to spawn our player instead of add Command data to our buffer
146
if (commandTargetComponent.targetEntity == Entity.Null)
147
{
148
if (shoot != 0)
149
{
150
var req = commandBuffer.CreateEntity(nativeThreadIndex);
151
commandBuffer.AddComponent<PlayerSpawnRequestRpc>(nativeThreadIndex, req);
152
commandBuffer.AddComponent(nativeThreadIndex, req, new SendRpcCommandRequestComponent {TargetConnection = entity});
153
}
154
}
155
//Here we save our input data into a Command which will get automatically sent off to the server by NetCode
156
else
157
{
158
// If player, store commands in network command buffer
159
if (inputFromEntity.HasComponent(commandTargetComponent.targetEntity))
160
{
161
var willShoot = shoot;
162
//Here we implement our fire limiting
163
if (!canShoot && willShoot == 1)
164
willShoot = 0;
165
166
//Here we grab the CommandBuffer from our player entity
167
var input = inputFromEntity[commandTargetComponent.targetEntity];
168
169
//Notice that we are sending the ClientSimulationSystemGroup's server tick as part of the Command
170
//This is necessary for NetCode to function properly
171
input.AddCommandData(new PlayerCommand{Tick = inputTargetTick, left = left, right = right, thrust = thrust, reverseThrust = reverseThrust,
172
selfDestruct = selfDestruct, shoot = willShoot,
173
mouseX = mouseX,
174
mouseY = mouseY});
175
}
176
}
177
}).Schedule();
178
179
//We need to add the jobs dependency to the command buffer
180
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
181
}
182
}
Copied!
    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.
As well as setting the input buffer on an entity, your game code must also 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.
    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:
1
using Unity.Entities;
2
using Unity.Mathematics;
3
using Unity.Transforms;
4
using Unity.NetCode;
5
using Unity.Networking.Transport.Utilities;
6
using Unity.Collections;
7
using Unity.Physics;
8
using Unity.Jobs;
9
using UnityEngine;
10
11
//InputResponseMovementSystem runs on both the Client and Server
12
//It is predicted on the client but "decided" on the server
13
[UpdateInWorld(UpdateInWorld.TargetWorld.ClientAndServer)]
14
public class InputResponseMovementSystem : SystemBase
15
{
16
//This is a special NetCode group that provides a "prediction tick" and a fixed "DeltaTime"
17
private GhostPredictionSystemGroup m_PredictionGroup;
18
19
20
protected override void OnCreate()
21
{
22
// m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
23
24
//We will grab this system so we can use its "prediction tick" and "DeltaTime"
25
m_PredictionGroup = World.GetOrCreateSystem<GhostPredictionSystemGroup>();
26
27
}
28
29
protected override void OnUpdate()
30
{
31
//No need for a CommandBuffer because we are not making any structural changes to any entities
32
//We are setting values on components that already exist
33
// var commandBuffer = m_BeginSimEcb.CreateCommandBuffer().AsParallelWriter();
34
35
//These are special NetCode values needed to work the prediction system
36
var currentTick = m_PredictionGroup.PredictingTick;
37
var deltaTime = m_PredictionGroup.Time.DeltaTime;
38
39
//We must declare our local variables before the .ForEach()
40
var playerForce = GetSingleton<GameSettingsComponent>().playerForce;
41
42
//We will grab the buffer of player commands from the player entity
43
var inputFromEntity = GetBufferFromEntity<PlayerCommand>(true);
44
45
//We are looking for player entities that have PlayerCommands in their buffer
46
Entities
47
.WithReadOnly(inputFromEntity)
48
.WithAll<PlayerTag, PlayerCommand>()
49
.ForEach((Entity entity, int nativeThreadIndex, ref Rotation rotation, ref VelocityComponent velocity,
50
in GhostOwnerComponent ghostOwner, in PredictedGhostComponent prediction) =>
51
{
52
//Here we check if we SHOULD do the prediction based on the tick, if we shouldn't, we return
53
if (!GhostPredictionSystemGroup.ShouldPredict(currentTick, prediction))
54
return;
55
56
//We grab the buffer of commands from the player entity
57
var input = inputFromEntity[entity];
58
59
//We then grab the Command from the current tick (which is the PredictingTick)
60
//if we cannot get it at the current tick we make sure shoot is 0
61
//This is where we will store the current tick data
62
PlayerCommand inputData;
63
if (!input.GetDataAtTick(currentTick, out inputData))
64
inputData.shoot = 0;
65
66
if (inputData.right == 1)
67
{ //thrust to the right of where the player is facing
68
velocity.Linear += math.mul(rotation.Value, new float3(1,0,0)).xyz * playerForce * deltaTime;
69
}
70
if (inputData.left == 1)
71
{ //thrust to the left of where the player is facing
72
velocity.Linear += math.mul(rotation.Value, new float3(-1,0,0)).xyz * playerForce * deltaTime;
73
}
74
if (inputData.thrust == 1)
75
{ //thrust forward of where the player is facing
76
velocity.Linear += math.mul(rotation.Value, new float3(0,0,1)).xyz * playerForce * deltaTime;
77
}
78
if (inputData.reverseThrust == 1)
79
{ //thrust backwards of where the player is facing
80
velocity.Linear += math.mul(rotation.Value, new float3(0,0,-1)).xyz * playerForce * deltaTime;
81
}
82
83
84
if (inputData.mouseX != 0 || inputData.mouseY != 0)
85
{ //move the mouse
86
//here we have "hardwired" the look speed, we could have included this in the GameSettingsComponent to make it configurable
87
float lookSpeedH = 2f;
88
float lookSpeedV = 2f;
89
Quaternion currentQuaternion = rotation.Value;
90
float yaw = currentQuaternion.eulerAngles.y;
91
float pitch = currentQuaternion.eulerAngles.x;
92
93
//MOVING WITH MOUSE
94
yaw += lookSpeedH * inputData.mouseX;
95
pitch -= lookSpeedV * inputData.mouseY;
96
Quaternion newQuaternion = Quaternion.identity;
97
newQuaternion.eulerAngles = new Vector3(pitch,yaw, 0);
98
rotation.Value = newQuaternion;
99
}
100
101
}).ScheduleParallel();
102
103
//No need to .AddJobHandleForProducer() because we did not need a CommandBuffer to make structural changes
104
}
105
106
}
Copied!
    Now we need to add our VelocityComponent to our Player prefab
      Select Player and Add Component in the Inspector to add VelocityComponent
    Reimport ConvertedSubScene then navigate to "Multiplayer" select "Code Generation Window", hit "Rescan" then "Build"
      Sometimes adding components to the Player prefab requires rescanning and rebuilding NetCode generated code
Adding VelocityComponent to Player prefab and re-building NetCode generated code
    Navigate to GameSettings in ConvertedSubScene and slow down the Asteroid Velocity to 1 to make them feel a bit more "floaty" and increase the Player Force to 50 to make the player controls feel a bit more "zippy" (if it isn't at 50 already)
    Reimport ConvertedSubScene and hit "play"
Updating GameSettings and navigating
    We are able to spawn and move around through ICommandData
    Now let's navigate to PlayMode Tools (under Multiplayer menu) and make Num Thin Clients = 2 and hit play
    We have our simulated clients running commands and we are able to move around
    Now let's do some clean up
      Move PlayerTag into Mixed/Components
      Move PlayerAuthoringComponent to Server/Components
    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 InputResponse system
Github branch link:
git clone https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/ git checkout 'Updating-Players'
Last modified 5mo ago