Use DOTS Physics for Collisions
Code and workflows to update collisions that change material and destroy prefab entities using DOTS Physics

What will be developed on this page

We will update our project so that bullet collisions cause the material of the colliding entity to change and for the entity to be destroyed

Adding trigger events

First, some background:

We have mostly used .ForEach() and Job.WithCode() in this project. At their core, both of these interfaces run "jobs." When we implement our trigger events for bullet collisions we will also be using jobs, but some of the code will be using the core job interface. In some of the files we will be examining you can see the "job" struct.
    Open the downloaded EntityComponentSystemSamples repo and navigate to Demos > 2. Setup > 2d. Events > 2d1. Triggers > Scripts
    In this folder there are 2 files we are going to base our bullet interactions on
      DynamicBufferTriggerEventAuthoring
      TriggerVolumeChangeMaterialAuthoring
    Open DynamicBufferTriggerEventAuthoring and checkout through the code to get an understanding of what it does
Navigating to DynamicBufferTriggerEventAuthoring in the Unity Physics sample repo
    This file has a few key components
      EventOverlapState
      StatefulTriggerEvent
      ExcludeFromTriggerEventConversion
      TriggerEventConversionSystem
      DynamicBufferTriggerEventAuthoring
    EventOverlapState
      These are the "states" a trigger can be
      EventOverlapState.Enter, when 2 bodies are overlapping in the current frame, but they did not overlap in the previous frame
      EventOverlapState.Stay, when 2 bodies are overlapping in the current frame, and they did overlap in the previous frame
      EventOverlapState.Exit, when 2 bodies are NOT overlapping in the current frame, but they did overlap in the previous frame
    StatefulTriggerEvent
      This is the actual stateful trigger event being stored in a dynamic buffer
      Provides some methods like "GetOtherEntity"
      So when we have a trigger event (like we will when our bullet passes through an asteroid) we can call "GetOtherEntity" to reference the asteroid (and in our case, add a DestroyTag to it)
    ExcludeFromTriggerEventConversion
      This is a Tag that is used for CharacterControllers to exclude it from adding trigger events to the dynamic buffer
      Not important for this gitbook, but still good to know
    TriggerEventConversionSystem
      This is a hardcore system that takes the intermediate results of the Solver (which are stateless) and assigns them state (enter, stay, and exit)
      In order for TriggerEvents to be transformed to StatefulTriggerEvents and stored in a Dynamic Buffer, you must:
        1) Tick "IsTrigger" on PhysicsShapeAuthoring on the entity that should raise trigger events
        2) Add a DynamicBufferTriggerEventAuthoring component to that entity
        3) If this is desired on a Character Controller, tick "RaiseTriggerEvents" on CharacterControllerAuthoring (and skip (1) and (2))
        We already chose "Raise Trigger Events" on our bullet prefab so we are set there, and we will add DynamicBufferTriggerEventAuthoring on the bullet
      We can see in TriggerEventConversionSystem's OnUpdate() that the system takes the FinalSimulationJobHandle of the StepPhysicsWorld as a dependency
        That means this system is dependent on the results of that system to perform its operations
        This makes sense because we need to grab the results of the Solver to get the trigger events
      Near the bottom we can see CollectTriggerEventJob which implements the ITriggerEventsJob interface (mentioned in the previous section)
    DynamicBufferTriggerEventAuthoring
      An implementation of the IConvertGameObjectToEntity interface
        When we place this script on a GameObject, it creates "stateful" trigger events from the stateless triggers created by DOTS Physics
In short, if you put the DynamicBufferTriggerEventAuthoring on a prefab, it will create a dynamic buffer of stateful triggers on the entity. This is why it is highly recommended to look through Unity DOTS Physics samples; making this type of script completely on your own would not be very fun πŸ‘Ž
    Now take a look at TriggerVolumeChangeMaterialAuthoring
