How to make one entity damage another on collision?

I have a bunch of entities, each with a Strength and a Health component, as well as a physics shape.

I am having trouble figuring out how to make them damage each other however. How can I detect a collision between them, and act accordingly?

I was thinking something like:

class DamageOnTouch : SystemBase
{
    protected override void OnUpdate()
    {
        var otherStrength = new ComponentDataFromEntity<Strength>();
        var dt = Time.DeltaTime;

        Entities.ForEach((Entity entity, ref Health health, in Collision collision) =>
            {
                if (otherStrength.Exists(collision.Other))
                {
                    health.Value -= otherStrength[collision.Other].Value * dt;
                }
            })
            .Run();
    }
}

So I lower my health with the strength of the other entity when there is a collision.

Is something like this possible? Or what is the way to do this?

I tried finding some examples, but I’m getting confused as there seems to be old ways of doing it, so I thought to just ask what the current way is.

The most simple way is just using a ITriggerEventsJob/ICollisionEventsJob. On Execute, the job gets a TriggerEvent/Collisionevent, which has .EntityA and .EntityB as fields, so you can do something like this in the ITriggerEventsJob/ICollisionEventsJob:

[ReadOnly] public ComponentDataFromEntity<Strength> damageComponents;
public ComponentDataFromEntity<Health> damageReceiveComponents;

public void Execute(TriggerEvent triggerEvent){
DamageEvaluation(triggerEvent.EntityA, triggerEvent.EntityB);
DamageEvaluation(triggerEvent.EntityB, triggerEvent.EntityA);
}

void DamageEvaluation(Entity dmg_cause, Entity dmg_rec)
{
      if (damageComponents.HasComponent(dmg_cause))
      {
            if (damageReceiveComponents.HasComponent(dmg_rec))
            {
                             //[Your damage code]
            }
     }
}

Works the same in CollisionEvent if you use the collisionevent instead.
The physics samples also have a more powerful CollisionEvent/TriggerEvent system in DynamicBufferCollisionEventAuthoring/DynamicBufferTriggerEventAuthoring if you need the old Stay/Enter/Exit functionality of collisions you may have used before DOTS.

As with all things DOTS, there’s a few things you need to be aware of when using these, the main one being that a single collision can raise more than one hit per frame, so you need to have some functionality in your damage code that queues hits and discards duplicate, so each collider can only apply the damage once per frame.

For more details (and issues :smile:) and samples of code people are using, check this thread: ICollisionEventsJob raises events many times at once

1 Like

Thanks! But it seems you can’t really write to this safely. Do you know the proper way of writing to a component in this case?

You want to queue the damage in some form, for example via a buffer.

I did it this way in my GMTK gamejam game (that I did with DOTS to learn how things can be done):

       if (damageBuffers[targetEntity][_threadId].applier == Entity.Null)
                {
                    DynamicBuffer<DamageApplyBufferElement> damageBuffer = damageBuffers[targetEntity];
                    damageBuffer[_threadId] = dmbuff;
                }

It’s not perfect (and certainly naive in some ways), but works pretty well even with a pretty high number of entities.

_threadID is the # of the workerthread, defined by [NativeSetThreadIndex] private int _threadId;
damagebuffers is public BufferFromEntity<DamageApplyBufferElement> damageBuffers;
A single DamageApplyBufferElement is just a struct holding the data of a single hit (damage value, entity that did the hit, knockback, knockback direction, that sort of stuff).

If a frame has a potential damage hit, I check if the position in the buffer representing the worker thread isn’t already occupied by a hit this frame, and if it isn’t, I place the Bufferelement with the hit data in there.

I then evaluate that afterwards by going through the buffers of all involved entities, discard excess hits, and apply damage. Then I clear the buffer.

In theory, it’s possible that this way means that if an entity is hit by LOTS of entities in a frame, some hits are discarded, but for my use case, that doesn’t matter - if say 10 things hit an entity, only one would do damage and the rest would be blocked by iframes anyway. And the buffer is small enough that it really doesn’t seem to be a performance problem.

[Of course, if Pros have better ways, I’m all ears myself, but for my use case, it totally seems to work and might make a good base for you, too!]

3 Likes

I don’t really understand these buffers yet. Could you provide a full code example?

