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

Github branch link: https://github.com/moetsi/Unity-DOTS-Multiplayer-XR-Sample/tree/Updating-Bullets-to-Destroy

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 > Scripts > Stateful and Demos > 2. Setup > 2d. Events > 2d1. Triggers > Scripts

  • There are 6 files that we are going to base our bullet interactions on

    • in /2d. Events/Scripts/Stateful

      • 1. IStatefulSimulationEvent.cs

      • 2. StatefulSimulationEventBuffers.cs

      • 3. StatefulTriggerEvent.cs

      • 4. StatefulTriggerEventBufferAuthoring.cs

      • 5. StatefulTriggerEventBufferSystem.cs

    • in /2d. Events/2d1. Triggers/Scripts

      • 6. TriggerVolumeChangeMaterialAuthoring.cs

The first 5 files purpose is to take DOTS Physics TriggerEvents (which are stateless) and convert them to StatefulTriggerEvents. "Stateless" means that Unity Physics doesn't know what happened "before" ("before" = the previous "state"). So when 2 Physics bodies interact, there is no iterface to know if they "just" interacted this frame, or if they have already interacted.

Classic MonoBehavior physics provided stateful methods like "OnTriggerEnter" and "OnTriggerStay" and "OnTriggerExit". The first 5 files in /2d. Events/Scripts/Stateful job is to provide the same type of functionality when using DOTS Physics. The ability to know when an interaction is Enter/Stay/Exit.

  • The states are defined in IStatefulSimulationEvent.cs as an enum "StatefulEventState"

    • Undefined

    • Enter

    • Stay

    • Exit

  • StatefulTriggerEvent.cs defines the new StatefulTriggerEvent interface which is very similar to TriggerEvent but with the addition of the StatefulEventState

  • StatefulTriggerEventBufferAuthoring.cs is used to for Authoring and is meant to be placed on prefabs to provide it a component that can hold a buffer of StatefulTriggerEvents

  • StatefulSimulationEventBuffers.cs provides jobs used by StatefulTriggerEventBufferSystem.cs to convert TriggerEvents into StatefulEventStates and place them in the buffer provided by StatefulTriggerEventBufferAuthoring.cs

  • TriggerVolumeChangeMaterialAuthoring.cs uses StatefulTriggerEvents on the prefabs to change materials based on interactions

Let's take a deeper look into StatefulTriggerEventBufferSystem.cs to understand a bit more about Dependencies and scheduling. Unity Physics Upgrade guide for 0.7 can help us understand what is going on.

  • RegisterPhysicsRuntimeSystemReadOnly() and RegisterPhysicsRuntimeSystemReadWrite() (both registered as extensions of SystemBase) should be used to manage physics data dependencies instead of the old AddInputDependency() and GetOutputDependency() approach. Users should declare their UpdateBefore and UpdateAfter systems as before and additionally only call one of the two new functions in their system's OnStartRunning(), which will be enough to get an automatic update of the Dependency property without the need for manually combining the dependencies as before. Note that things have not changed if you want to read or write physics runtime data directly in your system's OnUpdate() - in that case, you still need to ensure that jobs from previous systems touching physics runtime data are complete, by completing the Dependency. Also note that BuildPhysicsWorld.AddInputDependencyToComplete() still remains needed for jobs that need to finish before any changes are made to the PhysicsWorld, if your system is not scheduled in between other 2 physics systems that will do that for you.

  • StatefulTriggerEventBufferSystem

    • This is the system that takes the intermediate results of the Solver (which are stateless) and assigns them state (enter, stay, and exit)

    • You can see that (following the instructions from the v0.7 upgrade guide) we are declaring when this system should be run, after running the StepPhysicsWorld and before EndFramePhysicsSystem

      [UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
      [UpdateAfter(typeof(StepPhysicsWorld))]
      [UpdateBefore(typeof(EndFramePhysicsSystem))]
    • We also call RegisterPhysicsRuntimeSystemReadOnly() as described in the upgrade guide

        protected override void OnStartRunning()
        {
            base.OnStartRunning();
            this.RegisterPhysicsRuntimeSystemReadOnly();
        }
    • We can see in StatefulTriggerEventBufferSystem's OnUpdate() that we also follow the note from the v0.7 upgrade guide that "if you want to read or write physics runtime data directly in your system's OnUpdate() - in that case, you still need to ensure that jobs from previous systems touching physics runtime data are complete, by completing the Dependency"

            Dependency = new StatefulEventCollectionJobs.CollectTriggerEvents
            {
                TriggerEvents = currentEvents
            }.Schedule(m_StepPhysicsWorld.Simulation, Dependency);
    • We schedule a StatefulEventCollectionJobs.CollectTriggerEvents job (which itself we define as dependent on StepPhysicsWorld.Simulation) and add the resulting dependency to StatefulTriggerEventBufferSystem's OnUpdate() job

      • 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 (which occurs during StepPhysicsWorld.Simulation) to get the trigger events

      • Otherwise we would be trying to convert TriggerEvents to StatefulTriggerEvents before the TriggerEvents have been calculated (no good! can't convert something that doesn't exist yet!)

    • You can see the requirements for for TriggerEvents to be transformed to StatefulTriggerEvents and stored in a Dynamic Buffer at the top of StatefulSimulationEventBuffers.cs

      • 1) 'Raise Trigger Events' option of the 'Collision Response' on PhysicsShapeAuthoring on the entity that should raise trigger events

      • 2) Add a StatefulTriggerEventBufferAuthoring 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 StatefulTriggerEventBufferAuthoring on the bullet

  • Now take a look at TriggerVolumeChangeMaterialAuthoring.cs

  • 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(StatefulTriggerEventBufferSystem))]

        • StatefulTriggerEventBufferSystemis the system from the previous file we just reviewed

      • This makes sense because we want to perform our actions in the simulation and after we have created our stateful triggers

    • 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

  • 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:

  • Copy and paste the 5 files from /2d. Events/Scripts/Stateful to /ScriptsAndPrefabs

    • 1. IStatefulSimulationEvent.cs

    • 2. StatefulSimulationEventBuffers.cs

    • 3. StatefulTriggerEvent.cs

    • 4. StatefulTriggerEventBufferAuthoring.cs

    • 5. StatefulTriggerEventBufferSystem.cs

  • as well as 3 additional files (these are used for CollisionEvents which we are not concerned with but are also referenced by our systems so they are necessary)

    • 6. StatefulCollisionEvent.cs

    • 7. StatefulCollisionEventBufferAuthoring.cs

    • 8. StatefulCollisionEventBufferSystem.cs

