When the client side receives a new ghost, the ghost type is determined by a set of classification systems and then a spawn system spawns it. There is no specific spawn message, and when the client receives an unknown ghost ID, it counts as an implicit spawn.
Because the client interpolates snapshot data, Unity cannot spawn entities immediately, unless it was preemptively spawned, such as with spawn prediction. This is because the data is not ready for the client to interpolate it. Otherwise, the object would appear and then not get any more updates until the interpolation is ready.
Therefore normal spawns happen in a delayed manner. Spawning is split into three main types as follows:
Delayed or interpolated spawning. The entity is spawned when the interpolation system is ready to apply updates. This is how remote entities are handled, because they are interpolated in a straightforward manner.
Predicted spawning for the client predicted player object. The object is predicted so the input handling applies immediately. Therefore, it doesn't need to be delay spawned. While the snapshot data for this object arrives, the update system applies the data directly to the object and then plays back the local inputs which have happened since that time, and corrects mistakes in the prediction.
Predicted spawning for player spawned objects. These are objects that the player input spawns, like in-game bullets or rockets that the player fires.
Implement Predicted Spawning for player spawned objects
The spawn code needs to run on the client, in the client prediction system. The spawn should use the predicted client version of the ghost prefab and add a PredictedGhostSpawnRequestComponent to it. Then, when the first snapshot update for the entity arrives it will apply to that predict spawned object (no new entity is created). After this, the snapshot updates are applied the same as in the predicted spawning for client predicted player object model.
To create the prefab for predicted spawning, you should use the utility method GhostCollectionSystem.CreatePredictedSpawnPrefab.
You need to implement some specific code to handle the predicted spawning for player spawned objects. You need to create a system updating in the ClientSimulationSystemGroup after GhostSpawnClassificationSystem. The system needs to go through the GhostSpawnBuffer buffer stored on a singleton with a GhostSpawnQueueComponent. For each entry in that list it should compare to the entries in the PredictedGhostSpawn buffer on the singleton with a PredictedGhostSpawnList component. If the two entries are the same the classification system should set the PredictedSpawnEntity property in the GhostSpawnBuffer and remove the entry from GhostSpawnBuffer.
NetCode spawns entities on clients when there is a Prefab available for it. Pre spawned ghosts will work without any special consideration since they are referenced in a sub scene, but for manually spawned entities you must make sure that the prefabs exist on the client. You make sure that happens by having a component in a scene which references the prefab you want to spawn.
We are now on the third type of spawning listed above, "Predicted spawning for player spawned objects". Asteroids were "Delayed or interpolated spawning".
A fair amount of the workflow in this section is similar to spawning the Player prefab from the previous section because it is also predicted. We will have a BulletGhostSpawnClassificationSystem that is similar to PlayerGhostSpawnClassificationSystem. However, there will be a bit of a difference in workflow within BulletGhostSpawnClassificationSystem when it comes to identifying the client's bullets.
Updating our bullet spawn
Updating the Bullet Prefab
Navigate to the Bullet prefab
Add GhostAuthoringComponent
Name = Bullet
Importance = 200
Supported Ghost Mode = All
Default Ghost Mode = Owner Predicted
Optimization Mode = Dynamic
Check "Has Owner"
Updating to predicted bullet spawning
We are going to change our implementation of rate limiting from rate limiting on the client side, to rate limiting on the server side
This is more line with the authoritative model, the server should be in control of these limits
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;
//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 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
{
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);
}
}
We need a way to store firing data on the player entity so the server can know if the client's firing system has "cooled down"
So we are going to repurpose our BulletSpawnOffsetComponent and rename it to PlayerStateAndOffsetComponent
We could make a new component for rate limiting but this would mean we would need to put 9 different components into our .ForEach() for our InputResponseSpawnSystem 😱
Paste the code snippet below into PlayerStateAndOffsetComponent.cs:
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
public struct PlayerStateAndOffsetComponent : IComponentData
{
public float3 Value;
[GhostField]
public int State;
public uint WeaponCooldown;
}
We also included a "State" field in the component that can be updated when the user is thrusting or firing
Although we won't be doing anything with "State" in this project, you are totally free to add something like change a client's color when it is thrusting or firing, if you want! To do this update the State value and create a client-only system that updates Player prefab meshes based on the State value
Just a thought!
Since we updated BulletOffsetComponent we will also need to update our SetBulletSpawnOffset which references it
We will not rename the SetBulletSpawnOffset system and can keep the system as the same name, because even though we are adding the PlayerStateAndOffsetComponent to the entity the primary purpose of this component is to set the bullet offset
So the name still works (kind of)
Paste the code snippet below into SetBulletSpawnOffset.cs:
using Unity.Entities;
using Unity.Mathematics;
using UnityEngine;
public class SetBulletSpawnOffset : UnityEngine.MonoBehaviour, IConvertGameObjectToEntity
{
public GameObject bulletSpawn;
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
{
var bulletOffset = default(PlayerStateAndOffsetComponent);
var offsetVector = bulletSpawn.transform.position;
bulletOffset.Value = new float3(offsetVector.x, offsetVector.y, offsetVector.z);
dstManager.AddComponentData(entity, bulletOffset);
}
}
Now let's re-add our SetBulletSpawnOffset system onto our Player prefab and drag the Bullet Spawn GameObject into the bulletSpawn field
This really isn't necessary, but sometimes updating components that are on prefabs causes errors so better to be safe than sorry
Now we need to create the system that will be responding to Commands that have to do with spawning
Similar to InputResponseMovementSystem, which responds to inputs for movement, we need to create InputResponseSpawnSystem (which responds to inputs for spawning)
Create InputResponseSpawnSystem in the Mixed/Systems folder
Paste the code snippet below into InputResponseSpawnSystem.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;
//InputResponseSpawntSystem runs on both the Client and Server
//It is predicted on the client but "decided" on the server
[UpdateInWorld(TargetWorld.ClientAndServer)]
[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
[UpdateAfter(typeof(ExportPhysicsWorld))]
public partial class InputResponseSpawnSystem : SystemBase
{
//We will use the BeginSimulationEntityCommandBufferSystem for our structural changes
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
//This is a special NetCode group that provides a "prediction tick" and a fixed "DeltaTime"
private GhostPredictionSystemGroup m_PredictionGroup;
//This will save our Bullet prefab to be used to spawn bullets
private Entity m_BulletPrefab;
//We are going to use this for "weapon cooldown"
private const int k_CoolDownTicksCount = 5;
protected override void OnCreate()
{
//This will grab the BeginSimulationEntityCommandBuffer system to be used in OnUpdate
m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
//This will grab the BeginSimulationEntityCommandBuffer system to be used in OnUpdate
m_PredictionGroup = World.GetOrCreateSystem<GhostPredictionSystemGroup>();
//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>();
// Make sure we have the bullet prefab to be able to create the predicted spawning
RequireSingletonForUpdate<BulletAuthoringComponent>();
}
protected override void OnUpdate()
{
//Here we set the prefab we will use
if (m_BulletPrefab == Entity.Null)
{
//We grab the converted PrefabCollection Entity's BulletAuthoringComponent
//and set m_BulletPrefab to its Prefab value
var foundPrefab = GetSingleton<BulletAuthoringComponent>().Prefab;
m_BulletPrefab = GhostCollectionSystem.CreatePredictedSpawnPrefab(EntityManager, foundPrefab);
//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;
}
//We need a CommandBuffer because we will be making structural changes (creating bullet entities)
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer().AsParallelWriter();
//Must declare our local variables before the jobs in the .ForEach()
var bulletVelocity = GetSingleton<GameSettingsComponent>().bulletVelocity;
var bulletPrefab = m_BulletPrefab;
//These are special NetCode values needed to work the prediction system
var deltaTime = m_PredictionGroup.Time.DeltaTime;
var currentTick = m_PredictionGroup.PredictingTick;
//We will grab the buffer of player command from the palyer 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 PlayerStateAndOffsetComponent bulletOffset, in Rotation rotation, in Translation position, in PhysicsVelocity velocityComponent,
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;
//Here we add the destroy tag to the player if the self-destruct button was pressed
if (inputData.selfDestruct == 1)
{
commandBuffer.AddComponent<DestroyTag>(entityInQueryIndex, entity);
}
var canShoot = bulletOffset.WeaponCooldown == 0 || SequenceHelpers.IsNewer(currentTick, bulletOffset.WeaponCooldown);
if (inputData.shoot != 0 && canShoot)
{
// We create the bullet here
var bullet = commandBuffer.Instantiate(nativeThreadIndex, bulletPrefab);
//We declare it as a predicted spawning for player spawned objects by adding a special component
commandBuffer.AddComponent(entityInQueryIndex, bullet, new PredictedGhostSpawnRequestComponent());
//we set the bullets position as the player's position + the bullet spawn offset
//math.mul(rotation.Value,bulletOffset.Value) finds the position of the bullet offset in the given rotation
//think of it as finding the LocalToParent of the bullet offset (because the offset needs to be rotated in the players direction)
var newPosition = new Translation {Value = position.Value + math.mul(rotation.Value, bulletOffset.Value).xyz};
// bulletVelocity * math.mul(rotation.Value, new float3(0,0,1)).xyz) takes linear direction of where facing and multiplies by velocity
// adding to the players physics Velocity makes sure that it takes into account the already existing player velocity (so if shoot backwards while moving forwards it stays in place)
var vel = new PhysicsVelocity {Linear = (bulletVelocity * math.mul(rotation.Value, new float3(0,0,1)).xyz) + velocityComponent.Linear};
commandBuffer.SetComponent(entityInQueryIndex, bullet, newPosition);
commandBuffer.SetComponent(entityInQueryIndex, bullet, vel);
commandBuffer.SetComponent(entityInQueryIndex, bullet,
new GhostOwnerComponent {NetworkId = ghostOwner.NetworkId});
bulletOffset.WeaponCooldown = currentTick + k_CoolDownTicksCount;
}
}).ScheduleParallel();
//We must add our dependency to the CommandBuffer because we made structural changes
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
}
}
We identify the bullet as a Predicted spawning prefab by adding "PredictedGhostSpawnRequestComponent"
This allows us to identify it in the BulletGhostSpawnClassificationSystem
The bulletOffset.WeaponCooldown may seem confusing
It makes sure that the Player can only fire every 5 server ticks
If you want to increase or decrease the rate of fire update k_CoolDownTicksCount
Implement Predicted Spawning for player spawned objects
The spawn code needs to run on the client, in the client prediction system. The spawn should use the predicted client version of the ghost prefab and add a PredictedGhostSpawnRequestComponent to it. Then, when the first snapshot update for the entity arrives it will apply to that predict spawned object (no new entity is created). After this, the snapshot updates are applied the same as in the predicted spawning for client predicted player object model.
To create the prefab for predicted spawning, you should use the utility method GhostCollectionSystem.CreatePredictedSpawnPrefab.
You need to implement some specific code to handle the predicted spawning for player spawned objects. You need to create a system updating in the ClientSimulationSystemGroup after GhostSpawnClassificationSystem. The system needs to go through the GhostSpawnBuffer buffer stored on a singleton with a GhostSpawnQueueComponent. For each entry in that list it should compare to the entries in the PredictedGhostSpawn buffer on the singleton with a PredictedGhostSpawnList component. If the two entries are the same the classification system should set the PredictedSpawnEntity property in the GhostSpawnBuffer and remove the entry from GhostSpawnBuffer.
NetCode spawns entities on clients when there is a Prefab available for it. Pre spawned ghosts will work without any special consideration since they are referenced in a sub scene, but for manually spawned entities you must make sure that the prefabs exist on the client. You make sure that happens by having a component in a scene which references the prefab you want to spawn.
Now we must classify these predicted bullets with BulletGhostSpawnClassificationSystem
Create BulletGhostSpawnClassificationSystem in the Client/Systems folder
Paste the code snippet below into BulletGhostSpawnClassificationSystem.cs:
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.NetCode;
using Unity.Networking.Transport.Utilities;
//This system will only run on the client and within GhostSimulationSystemGroup
//and after GhostSpawnClassification system as is specified in the NetCode documentation
[UpdateInWorld(TargetWorld.Client)]
[UpdateInGroup(typeof(GhostSimulationSystemGroup))]
[UpdateAfter(typeof(GhostSpawnClassificationSystem))]
public partial class BulletGhostSpawnClassificationSystem : SystemBase
{
protected override void OnCreate()
{
//Both of these components are needed in the OnUpdate so we will wait until they exist to update
RequireSingletonForUpdate<GhostSpawnQueueComponent>();
RequireSingletonForUpdate<PredictedGhostSpawnList>();
}
protected override void OnUpdate()
{
//This is the NetCode recommended method to identify predicted spawning for player spawned objects
//More information can be found at: https://docs.unity3d.com/Packages/com.unity.netcode@0.5/manual/ghost-snapshots.html
//under "Entity spawning"
var spawnListEntity = GetSingletonEntity<PredictedGhostSpawnList>();
var spawnListFromEntity = GetBufferFromEntity<PredictedGhostSpawn>();
Dependency = Entities
.WithAll<GhostSpawnQueueComponent>()
.WithoutBurst()
.ForEach((DynamicBuffer<GhostSpawnBuffer> ghosts, DynamicBuffer<SnapshotDataBuffer> data) =>
{
var spawnList = spawnListFromEntity[spawnListEntity];
for (int i = 0; i < ghosts.Length; ++i)
{
var ghost = ghosts[i];
if (ghost.SpawnType == GhostSpawnBuffer.Type.Predicted)
{
for (int j = 0; j < spawnList.Length; ++j)
{
if (ghost.GhostType == spawnList[j].ghostType && !SequenceHelpers.IsNewer(spawnList[j].spawnTick, ghost.ServerSpawnTick + 5) && SequenceHelpers.IsNewer(spawnList[j].spawnTick + 5, ghost.ServerSpawnTick))
{
ghost.PredictedSpawnEntity = spawnList[j].entity;
spawnList[j] = spawnList[spawnList.Length-1];
spawnList.RemoveAt(spawnList.Length - 1);
break;
}
}
ghosts[i] = ghost;
}
}
}).Schedule(Dependency);
}
}
This code implements the steps described in the official Unity NetCode documentation
It's a bit weird to follow, but no need to sweat it; this is just what needs to be done with predicted spawning
If you have other predicted-spawning objects you want to include in your project (i.e. not bullets) just make sure to follow the same steps as in this system
Navigate to Multiplayer > PlayMode Tools and make sure "Num Thin Clients" is at 0 and save
Hit play, then hit spacebar to spawn bullets
Although we are able to spawn bullets at first, after a while errors appear, what gives?
This is because our client is destroying bullets in BulletAgeSystem
We know that only the server can make those kind of decisions, so we will fix that in the next section
Now for some housekeeping:
Move into Mixed/Components
BulletTag
BulletAgeComponent
BulletAuthoringComponent
PlayerStateAndOffsetComponent
Create a new folder in Scripts and Prefabs called "Authoring" (same level as Client/Mixed/Server/Multiplayer Setup)
Here we will start moving items that aren't necessary at runtime, just during authoring
Move SetBulletSpawnOffset into the "Authoring" folder
Move SetGameSettingsSystem into the "Authoring" folder
No gif here, we believe in you 💪
We can now predicted-spawn bullets
We updated our Bullet Prefab
We updated InputSystem and removed rate-limiting
We updated BulletOffsetComponent to PlayerStateAndOffsetComponent
Changed the added component in SetBulletSpawnOffset
Created InputResponseSpawnSystem
Created BulletGhostSpawnClassificationSystem
Updating destruction workflows
Bullet destruction
We will need to update our BulletAgeSystem to run only on the server
The server must be the authority on when entities are destroyed, not the client
Move BulletAgeSystem into the Server/Systems folder
Update BulletAgeSystem.cs by pasting in the code snippet below:
using Unity.Entities;
using Unity.NetCode;
//Only our server will decide when bullets die of old age
[UpdateInGroup(typeof(ServerSimulationSystemGroup))]
public partial class BulletAgeSystem : SystemBase
{
//We will be using the BeginSimulationEntityCommandBuffer to record our structural changes
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
//This is a special NetCode group that provides a "prediction tick" and a fixed "DeltaTime"
private GhostPredictionSystemGroup m_PredictionGroup;
protected override void OnCreate()
{
//Grab the CommandBuffer for structural changes
m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
//We will grab this system so we can use its "DeltaTime"
m_PredictionGroup = World.GetOrCreateSystem<GhostPredictionSystemGroup>();
}
protected override void OnUpdate()
{
//We create our CommandBuffer and add .AsParallelWriter() because we will be scheduling parallel jobs
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer().AsParallelWriter();
//We must declare local variables before using them in the job below
var deltaTime = m_PredictionGroup.Time.DeltaTime;
//Our query writes to the BulletAgeComponent
//The reason we don't need to add .WithAll<BulletTag>() here is because referencing the BulletAgeComponent
//requires the Entities to have a BulletAgeComponent and only Bullets have those
Entities.ForEach((Entity entity, int nativeThreadIndex, ref BulletAgeComponent age) =>
{
age.age += deltaTime;
if (age.age > age.maxAge)
commandBuffer.DestroyEntity(nativeThreadIndex, entity);
}).ScheduleParallel();
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
}
}
Player destruction
Currently we have PlayerDestructionSystem running on both the client and the server
Let's update this system so that it only runs on the server
We'll also set the NCE CommandTargetComponent's targetEntity back to being equal to null (how it was before we spawned a player). Think of this as a "clean up"
It needs to be set back to null because if not, the server will not respond to any more player spawn requests from that NCE
This is because the server checks if the NCE is null before it spawns a player in PlayerSpawnSystem (that is one of those 4 checks)
First move the PlayerDestructionSystem file to the Server/Systems folder
Paste the code snippet below into PlayerDestructionSystem.cs:
using Unity.Burst;
using Unity.Entities;
using Unity.Collections;
using Unity.Mathematics;
using Unity.Jobs;
using Unity.Transforms;
using UnityEngine;
using Unity.NetCode;
//We are going to update LATE once all other systems are complete
//because we don't want to destroy the Entity before other systems have
//had a chance to interact with it if they need to
[UpdateInWorld(TargetWorld.Server)]
[UpdateInGroup(typeof(LateSimulationSystemGroup))]
public partial class PlayerDestructionSystem : SystemBase
{
private EndSimulationEntityCommandBufferSystem m_EndSimEcb;
protected override void OnCreate()
{
//We grab the EndSimulationEntityCommandBufferSystem to record our structural changes
m_EndSimEcb = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
}
protected override void OnUpdate()
{
//We add "AsParallelWriter" when we create our command buffer because we want
//to run our jobs in parallel
var commandBuffer = m_EndSimEcb.CreateCommandBuffer().AsParallelWriter();
//We are going to need to update the NCE CommandTargetComponent so we set the argument to false (not read-only)
var commandTargetFromEntity = GetComponentDataFromEntity<CommandTargetComponent>(false);
//We now any entities with a DestroyTag and an PlayerTag
//We could just query for a DestroyTag, but we might want to run different processes
//if different entities are destroyed, so we made this one specifically for Players
//We query specifically for players because we need to clear the NCE when they are destroyed
//In order to write over a variable that we pass through to a job we must include "WithNativeDisableParallelForRestricion"
//It means "yes we know what we are doing, allow us to write over this variable"
Entities
.WithNativeDisableParallelForRestriction(commandTargetFromEntity)
.WithAll<DestroyTag, PlayerTag>()
.ForEach((Entity entity, int entityInQueryIndex, in PlayerEntityComponent playerEntity) =>
{
// Reset the CommandTargetComponent on the Network Connection Entity to the player
//We are able to find the NCE the player belongs to through the PlayerEntity component
var state = commandTargetFromEntity[playerEntity.PlayerEntity];
state.targetEntity = Entity.Null;
commandTargetFromEntity[playerEntity.PlayerEntity] = state;
//Then destroy the entity
commandBuffer.DestroyEntity(entityInQueryIndex, entity);
}).ScheduleParallel();
//We then add the dependencies of these jobs to the EndSimulationEntityCOmmandBufferSystem
//that will be playing back the structural changes recorded in this sytem
m_EndSimEcb.AddJobHandleForProducer(Dependency);
}
}
Hit play, shoot around, self-destruct, and re-spawn to check out the updates
You might notice that sometimes asteroids and players sometimes turn red, but do not get destroyed. What's up with that?? Doesn't changing the render mesh to red mean the bullet and the object collided?!
Remember that both the client and server are running ChangeMaterialAndDestroySystem
So at times a client might "predict" that a bullet collided with an object and change the render mesh of the collided object
But the server calculated that those two entities did not actually collide and so the server does not add the "DestroyTag" to the object, and therefore it does not get destroyed
The render mesh of the object is not ghosted
That means the value of the render mesh is not synchronized between the server and clients
So because the client predicted something would happen that did not actually happen, we are left with red objects that do not actually get destroyed
We could build a workflow that changes the render mesh back to the appropriate color if the server does not confirm the hit, but that's out of scope of this gitbook
If you want to do this yourself and you have a cool solution you're willing to share, please let us know in the Moetsi Discord
Now let's add 2 Thin Clients and make sure everything still functions with Thin Clients
Go to Multiplayer menu > PlayMode Tools > Type 2 in the Num Thin Clients field
Boy, do those guys zip around!
You will notice that based on the randomly-generated inputs in InputSystem the Thin Clients accelerate into their own bullets which causes them to get destroyed
This is actually useful for testing because they continue spawning as you continue to try getting shot and try shooting them. This helps ensure you that all workflows are functioning
Final housekeeping in this section:
Move ChangeMaterialAndDestroySystem to Mixed/Systems
Move StatefulCollisionEventBufferAuthoring to Authoring
Move StatefulTriggerEventBufferAuthoring to Authoring
Move StatefulTriggerEventBufferSystem to Mixed/Systems
Move StatefulCollisionEventBufferSystem to Mixed/Systems
Move to Mixed/Components
IStatefulSimulationEvent
StatefulCollisionEvent
StatefulSimulationEventBuffers
StatefulTriggerEvent
No gif here, we believe in you 💪
We now are able to destroy bullets and players in NetCode
We updated BulletAgeSystem to run on server
We updated PlayerDestructionSystem to run on the server