Search Unity

Handle Collisions in Unity Physics ECS

Discussion in 'Entity Component System' started by EdRowlett-Barbu, Mar 31, 2019.

  1. EdRowlett-Barbu

    EdRowlett-Barbu

    Joined:
    Mar 16, 2015
    Posts:
    88
    For all archetypes of a certain set of ComponentDatas (they contain PhysicsShape/collider and a tag component), I want to execute a piece of code if they are in collision with something.

    So I'd have an IJobProcessComponentDataWithEntity processing all those archetypes, but then how do I check for each of those entities what collisions it has? Checking for each if they're in the array of collisions that's in the physics world would lead to O(n^2) complexity. Iterating over all the collisions and then looking up the entities doesn't seem scalable, because then every system that needs collisions would have to iterate again and again over that array of collisions.

    In short, how do you efficiently handle collisions/triggers in Unity Physics ecs?
     
  2. TRS6123

    TRS6123

    Joined:
    May 16, 2015
    Posts:
    246
    I'd recommend utilizing collision filters provided via the Physics Shape script and iterating through collision and trigger events
     
  3. jacasch

    jacasch

    Joined:
    Jan 20, 2017
    Posts:
    16
    Could you explain that a bit deeper?
     
  4. TRS6123

    TRS6123

    Joined:
    May 16, 2015
    Posts:
    246
    Here's an example of how I process Collision and Trigger events

    Code (CSharp):
    1. public struct CollisionStayRef : IBufferElementData
    2.     {
    3.         public Entity Value;
    4.     }
    5.  
    6.     public struct TriggerStayRef : IBufferElementData
    7.     {
    8.         public Entity Value;
    9.     }
    10.  
    11. [UpdateAfter(typeof(StepPhysicsWorld)), UpdateBefore(typeof(EndFramePhysicsSystem))]
    12.     public class ExportPhysicsEventsSystem : JobComponentSystem
    13.     {
    14.         [BurstCompile, RequireComponentTag(typeof(CollisionStayRef))]
    15.         private struct ClearCollisions : IJobForEachWithEntity<PhysicsVelocity>
    16.         {
    17.             [NativeDisableParallelForRestriction] public BufferFromEntity<CollisionStayRef> CollisionStayRefsFromEntity;
    18.  
    19.             public void Execute(Entity entity, int index, [ReadOnly] ref PhysicsVelocity velocity)
    20.             {
    21.                 CollisionStayRefsFromEntity[entity].Clear();
    22.             }
    23.         }
    24.  
    25.         [BurstCompile, RequireComponentTag(typeof(TriggerStayRef))]
    26.         private struct ClearTriggers : IJobForEachWithEntity<PhysicsVelocity>
    27.         {
    28.             [NativeDisableParallelForRestriction] public BufferFromEntity<TriggerStayRef> TriggerStayRefsFromEntity;
    29.  
    30.             public void Execute(Entity entity, int index, [ReadOnly] ref PhysicsVelocity velocity)
    31.             {
    32.                 TriggerStayRefsFromEntity[entity].Clear();
    33.             }
    34.         }
    35.  
    36.         [BurstCompile]
    37.         private struct ProcessCollisions : IJob
    38.         {
    39.             [NativeDisableParallelForRestriction] public BufferFromEntity<CollisionStayRef> CollisionStayRefsFromEntity;
    40.             [ReadOnly] public PhysicsWorld PhysicsWorld;
    41.             [ReadOnly] public CollisionEvents CollisionEvents;
    42.  
    43.             public void Execute()
    44.             {
    45.                 foreach (CollisionEvent collisionEvent in CollisionEvents)
    46.                 {
    47.                     Entity entityA = PhysicsWorld.Bodies[collisionEvent.BodyIndices.BodyAIndex].Entity;
    48.                     Entity entityB = PhysicsWorld.Bodies[collisionEvent.BodyIndices.BodyBIndex].Entity;
    49.                     if (CollisionStayRefsFromEntity.Exists(entityA))
    50.                         CollisionStayRefsFromEntity[entityA].Add(new CollisionStayRef { Value = entityB });
    51.                     if (CollisionStayRefsFromEntity.Exists(entityB))
    52.                         CollisionStayRefsFromEntity[entityB].Add(new CollisionStayRef { Value = entityA });
    53.                 }
    54.             }
    55.         }
    56.  
    57.         [BurstCompile]
    58.         private struct ProcessTriggers : IJob
    59.         {
    60.             [NativeDisableParallelForRestriction] public BufferFromEntity<TriggerStayRef> TriggerStayRefsFromEntity;
    61.             [ReadOnly] public PhysicsWorld PhysicsWorld;
    62.             [ReadOnly] public TriggerEvents TriggerEvents;
    63.  
    64.             public void Execute()
    65.             {
    66.                 foreach (TriggerEvent triggerEvent in TriggerEvents)
    67.                 {
    68.                     Entity entityA = PhysicsWorld.Bodies[triggerEvent.BodyIndices.BodyAIndex].Entity;
    69.                     Entity entityB = PhysicsWorld.Bodies[triggerEvent.BodyIndices.BodyBIndex].Entity;
    70.                     if (TriggerStayRefsFromEntity.Exists(entityA))
    71.                         TriggerStayRefsFromEntity[entityA].Add(new TriggerStayRef { Value = entityB });
    72.                     if (TriggerStayRefsFromEntity.Exists(entityB))
    73.                         TriggerStayRefsFromEntity[entityB].Add(new TriggerStayRef { Value = entityA });
    74.                 }
    75.             }
    76.         }
    77.  
    78.         private BuildPhysicsWorld buildPhysicsWorld;
    79.         private StepPhysicsWorld stepPhysicsWorld;
    80.         private EndFramePhysicsSystem endFramePhysicsSystem;
    81.         private EntityQuery group;
    82.  
    83.         protected override void OnCreate()
    84.         {
    85.             buildPhysicsWorld = World.GetOrCreateSystem<BuildPhysicsWorld>();
    86.             stepPhysicsWorld = World.GetOrCreateSystem<StepPhysicsWorld>();
    87.             endFramePhysicsSystem = World.GetOrCreateSystem<EndFramePhysicsSystem>();
    88.         }
    89.  
    90.         protected override JobHandle OnUpdate(JobHandle inputDeps)
    91.         {
    92.             var collisionStayRefsFromEntity = GetBufferFromEntity<CollisionStayRef>();
    93.             var triggerStayRefsFromEntity = GetBufferFromEntity<TriggerStayRef>();
    94.             inputDeps = JobHandle.CombineDependencies
    95.             (
    96.                 new ClearCollisions { CollisionStayRefsFromEntity = collisionStayRefsFromEntity }.Schedule(this, inputDeps),
    97.                 new ClearTriggers { TriggerStayRefsFromEntity = triggerStayRefsFromEntity }.Schedule(this, inputDeps)
    98.             );
    99.             inputDeps = JobHandle.CombineDependencies(inputDeps, buildPhysicsWorld.FinalJobHandle, stepPhysicsWorld.FinalSimulationJobHandle);
    100.             var physicsWorld = buildPhysicsWorld.PhysicsWorld;
    101.             var collisionEvents = stepPhysicsWorld.Simulation.CollisionEvents;
    102.             var triggerEvents = stepPhysicsWorld.Simulation.TriggerEvents;
    103.             inputDeps = JobHandle.CombineDependencies
    104.             (
    105.                 new ProcessCollisions { CollisionStayRefsFromEntity = collisionStayRefsFromEntity, PhysicsWorld = physicsWorld, CollisionEvents = collisionEvents }.Schedule(inputDeps),
    106.                 new ProcessTriggers { TriggerStayRefsFromEntity = triggerStayRefsFromEntity, PhysicsWorld = physicsWorld, TriggerEvents = triggerEvents }.Schedule(inputDeps)
    107.             );
    108.             endFramePhysicsSystem.HandlesToWaitFor.Add(inputDeps);
    109.             return inputDeps;
    110.         }
    111.     }
     
    RahulRaman, jdtec, NotaNaN and 8 others like this.
  5. EdRowlett-Barbu

    EdRowlett-Barbu

    Joined:
    Mar 16, 2015
    Posts:
    88
    I see you're using bufferelementdata to store the state of the object. That doesn't seem that efficient, since you're basically changing the archetype of the object whenever the collision state changes, unless I'm misunderstanding or missing something?

     
  6. TRS6123

    TRS6123

    Joined:
    May 16, 2015
    Posts:
    246
    Last time I checked changing a Dynamic Buffer on an entity doesn't change its archetype, only adding/removing them from the entity does.
     
    Enzi likes this.
  7. EdRowlett-Barbu

    EdRowlett-Barbu

    Joined:
    Mar 16, 2015
    Posts:
    88
    are buffer element datas not stored in the same chunk with the entity? I'm not super familiar with buffer element datas. I'd imagine if it is and you add elements, the entity would need to be relocated to a different archetype chunk. On the other hand, that would create a lot of chunks for entities with different amount of the same type of buffer element datas. I don't know how the system works, curious
     
  8. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,780
    As far I remember correctly, buffers which are bigger than chunk capacity, are stored outside chunks. And only reference to these buffer is used. So yes, changing buffers does not affect chunk itself.

    One thing I am not 100% sure is, if small buffers fitting in chunks, I.e. of few elements, are stored in chunks as well. Or if every buffer uses reference, irrespectively to buffer capacity.
     
  9. JooleanLogic

    JooleanLogic

    Joined:
    Mar 1, 2018
    Posts:
    447
    Antypodish is right. Some additional:
    Buffer header (16 bytes, points to buffer data) is always stored in the chunk like any other component. I think min buffer size in chunk is 128 bytes.
    Buffer data is also stored in chunk unless its initial capacity is greater than chunk size.
    Buffer data is moved to heap once you add greater than initial buffer capacity and then it stays there. You can bring it back to chunk with DynamicBuffer.TrimExcess() provided it fits.

    So if you're using variable sized buffers or large buffers, it might be better to specify a small initial capacity and have them all reallocated to the heap so you can fit more entities in chunk. It also means when you move entities, you're only moving 128 bytes instead of maybe kilobytes per entity.
     
    Antypodish likes this.
  10. EdRowlett-Barbu

    EdRowlett-Barbu

    Joined:
    Mar 16, 2015
    Posts:
    88
    Thanks for the info guys.
    Coming back to the original question of how to subscribe to collision events, do any of you guys do it differently than what TRS6123 posted? Is this the way the Unity.Physics team intended it to be used?
     
  11. JooleanLogic

    JooleanLogic

    Joined:
    Mar 1, 2018
    Posts:
    447
    I just create job component systems as event handlers and each one filters the collision events for what it's interested in.
    I add one optimisation system which first maps which types collided with each other. This allows the handlers to check if a collision they're interested in actually happened, and if not, don't bother running the job. E.g.
    Code (CSharp):
    1. JobHandle OnUpdate(JobHandle inputDeps) {
    2.     if (! CollisionManager.hasCollisions(Tag.Ship, Tag.FuelDepot, CollisionType.TriggerEnter)
    3.         return inputDeps;
    4.  
    5.     // run job
    6.     ...
    7. }
     
  12. nttLIVE

    nttLIVE

    Joined:
    Sep 13, 2018
    Posts:
    80
    Right now the Physics Team does not seem to have a specific replacement for the typical "OnEnter, OnExit, OnStay", and I would suspect that even if they ended up making an intended way to interact with these Events it would be very similar to what TRS posted. At least right now it seems like a very good approach.
     
  13. JooleanLogic

    JooleanLogic

    Joined:
    Mar 1, 2018
    Posts:
    447
    Is there any property within the trigger/collision data currently to say whether it was onEnter or onExit?
    Or is it the case that TriggerEvents and CollisionEvents are created for every frame that a pair remain in contact and thus you could infer this yourself?
    E.g. if A and B collide then remain in contact for 5 frames, is a CollisionEvent generated for every frame or just the first one?
     
  14. nttLIVE

    nttLIVE

    Joined:
    Sep 13, 2018
    Posts:
    80
    You need to infer it yourself.
     
    JooleanLogic likes this.
  15. Ashkan_gc

    Ashkan_gc

    Joined:
    Aug 12, 2009
    Posts:
    1,124
    @Zadirion Previously the physics engine would keep the list of currently in collision objects then after doing the two collision phases checked all of the objects in collision with its lists and then call callbacks (enter, sta, exit) on them and now in the data oriented way the physics engine doesn't assume you need this and just gives you the result of its two phases of collisions. if you don't need that , you can use the bvh tree code alone . it might seem much work to update all entities which you want to handle collisions for in the triggers job and then process them in another system but this has always been done before so you know have the chance to reduce the amount of work done.

    Previously no matter if you needed the thing or not the callback was being called so the cache would get invalidated. now in the triggers job you have to check the components of the entities and see if you need to run any additional code for it or not and if yes then probably add to its buffer/modify a component of it which says to the entity that you have collided with this other entity or just you have a collision. if you need to check enter/exit then you can do so yourself easily by having a system which checks last frame of collision for the entity and a system which writes the current frame to the component when collisions happen.

    So let's see what happened to collisions you had before and what happens now. coins only are interested to collision with characters so if a bullet goes through them they check its tag/component and then exit. for that to happen PhysX found the coin script and called its OnTriggerEnter on it and send it the other object.
    Now you have a huge array of entities which for each of them you check if they have a coinCollision component and if yes then check if the other entity colliding with them has a player component or not, if yes then you check if the coinCollision component processed current event when it started (enter) or not by checking its in collision field, if false then you set it to true and set its frame to current frame and either here or in another system process the collision.
    then you have another system which checks the current frame with all collision frames and if the collision frames are smaller than the current frame set their flag to false (this is the exit event)
    in between all of the events are stay
    now if you were not doing this then the physics system had to track your enter, stay and exit but now the difference is that for the objects which don't need them, this is no longer tracked. imagine coins which get destroyed , they are not added to physics lists entered collision list and then not removed from it either. you simply can ignore them all and just destroy the coin in the first place and don't attach any component which tracks collisions frames or if it is in collision and just at the first query which returns it with a player destroy it and add to player health.

    So in the worst case you are not much better than the PhysX system when dealing with this but when your data and functionality allows then you are.

    And if you think about it the only way to know this is to keep state for it , the only difference is that you are free to keep the state or not if you don't need it.

    The point of DOTS is to remove the state and conditional checks and jumps in memory when they don't have to be there but otherwise they are perfectly fine if you simply need them to be able to execute your logic. where you can take shortcuts like for pickups, coins, death zones and .. great and where you don't need events like things detected by ray casting great again but if you need the events and only when specific entities with specific components are participating then somebody needs to do the check, even if we specified for the physics engine to only give me this event if the other entity had the component, then the physics engine had to check if the component in question is on the entity or not since usually it only goes through the list of components with shapes using its high performance structures suited for that it is best to not jump while calculating that since it has to go through the shapes anyways no matter if somebody will be interested in them or not. if you are not totally interested then you disable collisions of the object which is another story and is obvious to you
     
  16. robert_g_moran

    robert_g_moran

    Joined:
    Mar 9, 2016
    Posts:
    1
    In order to implement this approach, how do you get the frame number within a JobComponentSystem class, as you cannot reference Time.frameCount?