Use DOTS NetCode for Collisions and Destroying Bullet Prefabs
Code and workflows to spawn ghosted bullets and update to server-side destruction

What you'll develop on this page

Spawning ghosted bullets to destroy asteroids and players while interacting with Thin Clients
We will "turn" our bullet prefabs "into" ghosts and update the entity destruction flow so it is server-authoritative.

NetCode client-predicted model background

Entity spawning

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. The spawn code needs to run on the client, in the client prediction system. Spawn the predicted client version of the ghost prefab and add a PredictedGhostSpawnRequestComponent. 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.
We are now on the third type of spawning listed above, "Predicted spawning for player spawned objects". Asteroids were "Delayed or interpolated spawning", and players were "predicted spawning for the client predicted player object".
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
    Add a GhostOwnerComponent
Updating the Bullet prefab
    You will notice that we did not add a VelocityComponent to the Bullet prefab
      But I thought PhysicsVelocity wasn't ready yet in v0.6 NetCode + v0.6 DOTS Physics?!
      Yes, updating the PhysicsVelocity is broken, but setting it once on instantiation actually works (well enough)
        Similar to how we set the PhysicsVelocity on asteroids once and they work great, setting the PhysicsVelocity on bullets once works as well
        So we can make use of PhysicsVelocity on the bullet
        The reason we needed to use VelocityComponent on the Player prefab is because we are always updating the velocity of the player through the WASD keys
        This means there will be good collision prediction with our bullets and asteroids 💪

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:
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 will use this for simulating inputs with our ThinClients
16
private int m_FrameCount;
17
18
protected override void OnCreate()
19
{
20
21
//This will grab the BeginSimulationEntityCommandBuffer system to be used in OnUpdate
22
m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
23
24
//We set our ClientSimulationSystemGroup who will provide its ServerTick needed for the Commands
25
m_ClientSimulationSystemGroup = World.GetOrCreateSystem<ClientSimulationSystemGroup>();
26
27
//The client must have loaded the game to spawn a player so we wait for the
28
//NetworkStreamInGame component added during the load game flow
29
RequireSingletonForUpdate<NetworkStreamInGame>();
30
}
31
32
protected override void OnUpdate()
33
{
34
//Because players aren't spawned immediatly for Thin clients we still need a place to store the commands
35
bool isThinClient = HasSingleton<ThinClientComponent>();
36
if (HasSingleton<CommandTargetComponent>() && GetSingleton<CommandTargetComponent>().targetEntity == Entity.Null)
37
{
38
if (isThinClient)
39
{
40
// No ghosts are spawned, so create a placeholder struct to store the commands in
41
var ent = EntityManager.CreateEntity();
42
EntityManager.AddBuffer<PlayerCommand>(ent);
43
SetSingleton(new CommandTargetComponent{targetEntity = ent});
44
}
45
}
46
47
//We now have all our inputs
48
byte right, left, thrust, reverseThrust, selfDestruct, shoot;
49
right = left = thrust = reverseThrust = selfDestruct = shoot = 0;
50
51
//for looking around with mouse
52
float mouseX = 0;
53
float mouseY = 0;
54
55
//We are adding this difference so we can use "Num Thin Client" in "Multiplayer Mode Tools"
56
//These are the instructions if we are NOT a thin client
57
if (!isThinClient)
58
{
59
if (Input.GetKey("d"))
60
{
61
right = 1;
62
}
63
if (Input.GetKey("a"))
64
{
65
left = 1;
66
}
67
if (Input.GetKey("w"))
68
{
69
thrust = 1;
70
}
71
if (Input.GetKey("s"))
72
{
73
reverseThrust = 1;
74
}
75
if (Input.GetKey("p"))
76
{
77
selfDestruct = 1;
78
}
79
if (Input.GetKey("space"))
80
{
81
shoot = 1;
82
}
83
if (Input.GetMouseButton(1))
84
{
85
mouseX = Input.GetAxis("Mouse X");
86
mouseY = Input.GetAxis("Mouse Y");
87
88
}
89
}
90
91
//Here are the instructions if we ARE a thin client
92
else
93
{
94
// Spawn and generate some random inputs
95
var state = (int) Time.ElapsedTime % 3;
96
shoot = 1;
97
if (state == 0)
98
left = 1;
99
else
100
thrust = 1;
101
++m_FrameCount;
102
if (m_FrameCount % 100 == 0)
103
{
104
shoot = 1;
105
m_FrameCount = 0;
106
}
107
}
108
109
//Must declare local variables before using them in the .ForEach()
110
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer().AsParallelWriter();
111
//We grab the PlayerCommand buffer from the player so we can add to it
112
var inputFromEntity = GetBufferFromEntity<PlayerCommand>();
113
//We are sending the simulationsystemgroup tick so the server can playback our commands appropriately
114
var inputTargetTick = m_ClientSimulationSystemGroup.ServerTick;
115
116
//We query for the NCE by checking for entities with a NetworkIdComponent
117
//We also do not want to send an RPC if the NCE has become disconnected
118
Entities
119
.WithAll<NetworkIdComponent>()
120
.WithNone<NetworkStreamDisconnected>()
121
.ForEach((Entity entity, int nativeThreadIndex, in CommandTargetComponent commandTargetComponent) =>
122
{
123
// Special handling for thin clients since we can't tell if the ship is spawned or not
124
//This code only runs for thin clients
125
if (isThinClient && shoot != 0)
126
{
127
var req = commandBuffer.CreateEntity(nativeThreadIndex);
128
commandBuffer.AddComponent<PlayerSpawnRequestRpc>(nativeThreadIndex, req);
129
commandBuffer.AddComponent(nativeThreadIndex, req, new SendRpcCommandRequestComponent {TargetConnection = entity});
130
}
131
132
//If the targetEntity is null we want to spawn our player instead of add Command data to our buffer
133
if (commandTargetComponent.targetEntity == Entity.Null)
134
{
135
if (shoot != 0)
136
{
137
var req = commandBuffer.CreateEntity(nativeThreadIndex);
138
commandBuffer.AddComponent<PlayerSpawnRequestRpc>(nativeThreadIndex, req);
139
commandBuffer.AddComponent(nativeThreadIndex, req, new SendRpcCommandRequestComponent {TargetConnection = entity});
140
}
141
}
142
//Here we save our input data into a Command which will get automatically sent off to the server by NetCode
143
else
144
{
145
// If player, store commands in network command buffer
146
if (inputFromEntity.HasComponent(commandTargetComponent.targetEntity))
147
{
148
//Here we grab the CommandBuffer from our player entity
149
var input = inputFromEntity[commandTargetComponent.targetEntity];
150
151
//Notice that we are sending the ClientSimulationSystemGroup's server tick as part of the Command
152
//This is necessary for NetCode to function properly
153
input.AddCommandData(new PlayerCommand{Tick = inputTargetTick, left = left, right = right, thrust = thrust, reverseThrust = reverseThrust,
154
selfDestruct = selfDestruct, shoot = shoot,
155
mouseX = mouseX,
156
mouseY = mouseY});
157
}
158
}
159
}).Schedule();
160
161
//We need to add the jobs dependency to the command buffer
162
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
163
}
164
}
Copied!
Updating InputSystem so that shooting rate-limiting happens on the server
    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:
1
using Unity.Entities;
2
using Unity.Mathematics;
3
using Unity.NetCode;
4
5
public struct PlayerStateAndOffsetComponent : IComponentData
6
{
7
public float3 Value;
8
[GhostField]
9
public int State;
10
public uint WeaponCooldown;
11
12
}
Copied!
Updating BulletOffsetComponent to PlayerStateAndOffsetComponent (better to do file renaming in Unity or else you will get a .meta warning)
    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:
1
using Unity.Entities;
2
using Unity.Mathematics;
3
using UnityEngine;
4
5
public class SetBulletSpawnOffset : UnityEngine.MonoBehaviour, IConvertGameObjectToEntity
6
{
7
public GameObject bulletSpawn;
8
9
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
10
{
11
var bulletOffset = default(PlayerStateAndOffsetComponent);
12
13
var offsetVector = bulletSpawn.transform.position;
14
bulletOffset.Value = new float3(offsetVector.x, offsetVector.y, offsetVector.z);
15
16
dstManager.AddComponentData(entity, bulletOffset);
17
}
18
}
19
Copied!
Updating SetBulletSpawnOffset to use PlayerStateAndOffsetComponent
    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
Removing and re-adding SetBulletOffset on the Player prefab (to be safe)
    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:
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 InputResponseSpawnSystem : SystemBase
15
{
16
//We will use the BeginSimulationEntityCommandBufferSystem for our structural changes
17
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
18
19
//This is a special NetCode group that provides a "prediction tick" and a fixed "DeltaTime"
20
private GhostPredictionSystemGroup m_PredictionGroup;
21
22
//This will save our Bullet prefab to be used to spawn bullets
23
private Entity m_BulletPrefab;
24
25
//We are going to use this for "weapon cooldown"
26
private const int k_CoolDownTicksCount = 5;
27
28
29
protected override void OnCreate()
30
{
31
//This will grab the BeginSimulationEntityCommandBuffer system to be used in OnUpdate
32
m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
33
34
//This will grab the BeginSimulationEntityCommandBuffer system to be used in OnUpdate
35
m_PredictionGroup = World.GetOrCreateSystem<GhostPredictionSystemGroup>();
36
37
//We check to ensure GameSettingsComponent exists to know if the SubScene has been streamed in
38
//We need the SubScene for actions in our OnUpdate()
39
RequireSingletonForUpdate<GameSettingsComponent>();
40
}
41
42
protected override void OnUpdate()
43
{
44
45
//Here we set the prefab we will use
46
if (m_BulletPrefab == Entity.Null)
47
{
48
//We grab the converted PrefabCollection Entity's BulletAuthoringComponent
49
//and set m_BulletPrefab to its Prefab value
50
m_BulletPrefab = GetSingleton<BulletAuthoringComponent>().Prefab;
51
//we must "return" after setting this prefab because if we were to continue into the Job
52
//we would run into errors because the variable was JUST set (ECS funny business)
53
//comment out return and see the error
54
return;
55
}
56
57
//We need a CommandBuffer because we will be making structural changes (creating bullet entities)
58
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer().AsParallelWriter();
59
60
//Must declare our local variables before the jobs in the .ForEach()
61
var bulletVelocity = GetSingleton<GameSettingsComponent>().bulletVelocity;
62
var bulletPrefab = m_BulletPrefab;
63
//These are special NetCode values needed to work the prediction system
64
var deltaTime = m_PredictionGroup.Time.DeltaTime;
65
var currentTick = m_PredictionGroup.PredictingTick;
66
67
//We will grab the buffer of player command from the palyer entity
68
var inputFromEntity = GetBufferFromEntity<PlayerCommand>(true);
69
70
//We are looking for player entities that have PlayerCommands in their buffer
71
Entities
72
.WithReadOnly(inputFromEntity)
73
.WithAll<PlayerTag, PlayerCommand>()
74
.ForEach((Entity entity, int nativeThreadIndex, ref PlayerStateAndOffsetComponent bulletOffset, in Rotation rotation, in Translation position, in VelocityComponent velocityComponent,
75
in GhostOwnerComponent ghostOwner, in PredictedGhostComponent prediction) =>
76
{
77
//Here we check if we SHOULD do the prediction based on the tick, if we shouldn't, we return
78
if (!GhostPredictionSystemGroup.ShouldPredict(currentTick, prediction))
79
return;
80
81
//We grab the buffer of commands from the player entity
82
var input = inputFromEntity[entity];
83
84
//We then grab the Command from the current tick (which is the PredictingTick)
85
//if we cannot get it at the current tick we make sure shoot is 0
86
//This is where we will store the current tick data
87
PlayerCommand inputData;
88
if (!input.GetDataAtTick(currentTick, out inputData))
89
inputData.shoot = 0;
90
91
//Here we add the destroy tag to the player if the self-destruct button was pressed
92
if (inputData.selfDestruct == 1)
93
{
94
commandBuffer.AddComponent<DestroyTag>(nativeThreadIndex, entity);
95
}
96
97
var canShoot = bulletOffset.WeaponCooldown == 0 || SequenceHelpers.IsNewer(currentTick, bulletOffset.WeaponCooldown);
98
if (inputData.shoot != 0 && canShoot)
99
{
100
// We create the bullet here
101
var bullet = commandBuffer.Instantiate(nativeThreadIndex, bulletPrefab);
102
//We declare it as a predicted spawning for player spawned objects by adding a special component
103
commandBuffer.AddComponent(nativeThreadIndex, bullet, new PredictedGhostSpawnRequestComponent());
104
105
106
//we set the bullets position as the player's position + the bullet spawn offset
107
//math.mul(rotation.Value,bulletOffset.Value) finds the position of the bullet offset in the given rotation
108
//think of it as finding the LocalToParent of the bullet offset (because the offset needs to be rotated in the players direction)
109
var newPosition = new Translation {Value = position.Value + math.mul(rotation.Value, bulletOffset.Value).xyz};
110
111
// bulletVelocity * math.mul(rotation.Value, new float3(0,0,1)).xyz) takes linear direction of where facing and multiplies by velocity
112
// 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)
113
var vel = new PhysicsVelocity {Linear = (bulletVelocity * math.mul(rotation.Value, new float3(0,0,1)).xyz) + velocityComponent.Linear};
114
115
commandBuffer.SetComponent(nativeThreadIndex, bullet, newPosition);
116
commandBuffer.SetComponent(nativeThreadIndex, bullet, vel);
117
commandBuffer.SetComponent(nativeThreadIndex, bullet,
118
new GhostOwnerComponent {NetworkId = ghostOwner.NetworkId});
119
120
121
bulletOffset.WeaponCooldown = currentTick + k_CoolDownTicksCount;
122
}
123
124
}).ScheduleParallel();
125
126
//We must add our dependency to the CommandBuffer because we made structural changes
127
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
128
}
129
}
Copied!
    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

