Search Unity

Using data from a job (Dynamic Buffer vs Native Array)

Discussion in 'Entity Component System' started by ujz, Nov 12, 2020.

  1. ujz

    ujz

    Joined:
    Feb 17, 2020
    Posts:
    29
    Hey guys, I'm just starting my DOTS journey and need help taking data from a job and using it elsewhere. I've had some success using DynamicBuffers to capture data during a ForEach, write it to a component, and then use in other jobs. But I think I'm doing it wrong and feel like NativeArrays are more suited. But not sure...

    In my scenario, I have no more than 10,000 entities. Every now and then an entity triggers a SpecialAction and broadcasts the result to nearby entities. In below code, I listen for SpecialAction triggers, use OverlapAabb to identify who is nearby, and then use the resulting hitList to write to a DynamicBuffer of every nearby entity. Then I use that DynamicBuffer in other jobs.

    Code (CSharp):
    1. [UpdateAfter(typeof(EndFramePhysicsSystem))]
    2. public class BroadcastSystem : SystemBase
    3. {
    4.     BuildPhysicsWorld physicsWorld;
    5.  
    6.     protected override void OnCreate(){
    7.         physicsWorld = World.GetOrCreateSystem<BuildPhysicsWorld>();
    8.     }
    9.  
    10.     protected override void OnUpdate(){
    11.         var colWorld = physicsWorld.PhysicsWorld.CollisionWorld;
    12.         var bodies = physicsWorld.PhysicsWorld.Bodies;
    13.         float dT = Time.DeltaTime;
    14.         float3 nearbyRadius = 5;
    15.  
    16.         Entities.ForEach((int nativeThreadIndex,
    17.                         in SpecialAction special,
    18.                         in Translation position) =>
    19.             {
    20.                 if (special.triggered){
    21.                     var hitList = new NativeList<int>(128, Allocator.Temp);
    22.                     OverlapAabbInput overlapAabbInput = new OverlapAabbInput{
    23.                         Aabb = new Aabb
    24.                         {
    25.                             Min = position.Value - nearbyRadius,
    26.                             Max = position.Value + nearbyRadius
    27.                         },
    28.                         Filter = new CollisionFilter
    29.                         {
    30.                             BelongsTo = ~0u,
    31.                             CollidesWith = ~0u,
    32.                             GroupIndex = 0
    33.                         }
    34.                     };
    35.                     if (colWorld.OverlapAabb(overlapAabbInput, ref hitList)){
    36.                         for (int i = 0;i<hitList.Length;i++){
    37.                             BufferFromEntity<BroadcastBuffered> hitListBuffs = GetBufferFromEntity<BroadcastBuffered>();
    38.                             DynamicBuffer<BroadcastBuffered> hitBuff = hitListBuffs[bodies[hitList[i]].Entity];
    39.                             BroadcastBuffered broadcastBuff = hitBuff[0];
    40.                             broadcastBuff.value++;
    41.                             hitBuff[0] = broadcastBuff;
    42.                         }
    43.                     }
    44.                     hitList.Dispose();
    45.                 }
    46.             }).Schedule();
    47.     }
    48. }
    I feel like having the DynamicBuffers inside the for loop is incredibly clunky? But I don't know how else to make use of the hitList. Is NativeArray better suited somehow? But not sure the syntax to take it to another job...

    Also, I considered a different design with each entity constantly listening for nearby SpecialActions. But I thought the overhead would be worse...

    Anyway, would really appreciate your advice. Thank you for reading.
     
    Last edited: Nov 13, 2020
  2. DK_A5B

    DK_A5B

    Joined:
    Jan 21, 2016
    Posts:
    110
    You mentioned you’re using the data from the “hit buffer” in other Jobs. Are these other Jobs being scheduled from BroadcastSystem or another System? The reason I ask is that it is much cleaner if the Jobs are being scheduled from inside BroadcastSystem . Otherwise, you’ll have to expose the “hit buffer” data publicly somehow (as a property, with a get function, etc.) so that other Systems can access the data. This is doable, but it means creating direct dependencies between Systems - which is “messier”.

    Another question I have is, how many elements are being stored in this hit buffer? If I’m reading your code correctly, it looks like you’re getting the i-th index of the hitBuff, where i is also the current index of the hitList. This would imply that the DynmicBuffer<BroadcastBuffer> on each entity had as many elements as there were hits, is that correct? I’m having a bit of difficulty understanding why you’re using i as the index to both the hitList (the NativeList of hits this tick for the current Entity in the ForEach) and to the hitBuff (the DynmicBuffer<BroadcastBuffer> used to store hits on the Entity that was hit). Perhaps you can elaborate on this?
     
  3. ujz

    ujz

    Joined:
    Feb 17, 2020
    Posts:
    29
    Appreciate you reading and replying. Firstly, sorry the "i" in hitBuff was a typo. I messed up while cleaning the code. BroadcastBuffer has only 1 element, so it should say "0". I fixed the code above to reflect that.

    As for your first point, I think I can nestle everything into Broadcast System. So I guess I would have three Jobs in sequence: pick up data, sort it, write it. In this case, I guess NativeArray or NativeList can be used to move data between the jobs.

    So I guess both DynamicBuffer or NativeArray can be used depending on how I structure the jobs/systems. But I wanted to structure my jobs/systems based on what's more efficient. So it's like a chicken and egg problem. Or rather, I don't know enough about DOTS yet to figure out what is best for me, so was hoping if there are some general guidelines or rule-of-thumb to follow?

    Thank you again.
     
  4. DK_A5B

    DK_A5B

    Joined:
    Jan 21, 2016
    Posts:
    110
    If I had to provide a couple of guidelines for working with DOTS, they would be:

    • "Always Be Bursting" - I really can't stress this enough. Wherever possible, make your code Burstable. If you're not using Burst, you're leaving performance on the table. It won't always be possible, there will be times that you need to write non-Burstable code. However, if you find yourself writing non-Burstable code, take a look at what you're doing and ask if there's another way to do it to make it Burstable.
    • "Group Structural Changes Together" - Structural changes (creating/destroying Entities, adding/removing Components, changing the value of Shared Components, etc.) create multi-threading bottlenecks in your application. Whenever you execute a structural change it forces all Jobs to Complete, blocking the main thread until they do. The Unity documentation calls these locations in your application "sync points" (because everything synchronizes with the main-thread). However, once you've made one structural change (and created a sync point) there's no additional cost to executing more structural changes immediately after that (because you no longer have any Jobs running). Therefore, it is desirable to group together all of your structural changes into as few locations in your code as possible. This is the idea behind Entity Command Buffers. They give you a place to store commands to make structural changes at a later time, and then they execute all of the structural changes at one time (creating a single sync point for each ECB System). This doesn't mean you can't use EntityManager to make structural changes. It just means that you need to be aware that you're creating a sync point when you do that, and you should avoid sprinkling calls to EntityManager methods that make structural changes all around your code.
    • "Avoid Random Access Where Possible" - One of the underpinnings of ECS performance is that modern processor architecture makes linear/sequential data access significantly faster in practice than theory would indicate (this has to do with the difference in speed between accessing the processor cache vs memory). This means it is much faster to loop through elements in an array than it is to dereference pointers or to access an array by jumping to random elements in it. Theory treats these operations as basically the same for the purposes of analyzing performance (e.g. big O notation), but in practice they can be orders of magnitude different in speed. Methods like GetComponent or GetComponentFromEntity (methods where you're passing the Entity as a parameter to get access to data) provide random access to the Component data of Entities. You need these methods. I'm not saying don't use them. However, whenever you find yourself using them, you should ask yourself if there is another way to arrange your data so that it can be accessed linearly instead of using these methods.
    • "Monitor Chunk Utilization" - Unity ECS exploits the performance gains from accessing data linearly by storing the Component data of Entities of the same Archetype together in Chunks. When it gets a Component for an Entity, it pulls the entire Chunk storing that data for that Entity into the cache as well. As a result, if you're linearly accessing the Entities, the data for the other Entities in the Chunk are already in the cache when you go to access them. All Chunks are the same size (16kb) and this means the larger an Archtype is (i.e. the more Components you have in it and the more data in those Components) the fewer Entities can be stored in a Chunk for that Archetype. As you reduce the number of Entities stored in a Chunk, you also reduce the performance gains from linear access (you're not loading the data for as many Entities into the cache each time you load a Chunk). So you want to organize your data in a way that keeps your Archetypes as small as possible (so you can pack as many Entities into a Chunk as possible) while balancing that against using methods to randomly access data (if your Archtypes are too small you'll need to rely on random access more). Another thing to watch out for with Chunks is having too many different types of Entity Archetypes. If you have a lot of Components that only go on a small number of Entities, you'll end up creating many Archetypes with only a couple of Entities belonging to each Archetype. This leads to poor Chunk utilization because a Chunk can only store the data a single Archtype. If you have many Archetypes with only a few Entites belonging to them, it means that you will many Chunks with each storing only a few Entities. This reduces the performance benefits of linear access because loading a Chunk only loads the data for a small number of Entities into the cache.