Search Unity

How to handle Jobs where you care about two archetypes? (And understanding ComponentDataFromEntity)

Discussion in 'Entity Component System' started by The5, Jan 29, 2019.

  1. The5

    The5

    Joined:
    May 26, 2017
    Posts:
    19
    I am trying to implement "TriggerVolumes", e.g. a AABB volume that deals damage to entities inside.
    This means I care about all Entities with a "VolumeAABB" and "Damage" components.
    But at the same time also about all Entities with "Position" and "Health" components.

    For collision handling I had a similar problem:
    I want all entities with position and care about all other positions.
    Like in the Nordeus demo this was solved by hashing the entity positions into a uniform grid then iterating each entity once and manipulating that entities data based on adjacent entities in the grid, getting their data via ComponentDataFromEntity.
    So the N entities were iterated by the IJobProcessComponent data, whereas I had only few adjacent entities to use ComponentDataFromEntity on.
    Not just does this brake down the complexity to O(n) but also eliminates the problem of writing to the same entity from multiple jobs.
    (Or otherwise put, the problem of writing to multiple entities from the same job.)
    The conclusion I drew from this was:
    Have the Job iterate the entities you plan to Write to and get the ReadOnly data from others via ComponentDataFromEntity.

    (For my trigger problem, the Nordeus demo does not seem to have a example for me this time. The area-spells just test the distance to all entities. Given that only a single spell happens per frame, that's fine.)

    So what I did so far is:
    • Have components for each area, e.g. VolumeAABB and VolumeSphere
    • Have static methods to check if a float3 is in a given Volume.
    • Have static methods that return a AABB from a given volume.
    • Have a "Grid-Iterator" class returning int2 based on a AABB passed into the constructor.
    This gives me a way to determine the Hash-Grid-Cells a volume covers.

    So I started off by gathering a group that contains any area.
    Code (CSharp):
    1.             VolumeArcGroup = GetComponentGroup(new EntityArchetypeQuery
    2.             {
    3.                 Any = new ComponentType[] {typeof(VolumeAABB), typeof(VolumeArc), typeof(VolumeTube)},
    4.                 None = Array.Empty<ComponentType>(),
    5.                 All = new ComponentType[] {typeof(Damage)},
    6.             });
    Planing to get the Grid-Iterators and look up entities in the Hash-Grid and then modifying... them... oops.

    This does not fit my understanding of iterating all entities I plan to change (e.g. all with position and health).

    I currently can not think of a way to handle these two Archetypes I want to process in my job.

    And the 2nd question regarding ComponentDataFromEntity:

    As I understand it, the ECS takes care of coherent memory layout for all entities (archetypes) my System makes use of. (Either explicitly via ComponentGroup queries or implicitly passed by IJobProcessComponentData.)
    So when I iterate those System-requested archetypes all is nice and well.
    But how does ComponentDataFromEntity play into this?
    Isn't it just a regular, random memory access?
    E.g. how can that hash-grid collision system still be efficient, when for every entity it iterates it has up to 6 ComponentDataFromEntity requesting positions from other entities?

    Would it be correct to assume:
    If my Job iterates N entities and in every iteration I care about M other entities' components,
    my job should be designed in such a way that N > M.
     
  2. elcionap

    elcionap

    Joined:
    Jan 11, 2016
    Posts:
    138
    If you are using chunk iteration you can determine the existence of a component per chunk. That's how the TransformSystem works in the ECS package. They have different calls based on every combination of Position, Rotation and Scale.
    You can also use multiple EntityArchetypeQuery in the same ComponentGroup if you need different constraints that aren't compatible between they.

    Code (CSharp):
    1. // This will get any archetype with Foo or Bar without both of them
    2. m_Group = GetComponentGroup(
    3.     new EntityArchetypeQuery {
    4.         All = new ComponentType[] { ComponentType.Create<Foo>() },
    5.         None = new ComponentType[] { ComponentType.Create<Bar>() }
    6.     },
    7.     new EntityArchetypeQuery {
    8.         All = new ComponentType[] { ComponentType.Create<Bar>() },
    9.         None = new ComponentType[] { ComponentType.Create<Foo>() }
    10.     }
    11. );
    You can then use IJobChunk:
    Code (CSharp):
    1. struct SomeJob : IJobChunk {
    2.     ArchetypeChunkComponentType<Foo> FooType;
    3.     ArchetypeChunkComponentType<Bar> BarType;
    4.  
    5.     public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
    6.         if (chunk.Has(FooType)) {
    7.             ProcessFoo(chunk.GetNativeArray(FooType));
    8.         } else {
    9.             ProcessBar(chunk.GetNativeArray(BarType));
    10.         }
    11.     }
    12.  
    13.     void ProcessFoo(NativeArray<Foo> fooArray) {
    14.         for (int index = 0; index < fooArray.Length; index++) {
    15.             fooArray[index] = dosomething(fooArray[index]);
    16.         }
    17.     }
    18. }
    19.  
    20. return new SomeJob {
    21.     FooType = GetArchetypeChunkComponentType<Foo>(),
    22.     BarType = GetArchetypeChunkComponentType<Bar>()
    23. }.Schedule(m_Group, inputDeps)
    About ComponentDataFromEntity, they have some overhead to lookup the entity chunk and index but the data itself will create cache misses according your access. If you are accessing entities in the same order of the chunk, excluding the overhead of the lookup, it will use the cache accordingly. If they are random...

    []'s
     
    DwinTeimlon, The5 and Timboc like this.
  3. Timboc

    Timboc

    Joined:
    Jun 22, 2015
    Posts:
    238
    Just dropping a quick thank you @elcionap - totally missed you could do this and it'll be super helpful.
     
  4. The5

    The5

    Joined:
    May 26, 2017
    Posts:
    19
    I was not aware that one could combine queries like that, thanks @elcionap!
    Also for reminding me that I could determine the existence of a component on a per-chunk level.

    Though I cant quite imagine how something like this below could be done:
    Code (CSharp):
    1. struct IsEntityInVolumeJob: IJobChunk {
    2.  
    3.      //iterate the entities I have lots of, e.g. all with "Position", in the Jobs main loop
    4.     public void Execute(ArchetypeChunk chunkPosition, ...)
    5.     {
    6.         //fetch a chunk of the other, fewer Entities, e.g. all with "Volume" so you can access both chunks coherently
    7.         foreach(chunkVolume in ChunksOfVolumes){
    8.             foreach(entityWithPosition in chunkPosition){
    9.                 foreach(entityWithVolume in chunkVolume){
    10.                     //if entityWithVolume.AABB.Contains(entityWithPosition)
    11.                     // do stuff...
    12.                 }
    13.             }
    14.         }
    15.     }
    16. }
    Though, even if this was possible, wouldn't pulling in another chunk into the cache mess with whatever the System/Job got cached to begin with?

    And another question concerning ComponentDataFromEntity:
    Since my entities with Position are mostly dynamic, their spatial position does not likely relate to their cache locations.
    However my Volumes, of witch relatively few exist compared to entities with position, should all fit into a small amount of memory, so a few chunks.
    So if I use the System/Job to iterate all entities with Position and Health I benefit from coherence there.
    Then accessing a Entity with Volume and Damage via ComponentDataFromEntity should pull in a whole chunk of then into the cache.
    Given I only have fewer volumes, several ComponentDataFromEntity-lookups may actually end up in that fetched cache line.
    I guess my question here is:
    Even if accessing ComponentDataFromEntity, that data is still stored coherently by the ECS itself?
    Does accessing this data via ComponentDataFromEntity pull a whole chunk of it into the cache making consecutive accesses via ComponentDataFromEntity potentially efficient?
     
  5. M_R

    M_R

    Joined:
    Apr 15, 2015
    Posts:
    559
    accessing *anything* pulls a cache line (usually 64 bytes, depends on hardware) and maybe the ones directly after it

    if you use CDFE for random access, you don't benefit from the tight packing

    if your volumes are mostly static, then you may build an index over their position (e.g. octree or MultiHashMap) so you can find the intersecting volumes in less that O(n) (rebuilding the index is heavy but you don't need to rebuild if your volumes don't change -- and then you can do it incrementally)
     
  6. The5

    The5

    Joined:
    May 26, 2017
    Posts:
    19
    @M_R I actually tried a MultiHashMap<int2,Entity> for the volumes. Where int2 is the uniform grid cell and entity is each Volume covering that cell.
    A little issue arose when I actually had to specify the size of that MultiHashMap. I would need a job to sum up the number of cells covered by all the volumes, which is doable, but I struggled with finding a way to actually do the summing in a job (like atomic-add).
    But then again the volumes are for stuff like hitboxes of spells too, so they are not quite static, but I could have a second type of volume for this too.
     
    Last edited: Jan 30, 2019
  7. Deleted User

    Deleted User

    Guest

    Next time use for loop instead of foreach. You would gain a lot of performance from it.

    https://jacksondunstan.com/articles/4713
     
  8. The5

    The5

    Joined:
    May 26, 2017
    Posts:
    19
    Oh awesome @wobes! I was actually wondering about iterators the other day.
     
    Deleted User likes this.