Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Resolved Access static helper function of System from Aspect - a.k.a. how to access data of a system

Discussion in 'Entity Component System' started by Rubenz, Jun 13, 2023.

  1. Rubenz

    Rubenz

    Joined:
    Feb 2, 2017
    Posts:
    8
    I want to implement collision avoidance between my enemies. To that end, I needed to know which enemies are close to each other and for that I implemented a QuadrantSystem which takes all enemies and inserts them in a NativeParallelMultiHashMap<int, QuadrantData> according to their position. I make this map available as a static variable like QuadrantSystem.EntitiesByQuadrant.

    I have no problem accessing the map in the OnUpdate() of my MoveEnemiesSystem which is called after the QuadrantSystem and hand it over to the MoveEnemyJob.

    The Enemies have an Aspect associated with them, the EnemyMoveAspect, which has a Move function that manages most of the heavy lifting for MoveEnemyJob.

    What I would like to do, is to get a list from within that EnemyMoveJob.Move() which has all the entities close to the one the job is currently working on, I thought I would use NativeArray<QuadrantData>. I tried to generate such an array from the EnemyMoveAspect like:
    Code (CSharp):
    1. NativeArray<QuadrantData> nearbyEntities = QuadrantSystem.GetNearbyEntities(EntitiesByQuadrant, Position);
    That makes the EnemyMoveJob.Move() not burst compilable, since I am returning a struct from an external function. I could hand over the return value as an out parameter instead, but the whole thing feels like I am missing something, it all feels to hacky for something that is probably a common problem.

    So my question would be, where do I put the helper functions for getting the nearby entities for an entity? I could just put them in the EnemyMoveAspect, but it does not feel like they belong there. Should I just hand over the NativeArray<QuadrantData> as an out parameter?

    Or do I get something more fundamentally wrong about how to properly access the data from QuadrantSystem? Is there a smart way, maybe some form of inheritance to not mark the helper functions as external for the sake of burst compiling?

    Basically, does anyone have any pointers on how to properly structure so that I can access the data in a readable way. It just feels like I am missing something.

    Thanks for your help!

    Error in Unity

    EnemyMoveAspect.cs
    Code (CSharp):
    1. using Assets.Scripts.ECS.Components;
    2. using Assets.Scripts.ECS.Systems;
    3. using Unity.Collections;
    4. using Unity.Entities;
    5. using Unity.Mathematics;
    6. using Unity.Transforms;
    7. using UnityEngine;
    8.  
    9. public readonly partial struct EnemyMoveAspect : IAspect
    10. {
    11.     public readonly Entity Entity;
    12.  
    13.     private readonly RefRW<LocalTransform> _transform;
    14.     private readonly RefRO<MoveSpeed> _moveSpeed;
    15.  
    16.     public float3 Position
    17.     {
    18.         get => _transform.ValueRO.Position;
    19.         set => _transform.ValueRW.Position = value;
    20.     }
    21.  
    22.     public float MoveSpeed
    23.     {
    24.         get => _moveSpeed.ValueRO.Value;
    25.     }
    26.  
    27.     public void Move(float3 playerPosition, NativeParallelMultiHashMap<int, QuadrantData>.ReadOnly EntitiesByQuadrant,  float deltatime)
    28.     {
    29.         NativeArray<QuadrantData> nearbyEntities = QuadrantSystem.GetNearbyEntities(EntitiesByQuadrant, Position);
    30.  
    31.         foreach (QuadrantData nearbyEntity in nearbyEntities)
    32.         {
    33.             Debug.Log($"Entity at {nearbyEntity.Position}\n");
    34.         }
    35.     }
    36. }

    QuadrantSystem.cs
    Code (CSharp):
    1.  
    2. [BurstCompile]
    3. public struct QuadrantData
    4. {
    5.     public Entity Entity;
    6.     public float3 Position;
    7. }
    8.  
    9. [BurstCompile]
    10. [UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
    11. [UpdateAfter(typeof(InputSystem))]
    12. public partial struct QuadrantSystem : ISystem
    13. {
    14.     public static NativeParallelMultiHashMap<int, QuadrantData> EntitiesByQuadrant;
    15.  
    16.     [BurstCompile]
    17.     public static NativeArray<QuadrantData> GetNearbyEntities(in NativeParallelMultiHashMap<int, QuadrantData>.ReadOnly entitiesByQuadrant, in float3 position)
    18.     {
    19.         NativeArray<NativeArray<QuadrantData>> nearbyEntities = new();
    20.         int index = 0;
    21.  
    22.         int keyOfPosition = GetHash(position);
    23.  
    24.         for (int x = -1; x <= 1; x++)
    25.         {
    26.             for (int z = -1; z <= 1; z++)
    27.             {
    28.                 nearbyEntities[index++] = GetNearbyEntitiesAtKey(entitiesByQuadrant, keyOfPosition + x + (QUADRANT_NUM_X_AXIS * z), position);
    29.             }
    30.         }
    31.  
    32.         NativeArray<QuadrantData> result = new();
    33.  
    34.         for (int i = 0; i < 9; i++)
    35.         {
    36.             result.Concat(nearbyEntities[i]);
    37.         }
    38.  
    39.         return result;
    40.     }
    41.  
    42.     // Some more functions
    43. }
    44.  
    EnemyMoveSystem.cs
    Code (CSharp):
    1. [BurstCompile]
    2. [UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
    3. [UpdateAfter(typeof(QuadrantSystem))]
    4. public partial struct EnemyMoveSystem : ISystem
    5. {
    6.     [BurstCompile]
    7.     public void OnCreate(ref SystemState state)
    8.     {
    9.     }
    10.  
    11.     [BurstCompile]
    12.     public void OnDestroy(ref SystemState state)
    13.     {
    14.     }
    15.  
    16.     public void OnUpdate(ref SystemState state)
    17.     {
    18.         var deltaTime = SystemAPI.Time.DeltaTime;
    19.         Entity player = SystemAPI.GetSingletonEntity<PlayerTag>();
    20.         float3 playerPosition = SystemAPI.GetComponentRO<LocalTransform>(player).ValueRO.Position;
    21.  
    22.         new EnemyMoveJob
    23.         {
    24.             DeltaTime = deltaTime,
    25.             PlayerPosition = playerPosition,
    26.             EntitiesByQuadrant = QuadrantSystem.EntitiesByQuadrant.AsReadOnly(),
    27.         }.ScheduleParallel();
    28.     }
    29. }
    30.  
    31. [BurstCompile]
    32. public partial struct EnemyMoveJob : IJobEntity
    33. {
    34.     public float DeltaTime;
    35.     public float3 PlayerPosition;
    36.     public NativeParallelMultiHashMap<int, QuadrantData>.ReadOnly EntitiesByQuadrant;
    37.  
    38.     [BurstCompile]
    39.     private void Execute(EnemyMoveAspect moveAspect)
    40.     {
    41.         moveAspect.Move(PlayerPosition, EntitiesByQuadrant, DeltaTime);
    42.     }
    43. }
     
    Last edited: Jun 13, 2023
  2. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,292
    Burst has limitations on what can be returned and what cannot be.
    Try returning collection (NativeArray) as ref (allocate before passing to the method GetNearbyEntities in aspect) or out.

    As of where to put it - best place is IJob*Something* struct.
    So that the entire job scheduled can be re-used in other systems too without coupling systems together.
    Otherwise system or aspect is fine too.
     
    Last edited: Jun 14, 2023
    Rubenz likes this.
  3. Rubenz

    Rubenz

    Joined:
    Feb 2, 2017
    Posts:
    8
    Thanks @xVergilx! I like the idea of using a series of jobs a lot.

    I tried implementing it, but I have problems chaining two IJobEntity jobs. The job that will replace GetNearbyEntities needs to be an IJobEntity from my understanding, since I need to generate a list of nearby entities for every entity. The EnemyMoveJob needs to be an IJobEntity as well, since I need to move all enemies.

    The question is, how can I get the NativeArray<QuadrantData> containing the nearby entities from the output of the first job as input for the second job for the same entity? Is this even possible?

    I could call the GetNearbyEntitiesJob form within the EnemyMoveJob, but I remember reading that scheduling a job from within a job isn't a good idea and I dislike using Complete().

    I suppose I can just return a map which holds all the nearby entities for every entity from the GetNearbyEntitiesJob but then every EnemyMoveJob will have a map of <Entity,NativeArray<QuadrantData>> pairs instead of just the NativeArray<QuadrantData> that it actually needs. That doesn't feel great either.

    Another option I see, is to add a NativeArray<QuadrantData> component to every Entity that needs it, but I read that collections are best reserved for singleton entities.

    So yeah, I'm still a bit at a loss. I will update, if I find something that works for me.

    Update:
    I thought about it again and decided that it is probably fine to hand over the EntityMoveJob more data than it needs. My solution follows, I am still very thankful for any feedback to my solution and any pointers for improvement.

    OnUpdate in EnemyMoveSystem
    Code (CSharp):
    1. public void OnUpdate(ref SystemState state)
    2. {
    3.     var deltaTime = SystemAPI.Time.DeltaTime;
    4.     Entity player = SystemAPI.GetSingletonEntity<PlayerTag>();
    5.     float3 playerPosition = SystemAPI.GetComponentRO<LocalTransform>(player).ValueRO.Position;
    6.     NativeHashMap<Entity, NativeArray<QuadrantData>> NearbyEntities
    7.         = new(QuadrantSystem.EntitiesByQuadrant.Capacity, state.WorldUpdateAllocator);
    8.  
    9.     JobHandle job = new QuadrantSystem.GetNearbyEntitiesJob
    10.     {
    11.         EntitiesPerQuadrant = QuadrantSystem.EntitiesByQuadrant.AsReadOnly(),
    12.         NearbyEntities = NearbyEntities
    13.     }.ScheduleParallel(state.Dependency);
    14.  
    15.     new EnemyMoveJob
    16.     {
    17.         DeltaTime = deltaTime,
    18.         PlayerPosition = playerPosition,
    19.         NearbyEntities = NearbyEntities.AsReadOnly(),
    20.     }.ScheduleParallel(job);
    21. }
    GetNearbyEntitiesJob
    Code (CSharp):
    1. [BurstCompile]
    2. public partial struct GetNearbyEntitiesJob : IJobEntity
    3. {
    4.     public NativeParallelMultiHashMap<int, QuadrantData>.ReadOnly EntitiesPerQuadrant;
    5.     public NativeHashMap<Entity, NativeArray<QuadrantData>> NearbyEntities;
    6.  
    7.     [BurstCompile]
    8.     private void Execute(Entity entity, LocalTransform localTransform)
    9.     {
    10.         NearbyEntities[entity] = GetNearbyEntities(EntitiesPerQuadrant, localTransform.Position);
    11.     }
    12. }
    EnemyMoveJob
    Code (CSharp):
    1. [BurstCompile]
    2. public partial struct EnemyMoveJob : IJobEntity
    3. {
    4.     public float DeltaTime;
    5.     public float3 PlayerPosition;
    6.     public NativeHashMap<Entity, NativeArray<QuadrantData>>.ReadOnly NearbyEntities;
    7.  
    8.     [BurstCompile]
    9.     private void Execute(EnemyMoveAspect moveAspect)
    10.     {
    11.         moveAspect.Move(PlayerPosition, NearbyEntities[moveAspect.Entity].AsReadOnly(), DeltaTime);      
    12.     }
    13. }
     
    Last edited: Jun 14, 2023
  4. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,292
    You could map it like that.
    For this case it makes more sense to make a separate system that detects nearby entities.
    And stores results directly in some kind of buffer.

    E.g.
    Code (CSharp):
    1. [InternalBufferCapacity(0)]
    2. public struct NearbyEntityInQuadrant : IBufferElementData {
    3.     public Entity Entity;
    4.     // Extra data etc;
    5. }
    This way logic is competely decoupled and chained automatically.
    + Less code;
    + You'll be able to query over it in different other systems that may require nearby data without re-running extra job each time it is needed;

    Chaining was a bad example for this case because I haven't looked into context.
    Systems do it automatically but its more of a general way of thinking when processing data.
    Layout data as a "conveyour" so that previous data state is prepared before next one requires it.
    Proactive processing rather than reactive. If that makes sense.
     
    Last edited: Jun 14, 2023
    Rubenz likes this.
  5. Rubenz

    Rubenz

    Joined:
    Feb 2, 2017
    Posts:
    8
    That makes a ton of sense! Thank you again!

    And I like the approach to use a buffer, the only problem is that every entity has different nearby entities, so I would either need to add a buffer to every entity or I would need for my IBufferElementData to hold a NativeArray of entities, if I want to just have one buffer.

    Which one would you recommend?

    I will try to implement a solution with buffers and will update once I did.

    Update:
    During the baking process for my enemies I added a DynamicBuffer<EntitiesTransformBufferElement> to them.
    I also added a new system FillNearbyEntitiesBufferSystem which runs after the QuadrantSystem and before the EnemyMoveSystem which fills said dynamic buffer for every enemy with the enemies this enemy considers closeby. I added that buffer to the EnemyMoveAspect, so that during the Move function on that aspect I know all the nearby enemies. With this I can implement a simple flocking algorithm on them.

    I am quite happy with this solution, but am as always appreciative of any further input that makes my code better readable, more performant or better maintainable.


    EntitiesTransformBufferElement
    Code (CSharp):
    1. [InternalBufferCapacity(50)]
    2. public struct EntitiesTransformBufferElement : IBufferElementData
    3. {
    4.     public Entity Entity;
    5.     public LocalTransform Transform;
    6. }
    FillNearbyEntitiesBufferSystem
    Code (CSharp):
    1. [BurstCompile]
    2. [UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
    3. [UpdateAfter(typeof(QuadrantSystem))]
    4. public partial struct FillNearbyEntitiesBufferSystem : ISystem
    5. {
    6.  
    7.     public void OnUpdate(ref SystemState state)
    8.     {
    9.         new FillNearbyEntitiesBufferJob
    10.         {
    11.             EntitiesByQuadrant = QuadrantSystem.EntitiesByQuadrant.AsReadOnly(),
    12.             DistanceCutoff = 1
    13.         }.ScheduleParallel();
    14.     }
    15.  
    16.     [BurstCompile]
    17.     public partial struct FillNearbyEntitiesBufferJob : IJobEntity
    18.     {
    19.         public NativeParallelMultiHashMap<int, EntitiesTransformBufferElement>.ReadOnly EntitiesByQuadrant;
    20.         public int DistanceCutoff;
    21.  
    22.         [BurstCompile]
    23.         private void Execute(Entity entity, DynamicBuffer<EntitiesTransformBufferElement> nearbyEntities, LocalTransform localTransform)
    24.         {
    25.             NativeParallelMultiHashMapIterator<int> hashMapIterator;
    26.             int keyAtEntity = QuadrantUtils.GetHash(localTransform.Position);
    27.  
    28.             nearbyEntities.Clear();
    29.  
    30.             for (int x = -DistanceCutoff; x <= DistanceCutoff; x++)
    31.             {
    32.                 for (int z = -DistanceCutoff; z <= DistanceCutoff; z++)
    33.                 {
    34.                     int keyAtNearby = keyAtEntity + x + (QuadrantUtils.QUADRANT_NUM_X_AXIS * z);
    35.                     if (EntitiesByQuadrant.TryGetFirstValue(keyAtNearby, out EntitiesTransformBufferElement entitiesBufferElement, out hashMapIterator))
    36.                     {
    37.                         do
    38.                         {
    39.                             if (!entitiesBufferElement.Transform.Equals(localTransform))
    40.                             {
    41.                                 nearbyEntities.Add(entitiesBufferElement);
    42.                             }
    43.  
    44.                         } while (EntitiesByQuadrant.TryGetNextValue(out entitiesBufferElement, ref hashMapIterator));
    45.                     }
    46.                 }
    47.             }
    48.         }
    49.     }
    50. }
     
    Last edited: Jun 14, 2023
  6. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,292
    Keep buffer on each entity (or entities that should detect nearby entities).
    Can't attach native collections to entities directly (that will crash outside of editor).
    Unless unsafe collections. But they have their own caveats.

    InternalBufferCapacity 0 moves buffer outside the chunk, so it doesn't waste any chunk memory. (Except for the reference to the allocated memory)

    And you can keep as many nearby entities as needed. Chunk memory is 16KB's max.
    Bigger per entity memory size -> more chunks, less entities per chunk.
    So wasting it on large buffers will hurt chunk iteration speed.

    As for the implementation itself, there are other spatial partitioning structures. Like NativeQuadTree / NativeOctree.
    If it works - then its okay. But you probably dont need to insert whole LocalTransform.
    Just the position (or position + radius).
     
  7. Rubenz

    Rubenz

    Joined:
    Feb 2, 2017
    Posts:
    8
    Thanks for helping me so much with this @xVergilx! I learned a lot from you and am now quite happy with my solution.

    I changed the InternalBufferCapacity to 0, this is shown differently most tutorials, but makes total sense with your explanation. That's the thing with trying to learn from minimal examples in a tutorial I guess, what makes sense for a InternalBufferCapacity of 4 doesn't make sense for one of 50, but you only ever see examples with 4.

    After I implemented the full collision avoidance, I will try some of the different spatial partitioning structures and do some benchmarking. Complex, problem specific data structures are always super interesting.

    For the question if I need LocalTransform, the algorithm I'm working on also syncs the alignment of the entities somewhat, so I probably need the rotation as well, but I will just finish my implementation and see what I end up actually using and then trim accordingly.
     
    xVergilx likes this.