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:
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 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.
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
using Unity.Entities;
using Unity.NetCode;
[GenerateAuthoringComponent]
public struct PlayerEntityComponent : IComponentData
{
public Entity PlayerEntity;
}
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:
using Unity.Entities;
using UnityEngine;
[GenerateAuthoringComponent]
public struct CameraAuthoringComponent : IComponentData
{
public Entity Prefab;
}
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:
using Unity.NetCode;
using Unity.Entities;
public struct PlayerSpawnRequestRpc : IRpcCommand
{
}
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:
using UnityEngine;
using Unity.Entities;
using Unity.NetCode;
//This is a special SystemGroup introduced in NetCode 0.5
//This group only exists on the client and is meant to be used when commands are being created
[UpdateInGroup(typeof(GhostInputSystemGroup))]
public partial class InputSystem : SystemBase
{
//We will use the BeginSimulationEntityCommandBufferSystem for our structural changes
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
//We need this sytem group so we can grab its "ServerTick" for prediction when we respond to Commands
private ClientSimulationSystemGroup m_ClientSimulationSystemGroup;
protected override void OnCreate()
{
//This will grab the BeginSimulationEntityCommandBuffer system to be used in OnUpdate
m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
//We set our ClientSimulationSystemGroup who will provide its ServerTick needed for the Commands
m_ClientSimulationSystemGroup = World.GetOrCreateSystem<ClientSimulationSystemGroup>();
//The client must have loaded the game to spawn a player so we wait for the
//NetworkStreamInGame component added during the load game flow
RequireSingletonForUpdate<NetworkStreamInGame>();
}
protected override void OnUpdate()
{
//We have removed the other inputs for now and will add them in the movement section
byte shoot;
shoot = 0;
if (Input.GetKey("space"))
{
shoot = 1;
};
//Must declare local variables before using them in the .ForEach()
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer();
TryGetSingletonEntity<PlayerCommand>(out var targetEntity);
Job.WithCode(() => {
if (targetEntity == Entity.Null && shoot != 0)
{
var req = commandBuffer.CreateEntity();
commandBuffer.AddComponent<PlayerSpawnRequestRpc>(req);
commandBuffer.AddComponent(req, new SendRpcCommandRequestComponent());
}
}).Schedule();
//We need to add the jobs dependency to the command buffer
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
}
}
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:
using System.Diagnostics;
using Unity.Entities;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
using Unity.NetCode;
using UnityEngine;
//This tag is only used by the systems in this file so we define it here
public struct PlayerSpawnInProgressTag : IComponentData
{
}
//Only the server will be running this system to spawn the player
[UpdateInGroup(typeof(ServerSimulationSystemGroup))]
public partial class PlayerSpawnSystem : SystemBase
{
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
private Entity m_Prefab;
protected override void OnCreate()
{
m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
//We check to ensure GameSettingsComponent exists to know if the SubScene has been streamed in
//We need the SubScene for actions in our OnUpdate()
RequireSingletonForUpdate<GameSettingsComponent>();
}
protected override void OnUpdate()
{
//Here we set the prefab we will use
if (m_Prefab == Entity.Null)
{
//We grab the converted PrefabCollection Entity's PlayerAuthoringComponent
//and set m_Prefab to its Prefab value
m_Prefab = GetSingleton<PlayerAuthoringComponent>().Prefab;
//we must "return" after setting this prefab because if we were to continue into the Job
//we would run into errors because the variable was JUST set (ECS funny business)
//comment out return and see the error
return;
}
//Because of how ECS works we must declare local variables that will be used within the job
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer();
var playerPrefab = m_Prefab;
var rand = new Unity.Mathematics.Random((uint) Stopwatch.GetTimestamp());
var gameSettings = GetSingleton<GameSettingsComponent>();
//GetComponentDataFromEntity allows us to grab data from an entity that we don't have access to
//until we are within a job
//We know we will need to get the PlayerSpawningStateComponent from an NCE but we don't know which one yet
//So we create a variable that will get PlayerSpawningStateComponent from an entity
var playerStateFromEntity = GetComponentDataFromEntity<PlayerSpawningStateComponent>();
//Similar to playerStateFromEntity, these variables WILL get data from an entity (in the job below)
//but do not have it currently
var networkIdFromEntity = GetComponentDataFromEntity<NetworkIdComponent>();
//We are looking for an Entity with a PlayerSpawnRequestRpc
//That means the client associated with that NCE wants a player to be spawned for them
Entities
.ForEach((Entity entity, in PlayerSpawnRequestRpc request,
in ReceiveRpcCommandRequestComponent requestSource) =>
{
//We immediately destroy the request so we act on it once
commandBuffer.DestroyEntity(entity);
// This makes sure that we don't act on another RPC from the same NCE before we finish spawning the first
if (!playerStateFromEntity.HasComponent(requestSource.SourceConnection) ||
playerStateFromEntity[requestSource.SourceConnection].IsSpawning != 0)
{
return;
}
//We create our player prefab
var player = commandBuffer.Instantiate(playerPrefab);
//We will spawn our player in the center-ish of our game
var width = gameSettings.levelWidth * .2f;
var height = gameSettings.levelHeight * .2f;
var depth = gameSettings.levelDepth * .2f;
var pos = new Translation
{
Value = new float3(rand.NextFloat(-width, width),
rand.NextFloat(-height, height), rand.NextFloat(-depth, depth))
};
//We will not spawn a random rotation for simplicity but include
//setting rotation for you to be able to update in your own projects if you like
var rot = new Rotation {Value = Quaternion.identity};
//Here we set the componets that already exist on the Player prefab
commandBuffer.SetComponent(player, pos);
commandBuffer.SetComponent(player, rot);
//This sets the GhostOwnerComponent value to the NCE NetworkId (how we know what client the player belongs to)
commandBuffer.SetComponent(player, new GhostOwnerComponent {NetworkId = networkIdFromEntity[requestSource.SourceConnection].Value});
//This sets the PlayerEntity value in PlayerEntityComponent to the NCE
commandBuffer.SetComponent(player, new PlayerEntityComponent {PlayerEntity = requestSource.SourceConnection});
//Here we add a component that was not included in the Player prefab, PlayerSpawnInProgressTag
//This is a temporary tag used to make sure the entity was able to be created and will be removed
//in PlayerCompleteSpawnSystem below
commandBuffer.AddComponent(player, new PlayerSpawnInProgressTag());
//We update the PlayerSpawningStateComponent tag on the NCE to "has a spawning/spawned player" (1)
playerStateFromEntity[requestSource.SourceConnection] = new PlayerSpawningStateComponent {IsSpawning = 1};
}).Schedule();
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
}
}
//We want to complete the spawn before ghosts are sent on the server
[UpdateInGroup(typeof(ServerSimulationSystemGroup))]
[UpdateBefore(typeof(GhostSendSystem))]
public partial class PlayerCompleteSpawnSystem : SystemBase
{
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
protected override void OnCreate()
{
m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
}
protected override void OnUpdate()
{
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer();
//GetComponentDataFromEntity allows us to grab data from an entity that we don't have access to
//until we are within a job
//We don't know exactly which NCE we currently want to grab data from, but we do know we will want to
//so we use GetComponentDataFromEntity to prepare ECS that we will be grabbing this data from an entity
var playerStateFromEntity = GetComponentDataFromEntity<PlayerSpawningStateComponent>();
var connectionFromEntity = GetComponentDataFromEntity<NetworkStreamConnection>();
// This is used to help with clean up when players disconnect, we keep track
var linkedEntityGroupFromEntity = GetBufferFromEntity<LinkedEntityGroup>();
//Now the server checks for Players that are in the middle of spawning (haven't been sent yet)
Entities
.WithAll<PlayerSpawnInProgressTag>()
.ForEach((Entity entity, in PlayerEntityComponent player) =>
{
//This is another check from Unity samples
//This ensures there was no disconnect
if (!playerStateFromEntity.HasComponent(player.PlayerEntity) ||
!connectionFromEntity[player.PlayerEntity].Value.IsCreated)
{
//Player was disconnected during spawn, or other error so delete
commandBuffer.DestroyEntity(entity);
return;
}
//If there was no error with spawning the player we can remove the PlayerSpawnInProgressTag
commandBuffer.RemoveComponent<PlayerSpawnInProgressTag>(entity);
// We add the the player to the linked entity group (so we know what to delete when the player disconnects)
var linkedEntityGroup = linkedEntityGroupFromEntity[player.PlayerEntity];
linkedEntityGroup.Add(new LinkedEntityGroup {Value = entity});
}).Schedule();
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
}
}
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:
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.NetCode;
using UnityEngine;
using Unity.Transforms;
using Unity.Mathematics;
//We are updating only in the client world because only the client must specify exactly which player entity it "owns"
[UpdateInWorld(TargetWorld.Client)]
//We will be updating after NetCode's GhostSpawnClassificationSystem because we want
//to ensure that the PredictedGhostComponent (which it adds) is available on the player entity to identify it
[UpdateInGroup(typeof(GhostSimulationSystemGroup))]
[UpdateAfter(typeof(GhostSpawnClassificationSystem))]
public partial class PlayerGhostSpawnClassificationSystem : SystemBase
{
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
//We will store the Camera prefab here which we will attach when we identify our player entity
private Entity m_CameraPrefab;
protected override void OnCreate()
{
m_BeginSimEcb = World.GetExistingSystem<BeginSimulationEntityCommandBufferSystem>();
//We need to make sure we have NCE before we start the update loop (otherwise it's unnecessary)
RequireSingletonForUpdate<NetworkIdComponent>();
RequireSingletonForUpdate<CameraAuthoringComponent>();
}
protected override void OnUpdate()
{
//Here we set the prefab we will use
if (m_CameraPrefab == Entity.Null)
{
//We grab our camera and set our variable
m_CameraPrefab = GetSingleton<CameraAuthoringComponent>().Prefab;
return;
}
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer().AsParallelWriter();
//We must declare our local variables before using them
var camera = m_CameraPrefab;
//The "playerEntity" is the NCE
var networkIdComponent = GetSingleton<NetworkIdComponent>();
//The false is to signify that the data will NOT be read-only
var commandTargetFromEntity = GetComponentDataFromEntity<CommandTargetComponent>(false);
//We will look for Player prefabs that we have not added a "PlayerClassifiedTag" to (which means we have checked the player if it is "ours")
Entities
.WithAll<PlayerTag>()
.WithNone<PlayerClassifiedTag>()
.ForEach((Entity entity, int entityInQueryIndex, in GhostOwnerComponent ghostOwnerComponent) =>
{
// If this is true this means this Player is mine (because the GhostOwnerComponent value is equal to the NetworkId)
// Remember the GhostOwnerComponent value is set by the server and is ghosted to the client
if (ghostOwnerComponent.NetworkId == networkIdComponent.Value)
{
//This creates our camera
var cameraEntity = commandBuffer.Instantiate(entityInQueryIndex, camera);
//This is how you "attach" a prefab entity to another
commandBuffer.AddComponent(entityInQueryIndex, cameraEntity, new Parent { Value = entity });
commandBuffer.AddComponent(entityInQueryIndex, cameraEntity, new LocalToParent() );
}
// This means we have classified this Player prefab
commandBuffer.AddComponent(entityInQueryIndex, entity, new PlayerClassifiedTag() );
}).ScheduleParallel();
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
}
}
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:
using Unity.Networking.Transport;
using Unity.NetCode;
using Unity.Burst;
using Unity.Entities;
using Unity. Transforms;
using Unity.Mathematics;
[GhostComponent(PrefabType = GhostPrefabType.AllPredicted)]
public struct PlayerCommand : ICommandData
{
public uint Tick {get; set;}
public byte right;
public byte left;
public byte thrust;
public byte reverseThrust;
public byte selfDestruct;
public byte shoot;
public float mouseX;
public float mouseY;
}
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
using Unity.Entities;
using UnityEngine;
public class PlayerCommandBufferAuthoringComponent : MonoBehaviour, IConvertGameObjectToEntity
{
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
{
dstManager.AddBuffer<PlayerCommand>(entity);
}
}
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
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.
Drag the InputSystem file into the Client/Systems folder
Next let's update InputSystem by pasting the code snippet below into InputSystem.cs:
using UnityEngine;
using Unity.Entities;
using Unity.NetCode;
//This is a special SystemGroup introduced in NetCode 0.5
//This group only exists on the client and is meant to be used when commands are being created
[UpdateInGroup(typeof(GhostInputSystemGroup))]
public partial class InputSystem : SystemBase
{
//We will use the BeginSimulationEntityCommandBufferSystem for our structural changes
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
//We need this sytem group so we can grab its "ServerTick" for prediction when we respond to Commands
private ClientSimulationSystemGroup m_ClientSimulationSystemGroup;
//We are going to use this to rate limit bullets per second
//We could have included this in the game settings, no "ECS reason" not to
private float m_PerSecond = 10f;
private float m_NextTime = 0;
protected override void OnCreate()
{
//This will grab the BeginSimulationEntityCommandBuffer system to be used in OnUpdate
m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
//We set our ClientSimulationSystemGroup who will provide its ServerTick needed for the Commands
m_ClientSimulationSystemGroup = World.GetOrCreateSystem<ClientSimulationSystemGroup>();
//The client must have loaded the game to spawn a player so we wait for the
//NetworkStreamInGame component added during the load game flow
RequireSingletonForUpdate<NetworkStreamInGame>();
}
protected override void OnUpdate()
{
//We now have all our inputs
byte right, left, thrust, reverseThrust, selfDestruct, shoot;
right = left = thrust = reverseThrust = selfDestruct = shoot = 0;
//for looking around with mouse
float mouseX = 0;
float mouseY = 0;
if (Input.GetKey("d"))
{
right = 1;
}
if (Input.GetKey("a"))
{
left = 1;
}
if (Input.GetKey("w"))
{
thrust = 1;
}
if (Input.GetKey("s"))
{
reverseThrust = 1;
}
if (Input.GetKey("p"))
{
selfDestruct = 1;
}
if (Input.GetKey("space"))
{
shoot = 1;
}
if (Input.GetMouseButton(1))
{
mouseX = Input.GetAxis("Mouse X");
mouseY = Input.GetAxis("Mouse Y");
}
//we are going to implement rate limiting for shooting
var canShoot = false;
if (UnityEngine.Time.time >= m_NextTime)
{
canShoot = true;
m_NextTime += (1/m_PerSecond);
}
//We are sending the simulationsystemgroup tick so the server can playback our commands appropriately
var inputTargetTick = m_ClientSimulationSystemGroup.ServerTick;
//Must declare local variables before using them in the .ForEach()
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer();
// This is how we will grab the buffer of PlayerCommands from the player prefab
var inputFromEntity = GetBufferFromEntity<PlayerCommand>();
TryGetSingletonEntity<PlayerCommand>(out var targetEntity);
Job.WithCode(() => {
if (targetEntity == Entity.Null)
{
if (shoot != 0)
{
var req = commandBuffer.CreateEntity();
commandBuffer.AddComponent<PlayerSpawnRequestRpc>(req);
commandBuffer.AddComponent(req, new SendRpcCommandRequestComponent());
}
}
else
{
if (shoot == 1 && canShoot)
shoot = 1;
else
shoot = 0;
var input = inputFromEntity[targetEntity];
input.AddCommandData(new PlayerCommand{Tick = inputTargetTick, left = left, right = right, thrust = thrust, reverseThrust = reverseThrust,
selfDestruct = selfDestruct, shoot = shoot,
mouseX = mouseX,
mouseY = mouseY});
}
}).Schedule();
//We need to add the jobs dependency to the command buffer
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
}
}
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.
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:
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using Unity.NetCode;
using Unity.Networking.Transport.Utilities;
using Unity.Collections;
using Unity.Physics;
using Unity.Physics.Systems;
using Unity.Jobs;
using UnityEngine;
//InputResponseMovementSystem runs on both the Client and Server
//It is predicted on the client but "decided" on the server
[UpdateInWorld(TargetWorld.ClientAndServer)]
// [UpdateInGroup(typeof(PredictedPhysicsSystemGroup))]
// want to change the Velocity BEFORE the physics is run (which happens after BuildPhysicsWorld)
// so it is not like the input had no affect on the player for a frame), so we run before BuildPhysicsWorld
[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
[UpdateBefore(typeof(BuildPhysicsWorld))]
public partial class InputResponseMovementSystem : SystemBase
{
//This is a special NetCode group that provides a "prediction tick" and a fixed "DeltaTime"
private GhostPredictionSystemGroup m_PredictionGroup;
protected override void OnCreate()
{
// m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
//We will grab this system so we can use its "prediction tick" and "DeltaTime"
m_PredictionGroup = World.GetOrCreateSystem<GhostPredictionSystemGroup>();
// Creating this Singleton is what allows the client to predict physics
// Once the singleton is present, all the physics systems are moved into a new group inside the GhostPredictionSystemGroup that run in sync with the ghost simulation as expected.
// The PhysicsVelocity is replicated to all clients and it is the only thing it necessary to sync the physic state.
Entity physicsSingleton = EntityManager.CreateEntity();
EntityManager.AddComponentData(physicsSingleton, new PredictedPhysicsConfig {});
}
protected override void OnUpdate()
{
//No need for a CommandBuffer because we are not making any structural changes to any entities
//We are setting values on components that already exist
// var commandBuffer = m_BeginSimEcb.CreateCommandBuffer().AsParallelWriter();
//These are special NetCode values needed to work the prediction system
var currentTick = m_PredictionGroup.PredictingTick;
var deltaTime = m_PredictionGroup.Time.DeltaTime;
//We must declare our local variables before the .ForEach()
var playerForce = GetSingleton<GameSettingsComponent>().playerForce;
//We will grab the buffer of player commands from the player entity
var inputFromEntity = GetBufferFromEntity<PlayerCommand>(true);
//We are looking for player entities that have PlayerCommands in their buffer
Entities
.WithReadOnly(inputFromEntity)
.WithAll<PlayerTag, PlayerCommand>()
.ForEach((Entity entity, int entityInQueryIndex, ref Rotation rotation, ref PhysicsVelocity velocity,
in GhostOwnerComponent ghostOwner, in PredictedGhostComponent prediction) =>
{
//Here we check if we SHOULD do the prediction based on the tick, if we shouldn't, we return
if (!GhostPredictionSystemGroup.ShouldPredict(currentTick, prediction))
return;
//We grab the buffer of commands from the player entity
var input = inputFromEntity[entity];
//We then grab the Command from the current tick (which is the PredictingTick)
//if we cannot get it at the current tick we make sure shoot is 0
//This is where we will store the current tick data
PlayerCommand inputData;
if (!input.GetDataAtTick(currentTick, out inputData))
inputData.shoot = 0;
if (inputData.right == 1)
{ //thrust to the right of where the player is facing
velocity.Linear += math.mul(rotation.Value, new float3(1,0,0)).xyz * playerForce * deltaTime;
}
if (inputData.left == 1)
{ //thrust to the left of where the player is facing
velocity.Linear += math.mul(rotation.Value, new float3(-1,0,0)).xyz * playerForce * deltaTime;
}
if (inputData.thrust == 1)
{ //thrust forward of where the player is facing
velocity.Linear += math.mul(rotation.Value, new float3(0,0,1)).xyz * playerForce * deltaTime;
}
if (inputData.reverseThrust == 1)
{ //thrust backwards of where the player is facing
velocity.Linear += math.mul(rotation.Value, new float3(0,0,-1)).xyz * playerForce * deltaTime;
}
if (inputData.mouseX != 0 || inputData.mouseY != 0)
{ //move the mouse
//here we have "hardwired" the look speed, we could have included this in the GameSettingsComponent to make it configurable
float lookSpeedH = 2f;
float lookSpeedV = 2f;
Quaternion currentQuaternion = rotation.Value;
float yaw = currentQuaternion.eulerAngles.y;
float pitch = currentQuaternion.eulerAngles.x;
//MOVING WITH MOUSE
yaw += lookSpeedH * inputData.mouseX;
pitch -= lookSpeedV * inputData.mouseY;
Quaternion newQuaternion = Quaternion.identity;
newQuaternion.eulerAngles = new Vector3(pitch,yaw, 0);
rotation.Value = newQuaternion;
}
}).ScheduleParallel();
//No need to .AddJobHandleForProducer() because we did not need a CommandBuffer to make structural changes
}
}
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
using UnityEngine;
using Unity.Entities;
using Unity.NetCode;
//This is a special SystemGroup introduced in NetCode 0.5
//This group only exists on the client and is meant to be used when commands are being created
[UpdateInGroup(typeof(GhostInputSystemGroup))]
public partial class InputSystem : SystemBase
{
//We will use the BeginSimulationEntityCommandBufferSystem for our structural changes
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
//We need this sytem group so we can grab its "ServerTick" for prediction when we respond to Commands
private ClientSimulationSystemGroup m_ClientSimulationSystemGroup;
//We are going to use this to rate limit bullets per second
//We could have included this in the game settings, no "ECS reason" not to
private float m_PerSecond = 10f;
private float m_NextTime = 0;
//We use this for thin client command generation
private int m_FrameCount;
protected override void OnCreate()
{
//This will grab the BeginSimulationEntityCommandBuffer system to be used in OnUpdate
m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
//We set our ClientSimulationSystemGroup who will provide its ServerTick needed for the Commands
m_ClientSimulationSystemGroup = World.GetOrCreateSystem<ClientSimulationSystemGroup>();
//The client must have loaded the game to spawn a player so we wait for the
//NetworkStreamInGame component added during the load game flow
RequireSingletonForUpdate<NetworkStreamInGame>();
}
protected override void OnUpdate()
{
bool isThinClient = HasSingleton<ThinClientComponent>();
if (HasSingleton<CommandTargetComponent>() && GetSingleton<CommandTargetComponent>().targetEntity == Entity.Null)
{
if (isThinClient)
{
// No ghosts are spawned, so create a placeholder struct to store the commands in
var ent = EntityManager.CreateEntity();
EntityManager.AddBuffer<PlayerCommand>(ent);
SetSingleton(new CommandTargetComponent{targetEntity = ent});
}
}
//We now have all our inputs
byte right, left, thrust, reverseThrust, selfDestruct, shoot;
right = left = thrust = reverseThrust = selfDestruct = shoot = 0;
//for looking around with mouse
float mouseX = 0;
float mouseY = 0;
//We are adding this difference so we can use "Num Thin Client" in "Multiplayer Mode Tools"
//These are the instructions if we are NOT a thin client
if (!isThinClient)
{
if (Input.GetKey("d"))
{
right = 1;
}
if (Input.GetKey("a"))
{
left = 1;
}
if (Input.GetKey("w"))
{
thrust = 1;
}
if (Input.GetKey("s"))
{
reverseThrust = 1;
}
if (Input.GetKey("p"))
{
selfDestruct = 1;
}
if (Input.GetKey("space"))
{
shoot = 1;
}
if (Input.GetMouseButton(1))
{
mouseX = Input.GetAxis("Mouse X");
mouseY = Input.GetAxis("Mouse Y");
}
}
else
{
// Spawn and generate some random inputs
var state = (int) Time.ElapsedTime % 3;
if (state == 0)
{
left = 1;
}
else {
thrust = 1;
}
++m_FrameCount;
if (m_FrameCount % 100 == 0)
{
shoot = 1;
m_FrameCount = 0;
}
}
//we are going to implement rate limiting for shooting
var canShoot = false;
if (UnityEngine.Time.time >= m_NextTime)
{
canShoot = true;
m_NextTime += (1/m_PerSecond);
}
//We are sending the simulationsystemgroup tick so the server can playback our commands appropriately
var inputTargetTick = m_ClientSimulationSystemGroup.ServerTick;
//Must declare local variables before using them in the .ForEach()
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer();
// This is how we will grab the buffer of PlayerCommands from the player prefab
var inputFromEntity = GetBufferFromEntity<PlayerCommand>();
TryGetSingletonEntity<PlayerCommand>(out var targetEntity);
Job.WithCode(() => {
if (isThinClient && shoot != 0)
{
// Special handling for thin clients since we can't tell if the ship is spawned or not
// This means every time we shoot we also send an RPC, but the Server protects against creating more Players
var req = commandBuffer.CreateEntity();
commandBuffer.AddComponent<PlayerSpawnRequestRpc>(req);
commandBuffer.AddComponent(req, new SendRpcCommandRequestComponent());
}
if (targetEntity == Entity.Null)
{
if (shoot != 0)
{
var req = commandBuffer.CreateEntity();
commandBuffer.AddComponent<PlayerSpawnRequestRpc>(req);
commandBuffer.AddComponent(req, new SendRpcCommandRequestComponent());
}
}
else
{
if (shoot == 1 && canShoot)
shoot = 1;
else
shoot = 0;
var input = inputFromEntity[targetEntity];
input.AddCommandData(new PlayerCommand{Tick = inputTargetTick, left = left, right = right, thrust = thrust, reverseThrust = reverseThrust,
selfDestruct = selfDestruct, shoot = shoot,
mouseX = mouseX,
mouseY = mouseY});
}
}).Schedule();
//We need to add the jobs dependency to the command buffer
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
}
}
Next let's update the PlayerSpawnSystem.cs
using System.Diagnostics;
using Unity.Entities;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
using Unity.NetCode;
using UnityEngine;
//This tag is only used by the systems in this file so we define it here
public struct PlayerSpawnInProgressTag : IComponentData
{
}
//Only the server will be running this system to spawn the player
[UpdateInGroup(typeof(ServerSimulationSystemGroup))]
public partial class PlayerSpawnSystem : SystemBase
{
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
private Entity m_Prefab;
protected override void OnCreate()
{
m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
//We check to ensure GameSettingsComponent exists to know if the SubScene has been streamed in
//We need the SubScene for actions in our OnUpdate()
RequireSingletonForUpdate<GameSettingsComponent>();
}
protected override void OnUpdate()
{
//Here we set the prefab we will use
if (m_Prefab == Entity.Null)
{
//We grab the converted PrefabCollection Entity's PlayerAuthoringComponent
//and set m_Prefab to its Prefab value
m_Prefab = GetSingleton<PlayerAuthoringComponent>().Prefab;
//we must "return" after setting this prefab because if we were to continue into the Job
//we would run into errors because the variable was JUST set (ECS funny business)
//comment out return and see the error
return;
}
//Because of how ECS works we must declare local variables that will be used within the job
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer();
var playerPrefab = m_Prefab;
var rand = new Unity.Mathematics.Random((uint) Stopwatch.GetTimestamp());
var gameSettings = GetSingleton<GameSettingsComponent>();
//GetComponentDataFromEntity allows us to grab data from an entity that we don't have access to
//until we are within a job
//We know we will need to get the PlayerSpawningStateComponent from an NCE but we don't know which one yet
//So we create a variable that will get PlayerSpawningStateComponent from an entity
var playerStateFromEntity = GetComponentDataFromEntity<PlayerSpawningStateComponent>();
//Similar to playerStateFromEntity, these variables WILL get data from an entity (in the job below)
//but do not have it currently
var commandTargetFromEntity = GetComponentDataFromEntity<CommandTargetComponent>();
var networkIdFromEntity = GetComponentDataFromEntity<NetworkIdComponent>();
//We are looking for NCEs with a PlayerSpawnRequestRpc
//That means the client associated with that NCE wants a player to be spawned for them
Entities
.ForEach((Entity entity, in PlayerSpawnRequestRpc request,
in ReceiveRpcCommandRequestComponent requestSource) =>
{
//We immediately destroy the request so we act on it once
commandBuffer.DestroyEntity(entity);
//These are checks to see if the NCE has disconnected or if there are any other issues
//These checks are pulled from Unity samples and we have left them in even though they seem
//Is there a PlayerSpawningState on the NCE
//Is there a CommandTargetComponent on the NCE
//Is the CommandTargetComponent targetEntity != Entity.Null
//Is the PlayerSpawningState == 0
//If all those are true we continue with spawning, otherwise we don't
if (!playerStateFromEntity.HasComponent(requestSource.SourceConnection) ||
!commandTargetFromEntity.HasComponent(requestSource.SourceConnection) ||
commandTargetFromEntity[requestSource.SourceConnection].targetEntity != Entity.Null ||
playerStateFromEntity[requestSource.SourceConnection].IsSpawning != 0)
return;
//We create our player prefab
var player = commandBuffer.Instantiate(playerPrefab);
//We will spawn our player in the center-ish of our game
var width = gameSettings.levelWidth * .2f;
var height = gameSettings.levelHeight * .2f;
var depth = gameSettings.levelDepth * .2f;
var pos = new Translation
{
Value = new float3(rand.NextFloat(-width, width),
rand.NextFloat(-height, height), rand.NextFloat(-depth, depth))
};
//We will not spawn a random rotation for simplicity but include
//setting rotation for you to be able to update in your own projects if you like
var rot = new Rotation {Value = Quaternion.identity};
//Here we set the componets that already exist on the Player prefab
commandBuffer.SetComponent(player, pos);
commandBuffer.SetComponent(player, rot);
//This sets the GhostOwnerComponent value to the NCE NetworkId
commandBuffer.SetComponent(player, new GhostOwnerComponent {NetworkId = networkIdFromEntity[requestSource.SourceConnection].Value});
//This sets the PlayerEntity value in PlayerEntityComponent to the NCE
commandBuffer.SetComponent(player, new PlayerEntityComponent {PlayerEntity = requestSource.SourceConnection});
//Here we add a component that was not included in the Player prefab, PlayerSpawnInProgressTag
//This is a temporary tag used to make sure the entity was able to be created and will be removed
//in PlayerCompleteSpawnSystem below
commandBuffer.AddComponent(player, new PlayerSpawnInProgressTag());
//We update the PlayerSpawningStateComponent tag on the NCE to "currently spawning" (1)
playerStateFromEntity[requestSource.SourceConnection] = new PlayerSpawningStateComponent {IsSpawning = 1};
}).Schedule();
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
}
}
//We want to complete the spawn before ghosts are sent on the server
[UpdateInGroup(typeof(ServerSimulationSystemGroup))]
[UpdateBefore(typeof(GhostSendSystem))]
public partial class PlayerCompleteSpawnSystem : SystemBase
{
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
protected override void OnCreate()
{
m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
}
protected override void OnUpdate()
{
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer();
//GetComponentDataFromEntity allows us to grab data from an entity that we don't have access to
//until we are within a job
//We don't know exactly which NCE we currently want to grab data from, but we do know we will want to
//so we use GetComponentDataFromEntity to prepare ECS that we will be grabbing this data from an entity
var playerStateFromEntity = GetComponentDataFromEntity<PlayerSpawningStateComponent>();
var commandTargetFromEntity = GetComponentDataFromEntity<CommandTargetComponent>();
var connectionFromEntity = GetComponentDataFromEntity<NetworkStreamConnection>();
Entities.WithAll<PlayerSpawnInProgressTag>().
ForEach((Entity entity, in PlayerEntityComponent player) =>
{
// This is another check from Unity samples
// This ensures there was no disconnect
if (!playerStateFromEntity.HasComponent(player.PlayerEntity) ||
!connectionFromEntity[player.PlayerEntity].Value.IsCreated)
{
//Player was disconnected during spawn, or other error so delete
commandBuffer.DestroyEntity(entity);
return;
}
//If there was no error with spawning the player we can remove the PlayerSpawnInProgressTag
commandBuffer.RemoveComponent<PlayerSpawnInProgressTag>(entity);
//We now update the NCE to point at our player entity
commandTargetFromEntity[player.PlayerEntity] = new CommandTargetComponent {targetEntity = entity};
//We can now say that our player is no longer spawning so we set IsSpawning = 0 on the NCE
playerStateFromEntity[player.PlayerEntity] = new PlayerSpawningStateComponent {IsSpawning = 0};
}).Schedule();
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
}
}
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