I however figured out a different way, using entityCommandBuffer.SetComponent. I’m assuming it has the same problem of only really allowing for a single hit per frame, as it’ll probably set the component multiple times but each time override the damage from the other hits this frame.

My whole system currently is:

using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Physics;
using Unity.Physics.Systems;

[UpdateAfter(typeof(EndFramePhysicsSystem))]
public class DealsDamageOnCollisionJobSystem : JobComponentSystem
{
    private EndSimulationEntityCommandBufferSystem endSimECBSystem;
    private BuildPhysicsWorld buildPhysicsWorld;
    private StepPhysicsWorld stepPhysicsWorld;

    private struct DealsDamageOnCollision : ICollisionEventsJob
    {
        private EntityCommandBuffer entityCommandBuffer;
        [ReadOnly] private ComponentDataFromEntity<Health> healthData;
        [ReadOnly] private ComponentDataFromEntity<Strength> damageData;
        [ReadOnly] private float dt;

        public DealsDamageOnCollision(
            EntityCommandBuffer entityCommandBuffer,
            [ReadOnly] ComponentDataFromEntity<Health> healthData,
            [ReadOnly] ComponentDataFromEntity<Strength> damageData,
            [ReadOnly] float dt)
        {
            this.entityCommandBuffer = entityCommandBuffer;
            this.healthData = healthData;
            this.damageData = damageData;
            this.dt = dt;
        }

        public void Execute(CollisionEvent collisionEvent)
        {
            // UnityEngine.Debug.Log($"{collisionEvent.Entities.EntityA}, {collisionEvent.Entities.EntityB}");
            CheckAndDealDamage(collisionEvent.EntityA, collisionEvent.EntityB);
            CheckAndDealDamage(collisionEvent.EntityB, collisionEvent.EntityA);
        }

        private void CheckAndDealDamage(Entity attacker, Entity defender)
        {
            if (damageData.Exists(attacker) && healthData.Exists(defender))
            {
                var health = healthData[defender];
                health.Value -= damageData[attacker].Value * dt;
                entityCommandBuffer.SetComponent(defender, health);
            }
        }
    }

    protected override void OnCreate()
    {
        endSimECBSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
        buildPhysicsWorld = World.GetOrCreateSystem<BuildPhysicsWorld>();
        stepPhysicsWorld = World.GetOrCreateSystem<StepPhysicsWorld>();
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        // If physics are off, get out of here
        if (stepPhysicsWorld.Simulation.Type == SimulationType.NoPhysics)
            return default;

        JobHandle jobHandle = new DealsDamageOnCollision(
            endSimECBSystem.CreateCommandBuffer(),
            GetComponentDataFromEntity<Health>(true),
            GetComponentDataFromEntity<Strength>(true),
            Time.DeltaTime
        ).Schedule(stepPhysicsWorld.Simulation, ref buildPhysicsWorld.PhysicsWorld, inputDeps);

        endSimECBSystem.AddJobHandleForProducer(jobHandle);

        return jobHandle;
    }
}

Essentially these three lines are the important part:

var health = healthData[defender];
health.Value -= damageData[attacker].Value * dt;
entityCommandBuffer.SetComponent(defender, health);

All the rest is just boilerplate tbh.

Sure! Here’s a version where I cut out everything not directly relevant.

using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Physics;
using Unity.Physics.Systems;
using Unity.Transforms;
using UnityEngine;
using static CharacterControllerUtilities;
using static Unity.Physics.PhysicsStep;
using Unity.Collections.LowLevel.Unsafe;

[UpdateAfter(typeof(ExportPhysicsWorld))]
public class CollisionTriggerSystem : SystemBase//JobComponentSystem
{
    EndFramePhysicsSystem m_EndFramePhysicsSystem;

    private BuildPhysicsWorld buildPhysicsWorld;
    private StepPhysicsWorld stepPhysicsWorld;
    private ExportPhysicsWorld expWorld;

    protected override void OnCreate()
    {
        buildPhysicsWorld = World.GetOrCreateSystem<BuildPhysicsWorld>();
        stepPhysicsWorld = World.GetOrCreateSystem<StepPhysicsWorld>();
        expWorld = World.GetOrCreateSystem<ExportPhysicsWorld>();
        m_EndFramePhysicsSystem = World.GetOrCreateSystem<EndFramePhysicsSystem>();
    }