Entity spawning

...
    Predicted spawning for player spawned objects. These are objects that the player input spawns, like in-game bullets or rockets that the player fires. The spawn code needs to run on the client, in the client prediction system. Spawn the predicted client version of the ghost prefab and add a PredictedGhostSpawnRequestComponent. 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.
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.
    Now we must classify these predicted bullets with BulletGhostSpawnClassificationSystem
    Create BulletGhostSpawnClassificationSystem in the Client/Systems folder
    Paste the code snippet below into BulletGhostSpawnClassificationSystem.cs:
1
using Unity.Burst;
2
using Unity.Collections;
3
using Unity.Entities;
4
using Unity.Jobs;
5
using Unity.Mathematics;
6
using Unity.NetCode;
7
using Unity.Networking.Transport.Utilities;
8
9
//This system will only run on the client and within GhostSimulationSystemGroup
10
//and after GhostSpawnClassification system as is specified in the NetCode documentation
11
[UpdateInWorld(UpdateInWorld.TargetWorld.Client)]
12
[UpdateInGroup(typeof(GhostSimulationSystemGroup))]
13
[UpdateAfter(typeof(GhostSpawnClassificationSystem))]
14
public class BulletGhostSpawnClassificationSystem : SystemBase
15
{
16
protected override void OnCreate()
17
{
18
//Both of these components are needed in the OnUpdate so we will wait until they exist to update
19
RequireSingletonForUpdate<GhostSpawnQueueComponent>();
20
RequireSingletonForUpdate<PredictedGhostSpawnList>();
21
}
22
protected override void OnUpdate()
23
{
24
//This is the NetCode recommended method to identify predicted spawning for player spawned objects
25
//More information can be found at: https://docs.unity3d.com/Packages/[email protected]/manual/ghost-snapshots.html
26
//under "Entity spawning"
27
var spawnListEntity = GetSingletonEntity<PredictedGhostSpawnList>();
28
var spawnListFromEntity = GetBufferFromEntity<PredictedGhostSpawn>();
29
Dependency = Entities
30
.WithAll<GhostSpawnQueueComponent>()
31
.WithoutBurst()
32
.ForEach((DynamicBuffer<GhostSpawnBuffer> ghosts, DynamicBuffer<SnapshotDataBuffer> data) =>
33
{
34
var spawnList = spawnListFromEntity[spawnListEntity];
35
for (int i = 0; i < ghosts.Length; ++i)
36
{
37
var ghost = ghosts[i];
38
if (ghost.SpawnType == GhostSpawnBuffer.Type.Predicted)
39
{
40
for (int j = 0; j < spawnList.Length; ++j)
41
{
42
if (ghost.GhostType == spawnList[j].ghostType && !SequenceHelpers.IsNewer(spawnList[j].spawnTick, ghost.ServerSpawnTick + 5) && SequenceHelpers.IsNewer(spawnList[j].spawnTick + 5, ghost.ServerSpawnTick))
43
{
44
ghost.PredictedSpawnEntity = spawnList[j].entity;
45
spawnList[j] = spawnList[spawnList.Length-1];
46
spawnList.RemoveAt(spawnList.Length - 1);
47
break;
48
}
49
}
50
ghosts[i] = ghost;
51
}
52
}
53
}).Schedule(Dependency);
54
}
55
}
56
Copied!
    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