Taking a look at TriggerVolumeChangeMaterialAuthoring from Unity DOTS Physics sample repo
    This file also has an implementation of the IConvertGameObjectToEntity interface
      This adds the "TriggerVolumeChangeMaterial" Component (defined at the top of the file) to the converted GameObject
      It will grab whatever GameObject we drag onto the public field "ReferenceGameObject" in TriggerVolumeChangeMaterialAuthoring and set it as the ReferenceEntity field of the TriggerVolumeChangeMaterial Component
      Or, if the public field is left null, IConvertGameObjectToEntity will set the reference entity as the GameObject too
      This ReferenceEntity is used further down to reset the material to the same material as the ReferenceEntity
    Let's keep moving down the file...
    TriggerVolumeChangeMaterialSystem
      There are a couple of decorators at the top of the file
        [UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
          FixedStepSimulationSystemGroup is the system group we reviewed in the previous section
        [UpdateAfter(typeof(TriggerEventConversionSystem))]
          TriggerEventConversionSystem is the system fro the previous file we just reviewed, DynamicBufferTriggerEventAuthoring
        This makes sense because we want to perform our actions in the simulation and after we have created our stateful triggers
      We can see at the beginning of the OnUpdate() that TriggerVolumeChangeMaterialSystem has a dependency on the output of TriggerEventConversionSystem, so the dependencies combine
      This makes sense because we need the stateful triggers produced by TriggerEventConversionSystem to exist if we want to act on them
      Now, in the OnUpdate() is where we find the code that causes the material to change
Taking a look at TriggerVolumeChangeMaterialAuthoring
    By taking a look at the code that runs when shapes stop intersecting, you can see the purpose of the ReferenceEntity field
      The ReferenceEntity's RenderMesh is used
      Once the intersection stops, the RenderMesh is set equal to the ReferenceEntity's RenderMesh
        This is why the ball turns back to its original color in the sample
    We are going to update this code so that when a bullet first intersects with any entity, the other entity will have its material changed to the same material as the bullet
    "On exit" we will add a DestroyTag to the entity
    We do not need the "ReferenceEntity" because we will not be changing material back on exit. Instead, we will be able to remove TriggerVolumeChangeMaterial and TriggerChangeMaterialAndDestroyAuthoring

Now, let's implement:

    We are going to create DynamicBufferTriggerEventAuthoring in our project and an updated TriggerVolumeChangeMaterialAuthoring called ChangeMaterialAndDestroySystem
    Create DynamicBufferTriggerEventAuthoring and paste the code snippet below into 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
// 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")
253
.WithBurst()
254
.WithNone<ExcludeFromTriggerEventConversion>()
255
.ForEach((ref DynamicBuffer<StatefulTriggerEvent> buffer) =>
256
{
257
buffer.Clear();
258
}).ScheduleParallel();
259
​
260
SwapTriggerEventStates();
261
​
262
var currentFrameTriggerEvents = m_CurrentFrameTriggerEvents;
263
var previousFrameTriggerEvents = m_PreviousFrameTriggerEvents;
264
​
265
var triggerEventBufferFromEntity = GetBufferFromEntity<StatefulTriggerEvent>();
266
var physicsWorld = m_BuildPhysicsWorld.PhysicsWorld;
267
​
268
var collectTriggerEventsJob = new CollectTriggerEventsJob
269
{
270
TriggerEvents = currentFrameTriggerEvents
271
};
272
​
273
var collectJobHandle = collectTriggerEventsJob.Schedule(m_StepPhysicsWorld.Simulation, ref physicsWorld, Dependency);
274
​
275
// Using HashMap since HashSet doesn't exist
276
// Setting value type to byte to minimize memory waste
277
NativeHashMap<Entity, byte> entitiesWithBuffersMap = new NativeHashMap<Entity, byte>(0, Allocator.TempJob);
278
​
279
var collectTriggerBuffersHandle = Entities
280
.WithName("CollectTriggerBufferJob")
281
.WithBurst()
282
.WithNone<ExcludeFromTriggerEventConversion>()
283
.ForEach((Entity e, ref DynamicBuffer<StatefulTriggerEvent> buffer) =>
284
{
285
entitiesWithBuffersMap.Add(e, 0);
286
}).Schedule(Dependency);
287
​
288
Dependency = JobHandle.CombineDependencies(collectJobHandle, collectTriggerBuffersHandle);
289
​
290
Job
291
.WithName("ConvertTriggerEventStreamToDynamicBufferJob")
292
.WithBurst()
293
.WithCode(() =>
294
{
295
currentFrameTriggerEvents.Sort();
296
​
297
var triggerEventsWithStates = new NativeList<StatefulTriggerEvent>(currentFrameTriggerEvents.Length, Allocator.Temp);
298
​
299
UpdateTriggerEventState(previousFrameTriggerEvents, currentFrameTriggerEvents, triggerEventsWithStates);
300
AddTriggerEventsToDynamicBuffers(triggerEventsWithStates, ref triggerEventBufferFromEntity, entitiesWithBuffersMap);
301
}).Schedule();
302
​
303
m_EndFramePhysicsSystem.AddInputDependency(Dependency);
304
entitiesWithBuffersMap.Dispose(Dependency);
305
}
306
​
307
[BurstCompile]
308
public struct CollectTriggerEventsJob : ITriggerEventsJob
309
{
310
public NativeList<StatefulTriggerEvent> TriggerEvents;
311
​
312
public void Execute(TriggerEvent triggerEvent)
313
{
314
TriggerEvents.Add(new StatefulTriggerEvent(
315
triggerEvent.EntityA, triggerEvent.EntityB, triggerEvent.BodyIndexA, triggerEvent.BodyIndexB,
316
triggerEvent.ColliderKeyA, triggerEvent.ColliderKeyB));
317
}
318
}
319
}
320
​
321
public class DynamicBufferTriggerEventAuthoring : MonoBehaviour, IConvertGameObjectToEntity
322
{
323
public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
324
{
325
dstManager.AddBuffer<StatefulTriggerEvent>(entity);
326
}
327
}
328
}
Copied!
    Create ChangeMaterialAndDestroySystem and paste the code snippet below into ChangeMaterialAndDestroySystem.cs:
