Search Unity

Getting the hit Entity from RaycastHits on CompoundColliders: Best Practices?

Discussion in 'Physics for ECS' started by florianhanke, Jul 1, 2020.

  1. florianhanke

    florianhanke

    Joined:
    Jun 8, 2018
    Posts:
    426
    Hi all,

    I have the following situation where I have a hierarchy of entities with physics shapes. I'm wondering if there are other people out there with a similar situation, and if so, how we'd best handle the following.
    spaceship.jpg Let's say it's a space ship with differently armored areas (entities). Each of these entities has an Armor component with e.g. thickness of steel, or shield energy.

    What happens when we shoot laser beams at it? Currently, when a compound collider is hit, the returned entity is the root one, with the physics body. For the physics simulation, that makes sense, and is what is usually needed when colliders collide.
    In the ECS paradigm, and especially when shooting raycasts at it, this result is surprising to me. The hit should return the specific collider and entity – the compound collider is an internal thing, and what I have are entities and physics shapes, so I'd expect to get the specific collider and entity, so I can extract e.g. Armor information of that entity.

    Getting collider and entity would be optimal for this case. However, as it stands, only the ColliderKey is returned, which uniquely identifies the collider in the compound collider.
    So what can we do to get the entity? @steveeHavok has already helped a lot here: https://forum.unity.com/threads/eas...t-hit-colliderkey-on-compoundcollider.829509/

    I've written a cached version that works for me (note that when an entity is destroyed it is not removed from the cache, since that is a case I do not have):
    Code (CSharp):
    1. using Unity.Collections;
    2. using Unity.Entities;
    3. using Unity.Mathematics;
    4. using Unity.Physics;
    5. using Unity.Transforms;
    6.  
    7. public static class Collisions
    8. {
    9.     public static class NonJobified
    10.     {
    11.         // Note: Set capacity to what is needed.
    12.         public static NativeHashMap<long, int> mapping = new NativeHashMap<long, int>(64, Allocator.Persistent);
    13.  
    14.         public static void extractBelongsToAndHitEntity(
    15.             in  SystemBase     system,
    16.             in  CollisionWorld collisionWorld,
    17.             in  RaycastHit     hit,
    18.             out uint           belongsTo,
    19.             out Entity         hitEntity)
    20.         {
    21.             // Adapted from this thread:
    22.             // https://forum.unity.com/threads/easiest-way-to-get-hit-entity-from-raycast-hit-colliderkey-on-compoundcollider.829509/
    23.             var collider = collisionWorld.Bodies[hit.RigidBodyIndex].Collider;
    24.             if (collider.Value.Type == ColliderType.Compound)
    25.             {
    26.                 unsafe
    27.                 {
    28.                     // Extract the belongsTo.
    29.                     collider.Value.GetLeaf(hit.ColliderKey, out var leaf);
    30.  
    31.                     var leafCollider = *leaf.Collider;
    32.                     belongsTo = leafCollider.Filter.BelongsTo;
    33.  
    34.                     // Caching: fetch.
    35.                     var key = generateKey(hit.Entity, hit.ColliderKey);
    36.                     if (fetch(system, key, hit, out hitEntity))
    37.                     {
    38.                         return;
    39.                     }
    40.  
    41.                     // Set childEntity to the default.
    42.                     var childEntity = Entity.Null;
    43.  
    44.                     var childEntities = system.GetBufferFromEntity<Child>(true);
    45.                     if (childEntities.Exists(hit.Entity)) // The hit entity has children.
    46.                     {
    47.                         var translations    = system.GetComponentDataFromEntity<Translation>(true);
    48.                         var leafTranslation = leaf.TransformFromChild.pos;
    49.  
    50.                         if (Collisions.childEntity(
    51.                                                    childEntities,
    52.                                                    hit.Entity,
    53.                                                    translations,
    54.                                                    leafTranslation,
    55.                                                    float3.zero,
    56.                                                    out childEntity
    57.                                                   ))
    58.                         {
    59.                             // Caching: store.
    60.                             store(key, childEntity);
    61.                             hitEntity = childEntity;
    62.                             return;
    63.                         }
    64.                     }
    65.                 }
    66.             }
    67.  
    68.             belongsTo = CollisionFilter.Default.BelongsTo;
    69.             hitEntity = hit.Entity;
    70.         }
    71.  
    72.         private static bool fetch(SystemBase system, long key, RaycastHit hit, out Entity hitEntity)
    73.         {
    74.             if (mapping.TryGetValue(key, out var actuallyHitEntityHashCode))
    75.             {
    76.                 var linkedEntityGroup = system.GetBufferFromEntity<LinkedEntityGroup>(true);
    77.                 var linkedEntities    = linkedEntityGroup[hit.Entity];
    78.                 for (var i = 0; i < linkedEntities.Length; i++)
    79.                 {
    80.                     var linkedEntity         = linkedEntities[i];
    81.                     var potentiallyHitEntity = linkedEntity.Value;
    82.                     if (actuallyHitEntityHashCode == potentiallyHitEntity.GetHashCode())
    83.                     {
    84.                         hitEntity = potentiallyHitEntity;
    85.                         return true;
    86.                     }
    87.                 }
    88.             }
    89.  
    90.             hitEntity = Entity.Null;
    91.             return false;
    92.         }
    93.  
    94.         private static void store(long key, Entity childEntity)
    95.         {
    96.             if (!mapping.ContainsKey(key))
    97.             {
    98.                 mapping.AsParallelWriter().TryAdd(key, childEntity.GetHashCode());
    99.             }
    100.         }
    101.     }
    102.  
    103.     private static bool childEntity(
    104.         BufferFromEntity<Child> childEntities,   Entity parent, ComponentDataFromEntity<Translation> translations,
    105.         float3                  leafTranslation, float3 localTranslationOffset,
    106.         out Entity              foundEntity)
    107.     {
    108.         // Iterate over children and test for each whether it matches the leafTranslation.
    109.         var children = childEntities[parent];
    110.         for (int i = 0; i < children.Length; i++)
    111.         {
    112.             var child = children[i].Value;
    113.  
    114.             if (childEntities.Exists(child))
    115.             {
    116.                 // The child entity has children.
    117.                 if (childEntity(
    118.                                 childEntities,
    119.                                 child,
    120.                                 translations,
    121.                                 leafTranslation,
    122.                                 translations[child].Value,
    123.                                 out foundEntity
    124.                                ))
    125.                 {
    126.                     return true;
    127.                 }
    128.             }
    129.  
    130.             var childTranslation = translations[child].Value + localTranslationOffset;
    131.             if (allNearlyEqual(childTranslation, leafTranslation, 0.0001f))
    132.             {
    133.                 foundEntity = child;
    134.                 return true;
    135.             }
    136.         }
    137.  
    138.         foundEntity = Entity.Null;
    139.         return false;
    140.     }
    141.  
    142.     private static bool allNearlyEqual(float3 childTranslation, float3 leafTranslation, float epsilon)
    143.     {
    144.         return nearlyEqual(childTranslation.x, leafTranslation.x, epsilon) &&
    145.                nearlyEqual(childTranslation.y, leafTranslation.y, epsilon) &&
    146.                nearlyEqual(childTranslation.z, leafTranslation.z, epsilon);
    147.     }
    148.  
    149.     private static long generateKey(in Entity entity, in ColliderKey colliderKey)
    150.     {
    151.         var  hitEntityHashCode = entity.GetHashCode();
    152.         var  colliderKeyValue  = colliderKey.Value;
    153.         long key               = hitEntityHashCode;
    154.         key =  (key << 32);
    155.         key |= colliderKeyValue;
    156.         return key;
    157.     }
    158.  
    159.     public static bool nearlyEqual(float a, float b, float epsilon)
    160.     {
    161.         var absA = math.abs(a);
    162.         var absB = math.abs(b);
    163.         var diff = math.abs(a - b);
    164.  
    165.         if (a == b)
    166.         {
    167.             // shortcut, handles infinities
    168.             return true;
    169.         }
    170.  
    171.         if (a == 0 || b == 0 || absA + absB < 1.0)
    172.         {
    173.             // a or b is zero or both are extremely close to it
    174.             // relative error is less meaningful here
    175.             return diff < epsilon;
    176.         }
    177.  
    178.         // use relative error
    179.         return diff / (absA + absB) < epsilon;
    180.     }
    181. }
    The method
    extractBelongsToAndHitEntity
    yields the layer and the hit entity.
    Note that this is non-jobified – I have a jobified version that does not yet cache. Also: I'm a bit unsure why the allNearlyEqual is needed. I have a feeling the order of systems is not quite right, and PhysicsVelocity or so is apply some movement.

    In any case, I hope this perhaps helps somebody. What are other people out there doing? If you have something better suited for the task, we could collect it in this thread :)

    For me, I'd already be happy to get the ColliderKey -> Entity mapping when the compound collider is created, but I haven't been able to access the internals. A callback during that conversion would be great.
     
    Last edited: Jul 1, 2020
    NotaNaN and LudiKha like this.
  2. LudiKha

    LudiKha

    Joined:
    Feb 15, 2014
    Posts:
    140
    I too would like to be able to access the corresponding entity from a TriggerEvent/CollisionEvent.
     
    one_one, NotaNaN and florianhanke like this.
  3. florianhanke

    florianhanke

    Joined:
    Jun 8, 2018
    Posts:
    426
    Have you tried the code I posted? If so, I'd be interested to know of improvement ideas :)
     
    LudiKha likes this.
  4. Thygrrr

    Thygrrr

    Joined:
    Sep 23, 2013
    Posts:
    700
    The idea to identify entities by translation is pretty ok, but have you thought to use a DynamicBuffer on the parent entity (that holds the compound collider), the buffer elements being structs with an Entity field of course, and then using Custom Tags on the child colliders to index into the dynamic buffer?

    Could work for up to 256 child colliders, hey, that's a good homage to the original Quake. :D
     
    Last edited: Jul 10, 2020
  5. LudiKha

    LudiKha

    Joined:
    Feb 15, 2014
    Posts:
    140
    I haven't tried it, and I generally don't want to test collider Entities by their translation. There would be use cases where all child colliders would occupy the same local position, so I don't consider it a good solution for the problem.

    Either we need a way to access leaf collider entities, or during conversion be able to specify we don't want a child Entity collider to be part of the compound collider.

    This is interesting, but would be highly involved authoring-wise. And as far as I know there is no way to do this procedurally at the moment because of a lack of callback during the conversion step (which would be another solution I'd be OK with).
     
    one_one likes this.
  6. florianhanke

    florianhanke

    Joined:
    Jun 8, 2018
    Posts:
    426
    I'm not a fan either – to be fair, it's just a stop-gap measure provided by the Unity Physics team – and yes, the local translations must be pairwise distinct. It works for me, for now, but getting the ColliderKey and Entity during conversion (or a better solution) would be preferable.

    I agree. Either would be fine. Looking at the code, getting the actual hit entity or having them not be part of the CompoundCollider appears to be much harder than providing the ColliderKey -> Entity mapping during conversion.

    :)
    I hadn't thought about that. Since I automatically generate the GOs, that could work for me.

    Without having a callback that provides us with the ColliderKey -> Entity mapping, we could create it by mimicking the CompoundCollider creation process and iterate over the children before the process does it. (The collider key is a representation of the parent-child structure as a bit field – can't remember the details since I moved on shortly after implementing the translation code above)

    P.S: One thing the translation + lazy caching solution is good at is resources – it has a once-expensive cost when finding the Entity for the ColliderKey, but then is fast, and only uses the caching resources for hit Entities.
     
    Last edited: Jul 14, 2020
    profNoobland, NotaNaN and LudiKha like this.