Predicted-spawning bullets and receiving errors
    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:
1
using Unity.Entities;
2
using Unity.NetCode;
3
4
//Only our server will decide when bullets die of old age
5
[UpdateInGroup(typeof(ServerSimulationSystemGroup))]
6
public class BulletAgeSystem : SystemBase
7
{
8
//We will be using the BeginSimulationEntityCommandBuffer to record our structural changes
9
private BeginSimulationEntityCommandBufferSystem m_BeginSimEcb;
10
11
//This is a special NetCode group that provides a "prediction tick" and a fixed "DeltaTime"
12
private GhostPredictionSystemGroup m_PredictionGroup;
13
14
protected override void OnCreate()
15
{
16
//Grab the CommandBuffer for structural changes
17
m_BeginSimEcb = World.GetOrCreateSystem<BeginSimulationEntityCommandBufferSystem>();
18
19
//We will grab this system so we can use its "DeltaTime"
20
m_PredictionGroup = World.GetOrCreateSystem<GhostPredictionSystemGroup>();
21
}
22
23
protected override void OnUpdate()
24
{
25
//We create our CommandBuffer and add .AsParallelWriter() because we will be scheduling parallel jobs
26
var commandBuffer = m_BeginSimEcb.CreateCommandBuffer().AsParallelWriter();
27
28
//We must declare local variables before using them in the job below
29
var deltaTime = m_PredictionGroup.Time.DeltaTime;
30
31
//Our query writes to the BulletAgeComponent
32
//The reason we don't need to add .WithAll<BulletTag>() here is because referencing the BulletAgeComponent
33
//requires the Entities to have a BulletAgeComponent and only Bullets have those
34
Entities.ForEach((Entity entity, int nativeThreadIndex, ref BulletAgeComponent age) =>
35
{
36
age.age += deltaTime;
37
if (age.age > age.maxAge)
38
commandBuffer.DestroyEntity(nativeThreadIndex, entity);
39
40
}).ScheduleParallel();
41
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
42
}
43
}
Copied!
Moving BulletAgeSystem to the Server/Systems folder and then updating the code

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
      First move the PlayerDestructionSystem file to the Server/Systems folder
    Paste the code snippet below into PlayerDestructionSystem.cs:
1
using Unity.Burst;
2
using Unity.Entities;
3
using Unity.Collections;
4
using Unity.Mathematics;
5
using Unity.Jobs;
6
using Unity.Transforms;
7
using UnityEngine;
8
using Unity.NetCode;
9
10
//We are going to update LATE once all other systems are complete
11
//because we don't want to destroy the Entity before other systems have
12
//had a chance to interact with it if they need to
13
[UpdateInWorld(UpdateInWorld.TargetWorld.Server)]
14
[UpdateInGroup(typeof(LateSimulationSystemGroup))]
15
public class PlayerDestructionSystem : SystemBase
16
{
17
private EndSimulationEntityCommandBufferSystem m_EndSimEcb;
18
19
protected override void OnCreate()
20
{
21
//We grab the EndSimulationEntityCommandBufferSystem to record our structural changes
22
m_EndSimEcb = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
23
}
24
25
protected override void OnUpdate()
26
{
27
//We add "AsParallelWriter" when we create our command buffer because we want
28
//to run our jobs in parallel
29
var commandBuffer = m_EndSimEcb.CreateCommandBuffer().AsParallelWriter();
30
31
//We are going to need to update the NCE CommandTargetComponent so we set the argument to false (not read-only)
32
var commandTargetFromEntity = GetComponentDataFromEntity<CommandTargetComponent>(false);
33
34
//We now any entities with a DestroyTag and an PlayerTag
35
//We could just query for a DestroyTag, but we might want to run different processes
36
//if different entities are destroyed, so we made this one specifically for Players
37
//We query specifically for players because we need to clear the NCE when they are destroyed
38
//In order to write over a variable that we pass through to a job we must include "WithNativeDisableParallelForRestricion"
39
//It means "yes we know what we are doing, allow us to write over this variable"
40
Entities
41
.WithNativeDisableParallelForRestriction(commandTargetFromEntity)
42
.WithAll<DestroyTag, PlayerTag>()
43
.ForEach((Entity entity, int nativeThreadIndex, in PlayerEntityComponent playerEntity) =>
44
{
45
// Reset the CommandTargetComponent on the Network Connection Entity to the player
46
//We are able to find the NCE the player belongs to through the PlayerEntity component
47
var state = commandTargetFromEntity[playerEntity.PlayerEntity];
48
state.targetEntity = Entity.Null;
49
commandTargetFromEntity[playerEntity.PlayerEntity] = state;
50
51
//Then destroy the entity
52
commandBuffer.DestroyEntity(nativeThreadIndex, entity);
53
54
}).ScheduleParallel();
55
56
//We then add the dependencies of these jobs to the EndSimulationEntityCOmmandBufferSystem
57
//that will be playing back the structural changes recorded in this sytem
58
m_EndSimEcb.AddJobHandleForProducer(Dependency);
59
60
}
61
}
62
Copied!
Updating PlayerDestructionSystem and moving it to the Server/Systems folder
    Now that we have updated our systems so that the player is destroyed on the server, we need to think about what updates we need to make when the player is destroyed on the client
    If you remember back in "Updating Players," our trigger to send a PlayerSpawnRequestRpc when hitting spacebar (instead of sending shoot == 1 in a PlayerCommand) was whether the NCE CommandTargetComponent's targetEntity field was equal to null
    If we don't reset targetEntity back to null on the client, we will not pass this conditional, so another PlayerSpawnRequestRpc will not be sent
      So let's update PlayerGhostSpawnClassificationSystem
        Paste the code snippet below into PlayerGhostSpawnClassificationSystem.cs:
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>(); //thank you Lrakulka!
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
Entities
57
.WithAll<PlayerTag, PredictedGhostComponent>()
58
.WithNone<GhostPlayerState>()
59
.WithNativeDisableParallelForRestriction(commandTargetFromEntity)
60
.ForEach((Entity entity, int entityInQueryIndex) =>
61
{
62
//Here is where we update the NCE's CommandTargetComponent targetEntity to point at our player entity
63
var state = commandTargetFromEntity[playerEntity];
64
state.targetEntity = entity;
65
commandTargetFromEntity[playerEntity] = state;
66
67
//Here we add a special "ISystemStateComponentData" component
68
//This component does NOT get deleted with the rest of the entity
69
//Unity provided this special type of component to provide us a way to do "clean up" when important entities are destroyed
70
//In our case we will use GhostPlayerState to know when our player entity has been destroyed and will
71
//allow us to set our NCE CommandTargetComponent's targetEntity field back to null
72
//It also helps us ensure that we only set up our player entity once because we have a
73
//.WithNone<GhostPlayerState>() on our entity query
74
commandBuffer.AddComponent(entityInQueryIndex, entity, new GhostPlayerState());
75
76
//This creates our camera
77
var cameraEntity = commandBuffer.Instantiate(entityInQueryIndex, camera);
78
//This is how you "attach" a prefab entity to another
79
commandBuffer.AddComponent(entityInQueryIndex, cameraEntity, new Parent { Value = entity });
80
commandBuffer.AddComponent(entityInQueryIndex, cameraEntity, new LocalToParent() );
81
82
}).ScheduleParallel();
83
84
//This .ForEach() looks for Entities with a GhostPlayerState but no Snapshot data
85
//Because GhostPlayerState is an ISystemStateComponentData component, it does not get deleted with the rest of the entity
86
//It must be manually deleted
87
//By using GhostPlayerState we are able to "clean up" on the client side and clear our targetEntity in our NCE's CommandTargetComponent
88
//This allows us to "reset" and create a PlayerSpawnRequestRpc again when we hit spacebar (targetEntity must equal null to trigger the RPC)
89
Entities.WithNone<SnapshotData>()
90
.WithAll<GhostPlayerState>()
91
.WithNativeDisableParallelForRestriction(commandTargetFromEntity)
92
.ForEach((Entity ent, int entityInQueryIndex) =>
93
{
94
var commandTarget = commandTargetFromEntity[playerEntity];
95
96
if (ent == commandTarget.targetEntity)
97
{
98
commandTarget.targetEntity = Entity.Null;
99
commandTargetFromEntity[playerEntity] = commandTarget;
100
}
101
commandBuffer.RemoveComponent<GhostPlayerState>(entityInQueryIndex, ent);
102
}).ScheduleParallel();
103
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
104
105
106
m_BeginSimEcb.AddJobHandleForProducer(Dependency);
107
}
108
}
109
Copied!

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.
    We are using a ISystemStateComponent on the client as a callback for when our player entity has been destroyed
      ISystemStateComponent components do not get destroyed along with the rest of the entity when the entity is destroyed
      Unity ECS created these component types for exactly a use case like ours, where clean-up needs to occur when an entity is destroyed
      We can check for ISystemStateComponents with no Snapshot data, which will indicate to us which ghosted entities were destroyed
    Hit play, shoot around, self-destruct, and re-spawn to check out the updates