    protected override void OnUpdate()//JobHandle inputDeps)
    {
        //Added this for other reasons, not sure if you need these!
        JobHandle inputDeps = JobHandle.CombineDependencies(Dependency, expWorld.GetOutputDependency(), buildPhysicsWorld.GetOutputDependency());
        var triggerCheckJob = new TriggerCheckJob
        {
            damageComponents = GetComponentDataFromEntity<DamageComponent>(),
            damageReceiveComponents = GetComponentDataFromEntity<DamageReceiveComponent>(),
            damageBuffers = GetBufferFromEntity<DamageApplyBufferElement>(),
        };
       
        inputDeps = triggerCheckJob.Schedule(stepPhysicsWorld.Simulation, ref buildPhysicsWorld.PhysicsWorld, inputDeps);
        Dependency = Entities
            .WithBurst()
            .ForEach((ref DamageReceiveComponent dmgbody, ref DynamicBuffer<DamageApplyBufferElement> dambuff) =>
        {
            if (dmgbody.iFrames > 0) { dmgbody.iFrames -= deltaTime; }
            DamageApplyBufferElement dmg_Clear = new DamageApplyBufferElement { };
            dmg_Clear.applier = Entity.Null;
            for (int buffIndex = 0; buffIndex < dambuff.Length; buffIndex++)
            {
                if (dambuff[buffIndex].applier != Entity.Null)
                {
                    if (dmgbody.iFrames <= 0.005f && dambuff[buffIndex].precalc_Damage > 0)
                    {
                        dmgbody.hp -= dambuff[buffIndex].precalc_Damage;
                        dmgbody.iFrames = dambuff[buffIndex].iframe_Dur;
                        //also knockback etc
                    }
                }
                dambuff[buffIndex] = dmg_Clear;
            }
        }).Schedule(inputDeps);
    }

    [BurstCompile]
    private struct TriggerCheckJob : ITriggerEventsJob
    {
        [NativeSetThreadIndex] private int _threadId;
        [ReadOnly] public ComponentDataFromEntity<DamageComponent> damageComponents;
        [ReadOnly] public ComponentDataFromEntity<DamageReceiveComponent> damageReceiveComponents;
        public BufferFromEntity<DamageApplyBufferElement> damageBuffers;

        public void Execute(TriggerEvent triggerEvent)
        {
            DamageEvaluation(triggerEvent.EntityA, triggerEvent.EntityB);
            DamageEvaluation(triggerEvent.EntityB, triggerEvent.EntityA);
        }
       
        void DamageEvaluation(Entity dmg_cause, Entity dmg_rec)
        {
            if (damageComponents.HasComponent(dmg_cause))
            {
                if (damageReceiveComponents.HasComponent(dmg_rec))
                {
                    DamageApplyBufferElement dmbuff = new DamageApplyBufferElement
                    {
                        applier = dmg_cause,
                        iframe_Dur = 0,
                        precalc_Damage = 0,
                    };
                    if (damageReceiveComponents[dmg_rec].iFrames > 0.005f) { return; }
                    DamageReceiveComponent damRecComp = damageReceiveComponents[dmg_rec];
                    dmbuff.precalc_Damage = damageComponents[dmg_cause].damage;
                    dmbuff.iframe_Dur = damageComponents[dmg_cause].iFrameDur;
                    DynamicBuffer<DamageApplyBufferElement> damageBuffer = damageBuffers[dmg_rec];
                    damageBuffer[_threadId] = dmbuff;
                }
            }
        }
    }

}

My buffer also holds the impact direction, force of the knockback, etc.

        DynamicBuffer<DamageApplyBufferElement> pusht = dstManager.AddBuffer<DamageApplyBufferElement>(entity);
        for (int i = 0; i < threadcount_max; i++)
        {
            DamageApplyBufferElement buff = new DamageApplyBufferElement {
                applier = Entity.Null,
                iframe_Dur = 0,
                precalc_Damage = 0,
                precalc_pushdir = float3.zero,
                precalc_pushtime = 0,
            };
            pusht.Add(buff);
        }

And this is how I add such a buffer in the AuthoringComponent. It seemed really straightforward to me, easy to expand too.

3 Likes