1
using Unity.Collections;
2
using Unity.Entities;
3
using Unity.Jobs;
4
using Unity.Physics.Stateful;
5
using Unity.Rendering;
6
using UnityEngine;
7
​
8
//We did not need the ReferenceEntity so we deleted the IConvertGameObjectToEntity interface
9
//and the TriggerVolumeChangeMaterial component
10
​
11
[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
12
[UpdateAfter(typeof(TriggerEventConversionSystem))]
13
public class ChangeMaterialAndDestroySystem : SystemBase
14
{
15
private EndFixedStepSimulationEntityCommandBufferSystem m_CommandBufferSystem;
16
​
17
private TriggerEventConversionSystem m_TriggerSystem;
18
private EntityQueryMask m_NonTriggerMask;
19
​
20
protected override void OnCreate()
21
{
22
m_CommandBufferSystem = World.GetOrCreateSystem<EndFixedStepSimulationEntityCommandBufferSystem>();
23
m_TriggerSystem = World.GetOrCreateSystem<TriggerEventConversionSystem>();
24
m_NonTriggerMask = EntityManager.GetEntityQueryMask(
25
GetEntityQuery(new EntityQueryDesc
26
{
27
None = new ComponentType[]
28
{
29
typeof(StatefulTriggerEvent)
30
}
31
})
32
);
33
}
34
​
35
protected override void OnUpdate()
36
{
37
Dependency = JobHandle.CombineDependencies(m_TriggerSystem.OutDependency, Dependency);
38
​
39
var commandBuffer = m_CommandBufferSystem.CreateCommandBuffer();
40
​
41
// Need this extra variable here so that it can
42
// be captured by Entities.ForEach loop below
43
var nonTriggerMask = m_NonTriggerMask;
44
​
45
Entities
46
.WithName("ChangeMaterialOnTriggerEnter")
47
.WithoutBurst()
48
.ForEach((Entity e, ref DynamicBuffer<StatefulTriggerEvent> triggerEventBuffer) =>
49
{
50
for (int i = 0; i < triggerEventBuffer.Length; i++)
51
{
52
var triggerEvent = triggerEventBuffer[i];
53
var otherEntity = triggerEvent.GetOtherEntity(e);
54
​
55
// exclude other triggers and processed events
56
if (triggerEvent.State == EventOverlapState.Stay || !nonTriggerMask.Matches(otherEntity))
57
{
58
continue;
59
}
60
​
61
if (triggerEvent.State == EventOverlapState.Enter)
62
{
63
var volumeRenderMesh = EntityManager.GetSharedComponentData<RenderMesh>(e);
64
var overlappingRenderMesh = EntityManager.GetSharedComponentData<RenderMesh>(otherEntity);
65
overlappingRenderMesh.material = volumeRenderMesh.material;
66
​
67
commandBuffer.SetSharedComponent(otherEntity, overlappingRenderMesh);
68
}
69
//The following is what happens on exit
70
else
71
{
72
commandBuffer.AddComponent(otherEntity, new DestroyTag {});
73
}
74
}
75
}).Run();
76
​
77
m_CommandBufferSystem.AddJobHandleForProducer(Dependency);
78
}
79
}
Copied!
    Next, open the Bullet prefab and add DynamicBufferTriggerEventAuthoring
    Navigate to SampleScene, reimport ConvertedSubScene
    Hit "play" and shoot around
Adding DynamicBufferTriggerEventAuthoring and ChangeMaterialAndDestroySystem
    Woo hoo! We have a working collision trigger system πŸ‘
      In our testing we found that occasionally it is necessary to "Reimport All" assets for the changes to take (go to "Assets" then select "Reimport All")
    The changing of material is... underwhelming
    Let's update the Bullet prefab to have a Red material
    In the Asset folder, right-click, choose "Create", select "Material" and name it "Red"
    Select the Red material, go to Albedo in the Inspector, and change it to a red color and save
    Select the Bullet prefab and change the Mesh Renderer material to Red
    Hit save, navigate to SampleScene, and reimport the ConvertedSubScene
Updating the Bullet prefab to be red
    Now hit "play" and checkout the difference
Shooting red bullets to destroy asteroids
    Now it's a bit more exciting, right?! 😬
      Remember, the purpose of this gitbook is to give a "how" of using Unity's DOTS packages, not to make an exciting game πŸ₯Ί
We now know how to make collisions using DOTS Physics
    We navigated through DOTS Physics samples to find a sample that matched our needs
    We checked out the sample scene to get an idea of the different components
    We read through DynamicBufferTriggerEventAuthoring and TriggerVolumeChangeMaterialAuthoring to get an idea of what changes we wanted to make
    We implemented DynamicBufferTriggerEventAuthoring and ChangeMaterialAndDestroySystem in our project
    We added the DynamicBufferTriggerEventAuthoring to our Bullet prefab and updated the prefab to be red
Github branch link:
git clone https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/ git checkout 'Updating-Bullets-to-Destroy'
Last modified 6mo ago