Hitting play and checking out destruction workflow 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
Adding 2 Thin Clients then hitting play
    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
    Now time for some housekeeping:
    First we are going to split DynamicBufferTriggerEventAuthoring into 2 files
      Instructions for how to do this are as follows (if you get lost, fear not, we provide the code snippets for each of the 2 files down below):
        First, open up DynamicBufferTriggerEventAuthoring.cs and Cut everything out between the namespace line and the IConvertGameObjectToEntity interface at the bottom of the file so that it only contains the IConvertGameObjectToEntity interface
        Make sure you're holding all that code copied on your Clipboard!
        Next, create a new file in the"Scripts and Prefabs" folder named TriggerConversionToStateful
          In this file, paste all that code you Cut from the old file and have held on your Clipboard
          Be sure to include all the "using _____" lines at the top from DynamicBufferTriggerEventAuthoring
Updating DynamicBufferTriggerEventAuthoring to be 2 files
    This results in 2 files:
    1. DynamicBufferTriggerEventAuthoring.cs:
1
using Unity.Entities;
2
using Unity.Jobs;
3
using Unity.Physics;
4
using Unity.Physics.Systems;
5
using UnityEngine;
6
using Unity.Collections;
7
using Unity.Burst;
8
using System;
9
using Unity.Assertions;
10
using Unity.Mathematics;
11
12
namespace Unity.Physics.Stateful
13
{
14
public class DynamicBufferTriggerEventAuthoring : MonoBehaviour, IConvertGameObjectToEntity
15
{
16
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
17
{
18
dstManager.AddBuffer<StatefulTriggerEvent>(entity);
19
}
20
}
21
}
Copied!
    2. TriggerConversionToStateful.cs:
1
using Unity.Entities;
2
using Unity.Jobs;
3
using Unity.Physics;
4
using Unity.Physics.Systems;
5
using UnityEngine;
6
using Unity.Collections;
7
using Unity.Burst;
8
using System;
9
using Unity.Assertions;
10
using Unity.Mathematics;
11
12
namespace Unity.Physics.Stateful
13
{
14
// Describes the overlap state.
15
// OverlapState in StatefulTriggerEvent is set to:
16
// 1) EventOverlapState.Enter, when 2 bodies are overlapping in the current frame,
17
// but they did not overlap in the previous frame
18
// 2) EventOverlapState.Stay, when 2 bodies are overlapping in the current frame,
19
// and they did overlap in the previous frame
20
// 3) EventOverlapState.Exit, when 2 bodies are NOT overlapping in the current frame,
21
// but they did overlap in the previous frame
22
public enum EventOverlapState : byte
23
{
24
Enter,
25
Stay,
26
Exit
27
}
28
29
// Trigger Event that is stored inside a DynamicBuffer
30
public struct StatefulTriggerEvent : IBufferElementData, IComparable<StatefulTriggerEvent>
31
{
32
internal EntityPair Entities;
33
internal BodyIndexPair BodyIndices;
34
internal ColliderKeyPair ColliderKeys;
35
36
public EventOverlapState State;
37
public Entity EntityA => Entities.EntityA;
38
public Entity EntityB => Entities.EntityB;
39
public int BodyIndexA => BodyIndices.BodyIndexA;
40
public int BodyIndexB => BodyIndices.BodyIndexB;
41
public ColliderKey ColliderKeyA => ColliderKeys.ColliderKeyA;
42
public ColliderKey ColliderKeyB => ColliderKeys.ColliderKeyB;
43
44
public StatefulTriggerEvent(Entity entityA, Entity entityB, int bodyIndexA, int bodyIndexB,
45
ColliderKey colliderKeyA, ColliderKey colliderKeyB)
46
{
47
Entities = new EntityPair
48
{
49
EntityA = entityA,
50
EntityB = entityB
51
};
52
BodyIndices = new BodyIndexPair
53
{
54
BodyIndexA = bodyIndexA,
55
BodyIndexB = bodyIndexB
56
};
57
ColliderKeys = new ColliderKeyPair
58
{
59
ColliderKeyA = colliderKeyA,
60
ColliderKeyB = colliderKeyB
61
};
62
State = default;
63
}
64
65
// Returns other entity in EntityPair, if provided with one
66
public Entity GetOtherEntity(Entity entity)
67
{
68
Assert.IsTrue((entity == EntityA) || (entity == EntityB));
69
int2 indexAndVersion = math.select(new int2(EntityB.Index, EntityB.Version),
70
new int2(EntityA.Index, EntityA.Version), entity == EntityB);
71
return new Entity
72
{
73
Index = indexAndVersion[0],
74
Version = indexAndVersion[1]
75
};
76
}
77
78
public int CompareTo(StatefulTriggerEvent other)
79
{
80
var cmpResult = EntityA.CompareTo(other.EntityA);
81
if (cmpResult != 0)
82
{
83
return cmpResult;
84
}
85
86
cmpResult = EntityB.CompareTo(other.EntityB);
87
if (cmpResult != 0)
88
{
89
return cmpResult;
90
}
91
92
if (ColliderKeyA.Value != other.ColliderKeyA.Value)
93
{
94
return ColliderKeyA.Value < other.ColliderKeyA.Value ? -1 : 1;
95
}
96
97
if (ColliderKeyB.Value != other.ColliderKeyB.Value)
98
{
99
return ColliderKeyB.Value < other.ColliderKeyB.Value ? -1 : 1;
100
}
101
102
return 0;
103
}
104
}
105
106
// If this component is added to an entity, trigger events won't be added to dynamic buffer
107
// of that entity by TriggerEventConversionSystem. This component is by default added to
108
// CharacterController entity, so that CharacterControllerSystem can add trigger events to
109
// CharacterController on its own, without TriggerEventConversionSystem interference.
110
public struct ExcludeFromTriggerEventConversion : IComponentData {}
111
112
// This system converts stream of TriggerEvents to StatefulTriggerEvents that are stored in a Dynamic Buffer.
113
// In order for TriggerEvents to be transformed to StatefulTriggerEvents and stored in a Dynamic Buffer, it is required to:
114
// 1) Tick IsTrigger on PhysicsShapeAuthoring on the entity that should raise trigger events
115
// 2) Add a DynamicBufferTriggerEventAuthoring component to that entity
116
// 3) If this is desired on a Character Controller, tick RaiseTriggerEvents on CharacterControllerAuthoring (skip 1) and 2)),
117
// note that Character Controller will not become a trigger, it will raise events when overlapping with one
118
[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
119
[UpdateAfter(typeof(StepPhysicsWorld))]
120
[UpdateBefore(typeof(EndFramePhysicsSystem))]
121
public class TriggerEventConversionSystem : SystemBase
122
{
123
public JobHandle OutDependency => Dependency;
124
125
private StepPhysicsWorld m_StepPhysicsWorld = default;
126
private BuildPhysicsWorld m_BuildPhysicsWorld = default;
127
private EndFramePhysicsSystem m_EndFramePhysicsSystem = default;
128
private EntityQuery m_Query = default;
129
130
private NativeList<StatefulTriggerEvent> m_PreviousFrameTriggerEvents;
131
private NativeList<StatefulTriggerEvent> m_CurrentFrameTriggerEvents;
132
133
protected override void OnCreate()
134
{
135
m_StepPhysicsWorld = World.GetOrCreateSystem<StepPhysicsWorld>();
136
m_BuildPhysicsWorld = World.GetOrCreateSystem<BuildPhysicsWorld>();
137
m_EndFramePhysicsSystem = World.GetOrCreateSystem<EndFramePhysicsSystem>();
138
m_Query = GetEntityQuery(new EntityQueryDesc
139
{
140
All = new ComponentType[]
141
{
142
typeof(StatefulTriggerEvent)
143
},
144
None = new ComponentType[]
145
{
146
typeof(ExcludeFromTriggerEventConversion)
147
}
148
});
149
150
m_PreviousFrameTriggerEvents = new NativeList<StatefulTriggerEvent>(Allocator.Persistent);
151
m_CurrentFrameTriggerEvents = new NativeList<StatefulTriggerEvent>(Allocator.Persistent);
152
}
153
154
protected override void OnDestroy()
155
{
156
m_PreviousFrameTriggerEvents.Dispose();
157
m_CurrentFrameTriggerEvents.Dispose();
158
}
159
160
protected void SwapTriggerEventStates()
161
{
162
var tmp = m_PreviousFrameTriggerEvents;
163
m_PreviousFrameTriggerEvents = m_CurrentFrameTriggerEvents;
164
m_CurrentFrameTriggerEvents = tmp;
165
m_CurrentFrameTriggerEvents.Clear();
166
}
167
168
protected static void AddTriggerEventsToDynamicBuffers(NativeList<StatefulTriggerEvent> triggerEventList,
169
ref BufferFromEntity<StatefulTriggerEvent> bufferFromEntity, NativeHashMap<Entity, byte> entitiesWithTriggerBuffers)
170
{
171
for (int i = 0; i < triggerEventList.Length; i++)
172
{
173
var triggerEvent = triggerEventList[i];
174
if (entitiesWithTriggerBuffers.ContainsKey(triggerEvent.EntityA))
175
{
176
bufferFromEntity[triggerEvent.EntityA].Add(triggerEvent);
177
}
178
if (entitiesWithTriggerBuffers.ContainsKey(triggerEvent.EntityB))
179
{
180
bufferFromEntity[triggerEvent.EntityB].Add(triggerEvent);
181
}
182
}
183
}
184
185
public static void UpdateTriggerEventState(NativeList<StatefulTriggerEvent> previousFrameTriggerEvents, NativeList<StatefulTriggerEvent> currentFrameTriggerEvents,
186
NativeList<StatefulTriggerEvent> resultList)
187
{
188
int i = 0;
189
int j = 0;
190
191
while (i < currentFrameTriggerEvents.Length && j < previousFrameTriggerEvents.Length)
192
{
193
var currentFrameTriggerEvent = currentFrameTriggerEvents[i];
194
var previousFrameTriggerEvent = previousFrameTriggerEvents[j];
195
196
int cmpResult = currentFrameTriggerEvent.CompareTo(previousFrameTriggerEvent);
197
198
// Appears in previous, and current frame, mark it as Stay
199
if (cmpResult == 0)
200
{
201
currentFrameTriggerEvent.State = EventOverlapState.Stay;
202
resultList.Add(currentFrameTriggerEvent);
203
i++;
204
j++;
205
}
206
else if (cmpResult < 0)
207
{
208
// Appears in current, but not in previous, mark it as Enter
209
currentFrameTriggerEvent.State = EventOverlapState.Enter;
210
resultList.Add(currentFrameTriggerEvent);
211
i++;
212
}
213
else
214
{
215
// Appears in previous, but not in current, mark it as Exit
216
previousFrameTriggerEvent.State = EventOverlapState.Exit;
217
resultList.Add(previousFrameTriggerEvent);
218
j++;
219
}
220
}
221
222
if (i == currentFrameTriggerEvents.Length)
223
{
224
while (j < previousFrameTriggerEvents.Length)
225
{
226
var triggerEvent = previousFrameTriggerEvents[j++];
227
triggerEvent.State = EventOverlapState.Exit;
228
resultList.Add(triggerEvent);
229
}
230
}
231
else if (j == previousFrameTriggerEvents.Length)
232
{
233
while (i < currentFrameTriggerEvents.Length)
234
{
235
var triggerEvent = currentFrameTriggerEvents[i++];
236
triggerEvent.State = EventOverlapState.Enter;
237
resultList.Add(triggerEvent);
238
}
239
}
240
}
241
242
protected override void OnUpdate()
243
{
244
if (m_Query.CalculateEntityCount() == 0)
245
{
246
return;
247
}
248
249
Dependency = JobHandle.CombineDependencies(m_StepPhysicsWorld.FinalSimulationJobHandle, Dependency);
250
251
Entities
252
.WithName("ClearTriggerEventDynamicBuffersJobParallel")