Search Unity

Question Faster DOTS Physics Collisions

Discussion in 'Physics for ECS' started by EternalAmbiguity, Apr 11, 2021.

  1. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    I have a bullet system in my game that calls a trigger when colliding with another entity. The Trigger Job is below:

    Code (csharp):
    1.  
    2.     struct TriggerEventJob : ITriggerEventsJob
    3.     {
    4.         public EntityCommandBuffer.ParallelWriter cBuffer;
    5.         [NativeDisableParallelForRestriction] public ComponentDataFromEntity<WeaponEntity> weaponEntityInfo;
    6.         [NativeDisableParallelForRestriction] public ComponentDataFromEntity<Translation> translationInfo;
    7.         [NativeDisableParallelForRestriction] public ComponentDataFromEntity<PlayerEntity> playerEntityInfo;
    8.         [NativeDisableParallelForRestriction] public ComponentDataFromEntity<EnemyEntity> enemyEntityInfo;
    9.         [NativeDisableParallelForRestriction] public ComponentDataFromEntity<EnemyStructureEntity> enemyStructureEntityInfo;
    10.  
    11.         public void Execute(TriggerEvent collisionEvent)
    12.         {
    13.             CheckWeapon(collisionEvent.EntityA, collisionEvent.BodyIndexA);
    14.             CheckWeapon(collisionEvent.EntityB, collisionEvent.BodyIndexB);
    15.             CheckCollided(collisionEvent.EntityA, collisionEvent.EntityB);
    16.             CheckCollided(collisionEvent.EntityB, collisionEvent.EntityA);
    17.         }
    18.  
    19.         void CheckWeapon(Entity entity, int index)
    20.         {
    21.             if (weaponEntityInfo.HasComponent(entity))
    22.             {
    23.                 var cur = weaponEntityInfo[entity];
    24.                 if(cur.type == DamageType.Bullet)
    25.                 {
    26.                     var trans = translationInfo[entity];
    27.                     cBuffer.DestroyEntity(index, entity);
    28.                     StaticClass.Explode(cur.type, cur.element, trans);
    29.                 }
    30.                 else if (cur.type == DamageType.Mine)
    31.                 {
    32.                     cur.stop = true;
    33.                     cBuffer.SetComponent(entity.Index, entity, cur);
    34.                 }
    35.                 else if (cur.type == DamageType.Grenade)
    36.                 {
    37.                     //bouce...andled by physics?
    38.                 }
    39.             }
    40.             else
    41.             {
    42.                 //Debug.Log("weaponEntityInfo doesn't have entity");
    43.             }
    44.         }
    45.  
    46.         void CheckCollided(Entity a, Entity b)
    47.         {
    48.             if(enemyStructureEntityInfo.HasComponent(a) && weaponEntityInfo.HasComponent(b))
    49.             {
    50.                 var cur = weaponEntityInfo[b];
    51.                 StaticClass.damageEnemyStructure?.Invoke(a, cur.type, cur.element);
    52.                 UnityEngine.Debug.Log("Damage from " + b.Index + "-" + b.Version);
    53.             }
    54.         }
    55.     }
    56.  
    It's called in OnUpdate like so:

    Code (csharp):
    1.  
    2.         var commandBuffer = m_Barrier.CreateCommandBuffer().AsParallelWriter();
    3.         var weaponEntityInfo = GetComponentDataFromEntity<WeaponEntity>();
    4.         var translationInfo = GetComponentDataFromEntity<Translation>();
    5.         var playerEntityInfo = GetComponentDataFromEntity<PlayerEntity>();
    6.         var enemyEntityInfo = GetComponentDataFromEntity<EnemyEntity>();
    7.         var enemyStructureEntityInfo = GetComponentDataFromEntity<EnemyStructureEntity>();
    8.  
    9. // foreach handling other things
    10.  
    11.         Dependency = new TriggerEventJob {
    12.             cBuffer = commandBuffer,
    13.             weaponEntityInfo = weaponEntityInfo,
    14.             playerEntityInfo = playerEntityInfo,
    15.             enemyEntityInfo = enemyEntityInfo,
    16.             enemyStructureEntityInfo = enemyStructureEntityInfo,
    17.             translationInfo = translationInfo
    18.         }
    19.         .Schedule(m_StepPhysicsWorldSystem.Simulation, ref physicsWorldSystem.PhysicsWorld, Dependency);
    20.  
    The prefab from which it is derived is in the picture below.

    weaponBio.png

    unfortunately it seems slow - it's triggered 8 times for an entity it collides with. Is there any way to make this faster? I essentially only want it triggered once, so it only applies damage once.
     
  2. milos85miki

    milos85miki

    Joined:
    Nov 29, 2019
    Posts:
    197
    Can you use raycast for bullets? It should be much faster.
    If it must be a physics body (pls share why), does it have to live after the first contact with the first trigger or can you destroy the bullet entity?
     
  3. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    "Bullet" might be the wrong word, they don't fire instantaneously. See my post from Feedback Friday (third GIF has firing at the beginning):

    https://forum.unity.com/threads/feedback-friday-160-april-9-12-2021.1090372/#post-7023100

    It doesn't happen here, but it's possible for an enemy and the player to fire at one another and their "bullets" will collide. So it needs to be "real."

    I'd love to destroy the bullet directly after the first trigger - that's what I'm trying to do on line 27 in the first code block. But it still takes 8 frames for that to happen.
     
  4. milos85miki

    milos85miki

    Joined:
    Nov 29, 2019
    Posts:
    197
  5. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Okay thanks. It looks like there are a lot of different timings for the ECB - the default seems to be EndSimulationEntityCommandBufferSystem, but there's also BeginSimulationECBS or even BeginInitializationECBS. I might take a look at how those work.
     
  6. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    I tried out several different command buffers but none of them changed anything - the collision was still called 6-12 times even if the entity was deleted as soon as I determined it was of the appropriate type. I just made a change in the "damage" method to store the index and version of the last entity it was hit by, and skip if there was a match.

    Code (csharp):
    1.  
    2.                     if((ese.index == index && ese.version == version))
    3.                     {
    4.                         return;
    5.                     }
    6.                     ese.index = index;
    7.                     ese.version = version;
    This seems to work.
     
  7. milos85miki

    milos85miki

    Joined:
    Nov 29, 2019
    Posts:
    197
    Are those additional collisions from the same physics step?
    When you delete an Entity, the change will be picked up in next BuildPhysicsWorld run, so until that happens it is totally expected that you'll get other collisions. Sorry, I now realize I should have mentioned this right away.

    If it's not in the same physics step and ECB deleted the entity before next BuildPhysicsWorld had a chance to run, then it's suspicious (that entity should not be represented by a RigidBody in PhysicsWorld that was built for new step)...
     
  8. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    How would I determine that? Just a debug log in OnUpdate?
     
  9. milos85miki

    milos85miki

    Joined:
    Nov 29, 2019
    Posts:
    197
    You can do debug log or look at the profiler (use pause/step in editor to go step by step).
     
  10. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Just removed the piece I added to limit it based on index and put this in for the weapon trigger and OnUpdate:

    Code (csharp):
    1.  
    2. struct TriggerEventJob : ITriggerEventsJob
    3.     {
    4.         public EntityCommandBuffer.ParallelWriter cBuffer;
    5.         [NativeDisableParallelForRestriction] public ComponentDataFromEntity<WeaponEntity> weaponEntityInfo;
    6.         [NativeDisableParallelForRestriction] public ComponentDataFromEntity<Translation> translationInfo;
    7.         [NativeDisableParallelForRestriction] public ComponentDataFromEntity<PlayerEntity> playerEntityInfo;
    8.         [NativeDisableParallelForRestriction] public ComponentDataFromEntity<EnemyEntity> enemyEntityInfo;
    9.         [NativeDisableParallelForRestriction] public ComponentDataFromEntity<EnemyStructureEntity> enemyStructureEntityInfo;
    10.  
    11.         public void Execute(TriggerEvent collisionEvent)
    12.         {
    13.             CheckWeapon(collisionEvent.EntityA, collisionEvent.EntityB);
    14.             CheckWeapon(collisionEvent.EntityB, collisionEvent.EntityA);
    15.         }
    16.  
    17.         void CheckWeapon(Entity A, Entity B)
    18.         {
    19.             if (weaponEntityInfo.HasComponent(A))
    20.             {
    21.                 var cur = weaponEntityInfo[A];
    22.                 if(cur.type == DamageType.Bullet)
    23.                 {
    24.                     var type = cur.type;
    25.                     var element = cur.element;
    26.                     var trans = translationInfo[A];
    27.                     cBuffer.DestroyEntity(A.Index, A);
    28.                     StaticClass.Explode(type, element, trans);
    29.                     CheckCollided(B, type, element, A.Index, A.Version);
    30.                 }
    31.                 else if (cur.type == DamageType.Mine)
    32.                 {
    33.                     cur.stop = true;
    34.                     cBuffer.SetComponent(A.Index, A, cur);
    35.                 }
    36.                 else if (cur.type == DamageType.Grenade)
    37.                 {
    38.                     //bouce...andled by physics?
    39.                 }
    40.             }
    41.         }
    42.  
    43.         void CheckCollided(Entity B, DamageType type, DamageElement element, int index, int version)
    44.         {
    45.             if(enemyStructureEntityInfo.HasComponent(B))
    46.             {
    47.                 StaticClass.damageEnemyStructure?.Invoke(B, type, element, index, version);
    48.                 Debug.Log("hit by " + index + "-" + version);
    49.             }
    50.         }
    51.     }
    52.  
    53.     protected override void OnUpdate()
    54.     {
    55.         Debug.Log("Alive");
    56.         var commandBuffer = m_Barrier.CreateCommandBuffer().AsParallelWriter();
    57.         var weaponEntityInfo = GetComponentDataFromEntity<WeaponEntity>();
    58.         var translationInfo = GetComponentDataFromEntity<Translation>();
    59.         var playerEntityInfo = GetComponentDataFromEntity<PlayerEntity>();
    60.         var enemyEntityInfo = GetComponentDataFromEntity<EnemyEntity>();
    61.         var enemyStructureEntityInfo = GetComponentDataFromEntity<EnemyStructureEntity>();
    62.         var deltaTime = Time.DeltaTime;
    63.         var physicsWorld = physicsWorldSystem.PhysicsWorld;
    64.         var collisionWorld = physicsWorld.CollisionWorld;
    65.         var perimeter = new NativeQueue<Entity>(Allocator.TempJob);
    66.         var perimeterParallel = perimeter.AsParallelWriter();
    67.         Entities
    68.             .WithAll<WeaponEntity, PhysicsGravityFactor>() // can this be anything else in theory?
    69.             .WithBurst(Unity.Burst.FloatMode.Default, Unity.Burst.FloatPrecision.Standard, true)
    70.             .ForEach((Entity entity, int nativeThreadIndex, /*in Translation translation,*/ ref WeaponEntity weaponEntity, ref PhysicsVelocity velocity, ref PhysicsGravityFactor gravity, ref Translation translation, in LocalToWorld localToWorld/*, in WorldRenderBounds bounds, in PhysicsCollider collider*/) =>
    71.             {
    72.                 if (weaponEntity.stop)
    73.                 {
    74.                     velocity.Linear = new float3();
    75.                     velocity.Angular = new float3();
    76.                     gravity.Value = 0;
    77.                 }
    78.                 // if mine, check perimeter
    79.                 if(weaponEntity.type == DamageType.Mine)
    80.                 {
    81.                     perimeterParallel.Enqueue(entity);
    82.                 }
    83.             })
    84.             .ScheduleParallel();
    85.         Dependency = new TriggerEventJob {
    86.             cBuffer = commandBuffer,
    87.             weaponEntityInfo = weaponEntityInfo,
    88.             playerEntityInfo = playerEntityInfo,
    89.             enemyEntityInfo = enemyEntityInfo,
    90.             enemyStructureEntityInfo = enemyStructureEntityInfo,
    91.             translationInfo = translationInfo
    92.         }
    93.         .Schedule(m_StepPhysicsWorldSystem.Simulation, ref physicsWorldSystem.PhysicsWorld, Dependency);
    94.         m_Barrier.AddJobHandleForProducer(Dependency);
    95.         Dependency.Complete();
    96.         for (int i = 0; i < perimeter.Count; i++)
    97.         {
    98.             StaticClass.CheckMine(perimeter.Dequeue());
    99.         }
    100.         //cellWalkable0.Dispose();
    101.         perimeter.Dispose();
    102.     }
    Output was as the figure shows.
    output.png
    Looks like the trigger happens, then there are a few frames, then it happens again, then there's one frame, then it happens again, all for the same entity.
     
  11. milos85miki

    milos85miki

    Joined:
    Nov 29, 2019
    Posts:
    197
    As far as I know, after you destroy an Entity it should not appear again with the same index and version (tuple index, version is Entity's unique id).
    Just guessing without having a chance to read the code more carefully - is that maybe the same enemy hit by different bullets (or some other object came close enough to that enemy)?
     
  12. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    My Debug.Log is printing out the index and version of the thing that hits my structure (the bullet, with the collision detection job). It's outputting the same index and version, which means (going by your first sentence) it has to be the same entity.

    And there's the fact that I get it to work correctly (only one hit) by storing and checking against both the index and version of whatever-entity-hit-the-structure.
     
  13. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    Does Unity.Physics support continuous collision detection?

    In any case unless you need some dynamic bouncing around of bullets after collision, you could probably do something like the following.

    Formula for speed and effect of gravity over distance.

    Per frame you calculate movement. Take all bullets that could possibly hit each other, ones that are closer then the max movement in that frame.

    Take the distance traveled and divide by bullet size to get N steps.

    Then for bullets close enough to hit, step them all by their size for N steps. At each step check distance to every other bullet.

    And then once per frame per bullet raycast from last to current position for checking everything else.
     
  14. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    This seems kind of expensive for potentially dozens of bullets active at once, but that's just a guess.

    I'm not really concerned about multiple-triggers-and-not-immediate-entity-deletion for situations where bullets are colliding, because they'll just destroy each other, that's not going to affect the things around them. It's the situations where bullets are hitting other entities in the game, like structures or enemies (or the player).
     
  15. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    Without bullet to bullet collisions it's more performant then dynamic bodies and events. We do that part with upwards of 10k or so projectiles in flight.

    I see physics does have speculative ccd so dynamics should work fine. I would verify that though because this is an edge case and if it doesn't work as intended, you end up refactoring everything.

    Bullets move on two axis. So my solution above seems rather naive now that I think about it. I think you can likely use some straight forward line intersection tests and falloff over time math and know up front what bullets will colilde with others and where. Which might be more work then is worth it of course, unless dynamics doesn't work out for some reason.
     
  16. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Okay, I'll take a look at that when I get the chance. That does seem like a nontrivial amount of work for something the physics system out to handle, but oh well.
     
  17. milos85miki

    milos85miki

    Joined:
    Nov 29, 2019
    Posts:
    197
    Hi @EternalAmbiguity, just to re-confirm, you've seen the bullet Entity get destroyed (it's not rendered and not listed in Entities debugger) and then re-appearing with the same index and version later?
    Just trying to figure out if it maybe didn't get destroyed at all, since that would explain the behavior you're seeing.
    Entity debugger window (under window -> analysis), profiler (ctrl + 7), Physics Debug Display component and stepping the player frame by frame are very useful in these cases.

    If that's feasible for you, please feel free to create a minimal repro project and send it to me for debugging.
     
  18. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Unfortunately I've had issues with trying to pause and resume the game, it tends to break other things, so I can't run through it live frame by frame or anything. I have not visually seen the bullet get destroyed and then continue to trigger; the thing I have is my ECB command to destroy the entity which gets called every time a collision is triggered, but as can be seen in my screenshot with console output there the bullet persists for a frame or two (not to mention the multiple triggers during the same frame).

    I looked at the Physics component in the debugger but the only thing it showed was static colliders, which doesn't appear to drop even when the structure is destroyed (it does NOT show the creation and destruction of the bullets themselves). So basically nothing. Is there a setting somewhere to show entity stuff in the physics debugger? I have the "Physics Debug" window in Analysis as well, but that doesn't appear to be global and seems to require that one select an individual object to see anything about it (so it can't show previous frames).

    If I get the chance I'll try a repro in a new project and send that to you.
     
  19. milos85miki

    milos85miki

    Joined:
    Nov 29, 2019
    Posts:
    197
    When Entity is destroyed, it should disappear from Entity Debugger window. Physics Debug Display component needs to be added to some object on the scene and it enables drawing of colliders, event data and other useful physics stuff.

    I suspect that ECB usage is the culprit here.
     
  20. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    I tried creating a new project and reproducing the situation but it didn't really occur. I think this is the result of my game's high CPU usage and ECB as you said. My game has a lot of extra entities as well as multiple standard C# threads running things, so there's more going on that I can easily reproduce.

    I'd be more than willing to share the full project, but it's somewhat unwieldy and I'd understand if that sounds undesirable.
     
  21. papopov

    papopov

    Joined:
    Jun 29, 2020
    Posts:
    32
    Hey, I think I have an idea what's wrong:
    Physics systems run in FixedStepSimulationSystemsGroup, while your command buffer runs in from a system in SimulationSystemGroup (EndSimulationEntityCommandBufferSystem it is called I think). So what could happen here, is that Physics could execute multiple times before EndSimulationEntityCommandBufferSystem is executed, which is supposed to execute your buffer's actions. You could try initializing entityCommandBuffer from EndFixedStepSimulationEntityCommandBufferSystem, that way Physics and buffers will be in sync. Here's a code snippet how would you do it:

    Code (CSharp):
    1.  
    2. OnUpdate()
    3. {
    4.     var system = World.GetOrCreateSystem<EndFixedStepSimulationEntityCommandBufferSystem>();
    5.     var comandBuffer = system.CreateCommandBuffer().AsParallelWriter();
    6.     var jobHandle  =  ... ;// Schedule your trigger jobs here
    7.     system.AddJobHandleForProducer(jobHandle);
    8.    
    9. }
    Can you try this and check if it works?
     
    EternalAmbiguity likes this.
  22. EternalAmbiguity

    EternalAmbiguity

    Joined:
    Dec 27, 2014
    Posts:
    3,144
    Looks like that works. I'm not getting any frames in between when the collisiion's triggered and the entity actually being destroyed.

    I'm still getting about 8 hits on that frame, but that may be related to all that I have going on in my scene and not ECS itself.
     
    papopov likes this.
  23. papopov

    papopov

    Joined:
    Jun 29, 2020
    Posts:
    32
    Glad it worked!

    Getting 8 hits on that frame can happen, if physics steps 8 times per one frame, which can happen because it updates in FixedStepSimulationSystemsGroup.
    Here's some explanation how all that works: Unity - Fixed timestep features coming to Entities / DOTS Physics packages - Unity Forum
     
    milos85miki and EternalAmbiguity like this.