With these 8 files we have what we need to create StatefulEventTriggers on our prefabs 💪

Now let's make some modifications to TriggerVolumeChangeMaterialAuthoring.cs so that bullets change the color of the Asteroid when they intersect and that the Asteroid is given a DestroyTag when the bullet exists.

  • Create ChangeMaterialAndDestroySystem and paste the code snippet below into ChangeMaterialAndDestroySystem.cs:

using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Physics.Stateful;
using Unity.Rendering;
using UnityEngine;

//We did not need the ReferenceEntity so we deleted the IConvertGameObjectToEntity interface
//and the TriggerVolumeChangeMaterial component

[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
[UpdateAfter(typeof(StatefulTriggerEventBufferSystem))]
public partial class ChangeMaterialAndDestroySystem : SystemBase
{
    private EndFixedStepSimulationEntityCommandBufferSystem m_CommandBufferSystem;

    private EntityQueryMask m_NonTriggerMask;

    protected override void OnCreate()
    {
        m_CommandBufferSystem = World.GetOrCreateSystem<EndFixedStepSimulationEntityCommandBufferSystem>();
        m_NonTriggerMask = EntityManager.GetEntityQueryMask(
            GetEntityQuery(new EntityQueryDesc
            {
                None = new ComponentType[]
                {
                    typeof(StatefulTriggerEvent)
                }
            })
        );
    }

    protected override void OnUpdate()
    {
        var commandBuffer = m_CommandBufferSystem.CreateCommandBuffer();

        // Need this extra variable here so that it can
        // be captured by Entities.ForEach loop below
        var nonTriggerMask = m_NonTriggerMask;

        Entities
            .WithName("ChangeMaterialOnTriggerEnter")
            .WithoutBurst()
            .ForEach((Entity e, ref DynamicBuffer<StatefulTriggerEvent> triggerEventBuffer) =>
            {
                for (int i = 0; i < triggerEventBuffer.Length; i++)
                {
                    var triggerEvent = triggerEventBuffer[i];
                    var otherEntity = triggerEvent.GetOtherEntity(e);

                    // exclude other triggers and processed events
                    if (triggerEvent.State == StatefulEventState.Stay || !nonTriggerMask.Matches(otherEntity))
                    {
                        continue;
                    }

                    if (triggerEvent.State == StatefulEventState.Enter)
                    {
                        var volumeRenderMesh = EntityManager.GetSharedComponentData<RenderMesh>(e);
                        var overlappingRenderMesh = EntityManager.GetSharedComponentData<RenderMesh>(otherEntity);
                        overlappingRenderMesh.material = volumeRenderMesh.material;

                        commandBuffer.SetSharedComponent(otherEntity, overlappingRenderMesh);
                    }
                    //The following is what happens on exit
                    else
                    {
                        commandBuffer.AddComponent(otherEntity, new DestroyTag {});
                    }
                }
            }).Run();

        m_CommandBufferSystem.AddJobHandleForProducer(Dependency);
    }
}
  • Next, open the Bullet prefab and add StatefulTriggerEventBufferAuthoring

  • Navigate to SampleScene, reimport ConvertedSubScene

  • Hit "play" and shoot around

  • 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 Base Map 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

  • Now hit "play" and checkout the difference

  • 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 Physics Samples to get an idea of what changes we wanted to make

  • We copied 8 files needed to create StatefulEventTriggers and created ChangeMaterialAndDestroySystem in our project

  • We added the StatefulTriggerEventBufferAuthoring 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 updated