Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Resolved Best way to associate Entity data with a RaycastHit inside a job?

Discussion in 'Physics for ECS' started by Interjection, Nov 3, 2020.

  1. Interjection

    Interjection

    Joined:
    Jun 18, 2020
    Posts:
    63
    In my game I have a lot of raycasts going on to determine vision, so these raycasts are placed inside a burst-compiled job running for several frames.

    When a vision raycast collides with something I need to determine how much the vision gets reduced by that collider (how much shorter the ray becomes). The reduction depends on three things: Which team the ray gives vision to, which team the collider belongs to and how much the collider blocks vision (a base vision-block value).

    This was easy enough when I didn't involve jobs but now things are more difficult because I only have a clone of a Unity.Physics.CollisionWorld to work with + what the ray's Unity.Physics.RaycastHit provides: Unity.Physics.ColliderKey, Unity.Entities.Entity and Unity.Physics.RigidBody.CustomTags.

    I've determined that before the job starts I need to loop through all bodies in the Unity.Physics.CollisionWorld and associate all unique Unity.Entities.Entity with a team and a base vision-block value. Then store it in the job. Question is what's the best way to do this without slowing down the job a lot? There will be thousands of rays and there could be hundreds of colliders.

    My first thought was to have something like Unity.Collections.NativeList<MyStructName> in the job (the struct would have Entity + team + base vision-block value). But that means looping through a ton of entries for every ray hit. Outside the burst compiler I would use System.Collections.Generic.Dictionary to just look up the team and base vision-block value from the hit's Entity. I guess I could use Entity's Index in the NativeList but that would create a very large NativeList, taking up a lot of memory.

    I would check out how Unity.Entities.EntityManager's GetComponentData() looks up a Unity.Entities.IComponentData struct, but I couldn't find the source code.
     
    MNNoxMortem likes this.
  2. petarmHavok

    petarmHavok

    Joined:
    Nov 20, 2018
    Posts:
    461
    My initial idea was to simply "tag" those entities with component data that represents a team and the vision-block values. Then, you collect hits with your raycast job and store those entities in a container. After that, you just need to get component data for those entities whichever way you like.

    Alternatively, I suppose you could do the dictionary approach where each entity is mapped to a team/vision-block value. But don't loop through the bodies for sure, use IJobChunk or similar to iterate through all entities that have some data. Check BuildPhysicsWorld system, it does something like that. For example, if you want all bodies, simply schedule an IJobChunkJob for all entities that have a PhysicsCollider component.
     
  3. Interjection

    Interjection

    Joined:
    Jun 18, 2020
    Posts:
    63
    I can't simply store the raycast hits and then parse them on the main thread, the job needs to have access to that information so that it can finish determining if a spot has vision or not. The reason is because if a spot already got vision from a previous ray it will save other nearby "vision sources" from raycasting that spot, saving tens of thousands of raycasts from being cast (overlapping vision areas). Storing all the hits for later parsing would also take up a lot of memory, could be a hundred thousand hits if no raycast is skipped and no collider filtered.

    Do you know where I could find a code example of what you mention with IJobChunk/IJobChunkJob? Although it doesn't sound too tempting to use since I have the "team" and "base vision-block value" stored outside the Entity system, I'd rather not store that in several places and make sure it's synced.

    After transferring the IJobChunk stuff to a NativeList, how would I best go about finding the correct data from the Entity struct stored in the RaycastHit? Is iterating through the thing my only option?

    I'm leaning towards having several NativeLists and using a modulo operation on the Entity Index to determine which one to iterate through in search of the correct Entity information.
     
  4. petarmHavok

    petarmHavok

    Joined:
    Nov 20, 2018
    Posts:
    461
    Do you have a 1:1 mapping of bodies and entities? Or you have compound colliders (and therefore multiple entities per body)?

    Either way, you could use an array (where index is body index you get from raycast hit) and store there either a single entry (representing team and vision block) or a list of entities and their respective team and vision block data. A single raycast hit would give you the index to the array, and then it's either a constant time to get it, or a walk through a short list.

    Another thing you might find useful is the CustomTags field on RigidBody. It's a byte you can use for whatever you like. For example, you can use 1 bit to encode team and 7 bits to encode the block vision values (0-127). Or any other way you like.
     
    BigRookGames and florianhanke like this.
  5. Interjection

    Interjection

    Joined:
    Jun 18, 2020
    Posts:
    63
    Some bodies will make no entities, others will make several entities.

    The simplest solution would be to use CustomTags but 1 byte is too little for me to work with (it's not just two teams and I want to be able to support more stuff for vision blocking, like for example blocking more or less for certain teams). The first time I saw CustomTags I hoped it would be a long. But I guess that would have added too much needless overhead when not used most of the time.

    The reason I don't want to use the body index directly is because I think it could require a lot of memory. If one body index is 5 and another body index is 55000 the native array would need to have a length of 54995 just for those two bodies if I subtract the index I want to look up by the smallest known index. Maybe it's not as big of a deal that I think it could be, maybe there are no "empty"/unused RigidBodyIndexes so it won't ever be 55000 for just a few bodies still in use.

    I think I know what I must do now, there's no way around constructing a NativeList to look up stuff from. Thanks!
     
    petarmHavok likes this.
  6. PhilSA

    PhilSA

    Joined:
    Jul 11, 2013
    Posts:
    1,926
    I know you've mentioned that you "store team and base vision-block value outside of the Entities system" but I really feel like you're making your life difficult by doing so. A refactor might very much be worth it at this point, because it would allow the ECS to solve that kind of issue for you in a very efficient manner. (And it would also make your code use more "standard" practices as well, which is arguably another benefit)

    Basically, whenever you want some data that's associated with an entity (such as "team" and "vision-block value" in your case), that's pretty much literally what Components are meant to be used for

    Refactoring things the ECS way would allow you to do something like this:

    Code (CSharp):
    1.     public struct Team : IComponentData
    2.     {
    3.         public int TeamIndex;
    4.     }
    5.  
    6.     public struct VisionObstructer : IComponentData
    7.     {
    8.         public float ObstructionFactor;
    9.     }
    10.  
    11.     public struct MyVisionJob : IJobParallelFor
    12.     {
    13.         [ReadOnly]
    14.         public CollisionWorld CollisionWorld;
    15.  
    16.         public NativeList<VisionRaycastDefinition> VisionRaycasts;
    17.  
    18.         [ReadOnly]
    19.         public ComponentDataFromEntity<Team> TeamFromEntity;
    20.         [ReadOnly]
    21.         public ComponentDataFromEntity<VisionObstructer> VisionObstructerFromEntity;
    22.  
    23.         public void Execute(int index)
    24.         {
    25.             VisionRaycastDefinition raycastDefinition = VisionRaycasts[index];
    26.             if (CollisionWorld.CastRay(raycastDefinition.raycastParams, out ColliderCastHit closestHit))
    27.             {
    28.                 Entity hitEntity = closestHit.Entity;
    29.  
    30.                 float hitVisionObstructionFactor = 0f;
    31.  
    32.                 // Try to see if this entity has vision obstruction
    33.                 if (VisionObstructerFromEntity.HasComponent(hitEntity))
    34.                 {
    35.                     hitVisionObstructionFactor = VisionObstructerFromEntity[hitEntity].ObstructionFactor;
    36.                 }
    37.  
    38.                 // Reduce vision obstruction depending on team
    39.                 if (TeamFromEntity.HasComponent(hitEntity))
    40.                 {
    41.                     if (TeamFromEntity[hitEntity].TeamIndex == raycastDefinition.team)
    42.                     {
    43.                         hitVisionObstructionFactor *= 0.5f;
    44.                     }
    45.                 }
    46.  
    47.                 // Etc.... (do something with the final hitVisionObstructionFactor value)
    48.             }
    49.         }
    50.     }
    51.  
    But if you have reasons for not storing that data as components, I'd be curious to hear them. Maybe there are ways around the issues you've found. For example, if you need a notion of "Team" outside of entities as well, you can still re-use your "Team" component struct as a regular struct outside of the ECS
     
    Last edited: Nov 5, 2020
    MNNoxMortem, Onigiri and petarmHavok like this.
  7. MNNoxMortem

    MNNoxMortem

    Joined:
    Sep 11, 2016
    Posts:
    723
    Wait. That works? This is exactly what I am looking for at the moment. Do you have any example code to share on that order, in particular:
    - How to you get the copy of the CollisionWorld?
    - Are you using SystemBase, Entities.ForEach or IJobParallelFor?
    - If you are using Systems, which dependencies are you using?
    - Are you using Injection features or manually set references or aquire them?
    - How do keep long running jobs without Unity complaining?