Search Unity

Feedback Multiple Entities.ForEach

Discussion in 'Entity Component System' started by charleshendry, Dec 18, 2020.

  1. charleshendry

    charleshendry

    Joined:
    Jan 7, 2018
    Posts:
    97
    I would like to have a system that records various game statistics on how the player is progressing and records these in a native array. The plan is these statistics could be viewed by the player within some UI menu. These statistics are triggered by many different event components.

    Initially I was going to write a single Entities.WithAny<>() and then use several HasComponent<>() calls to check what type of statistic (which native array index) needs updating.

    Instead of this I'm wondering if it would be more performant using several Entities.ForEach calls within the same OnUpdate() function, each one only querying the relevant component, avoiding any HasComponent<>() calls. The downside is that you would have a few Entities.ForEach's that would not iterate any Entities on some of the update calls. So there would be some overhead for that, but presumably less work than several HasComponent<>() calls?

    These statistic updates will be quite infrequent. Does the multiple Entities.ForEach seem reasonable?
     
  2. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,775
    You can profile it, and validate, if your concern is justified. If you loose tiny fraction of ms on one job, just because is idle, while other iterates through thousands of entities, costing i.e. 1 ms, then overhead is really insignificant.

    Also, if system has no matching queries of entities, then it is pretty much skipped.
    So it is perfectly fine, to have "idle" systems.
     
    charleshendry likes this.
  3. Lieene-Guo

    Lieene-Guo

    Joined:
    Aug 20, 2013
    Posts:
    547
    Entities.WithAny<>() 
    Has no effect at all unless you use WriteGroup.
    WriteGroup is described here:
    https://docs.unity3d.com/Packages/com.unity.entities@0.16/manual/ecs_write_groups.html
    When filtering with WriteGroup, all types in the same WriteGroup not included in the WithAll types will be put into WithNone types. WithAny will remove those types marked in WithAny from the query's WithNone types.

    If you are not using WriteGroup,
    Entities.WithAny<>() 
    literally means give me all chunks with or without these types, which means that you are not filtering any chunk/entity out and could be going through every single entity in the world and checking if it has some component with HasComponent.

    So separate Entities.Foreach() is better for sure.
    And an alternative is to use IJobChunk to filter at chunk level, with chunk.Has(someTypeHandle)

    Also, you'd better have a tag type like
    StatsTag:IComponentData{}
    . To mark that this entity(Archetype chunk in fact) has stats on it. so it will narrow down your query to exactly what you what to dig into.
     
    Last edited: Dec 18, 2020
  4. brunocoimbra

    brunocoimbra

    Joined:
    Sep 2, 2015
    Posts:
    679
    This statement is not true, the "Any" filter will surely pick all entities with Any of those components (even if they only have one of those), but will require at least one of those components to exist.

    From the docs: https://i.imgur.com/5Nfk85z.png
     
    Lieene-Guo, elJoel and charleshendry like this.
  5. Lieene-Guo

    Lieene-Guo

    Joined:
    Aug 20, 2013
    Posts:
    547
    My bad you are right
    Code (CSharp):
    1.         //@TODO: All this could be much faster by having all ComponentType pre-sorted to perform a single search loop instead two nested for loops...
    2.         static bool IsMatchingArchetype(Archetype* archetype, EntityQueryData* query)
    3.         {
    4.             for (int i = 0; i != query->ArchetypeQueryCount; i++)
    5.             {
    6.                 if (IsMatchingArchetype(archetype, query->ArchetypeQuery + i))
    7.                     return true;
    8.             }
    9.  
    10.             return false;
    11.         }
    12.  
    13.         static bool IsMatchingArchetype(Archetype* archetype, ArchetypeQuery* query)
    14.         {
    15.             if (!TestMatchingArchetypeAll(archetype, query->All, query->AllCount, query->Options))
    16.                 return false;
    17.             if (!TestMatchingArchetypeNone(archetype, query->None, query->NoneCount))
    18.                 return false;
    19.             if (!TestMatchingArchetypeAny(archetype, query->Any, query->AnyCount))
    20.                 return false;
    21.  
    22.             return true;
    23.         }
    24.  
    25.         static bool TestMatchingArchetypeAny(Archetype* archetype, int* anyTypes, int anyCount)
    26.         {
    27.             if (anyCount == 0) return true;
    28.  
    29.             var componentTypes = archetype->Types;
    30.             var componentTypesCount = archetype->TypesCount;
    31.             for (var i = 0; i < componentTypesCount; i++)
    32.             {
    33.                 var componentTypeIndex = componentTypes[i].TypeIndex;
    34.                 for (var j = 0; j < anyCount; j++)
    35.                 {
    36.                     var anyTypeIndex = anyTypes[j];
    37.                     if (componentTypeIndex == anyTypeIndex)
    38.                         return true;
    39.                 }
    40.             }
    41.  
    42.             return false;
    43.         }
    Last Time I check ECS source I did not find this code block. But instead found that And EntityQuery holds a list call required components and it does not contain Any types. And this RequiredComponents is used in Chunk iteration. So I thought that Query's Type Matching is done on the fly.

    But that's not the case! This time I found this part
    Code (CSharp):
    1.         public void EndArchetypeChangeTracking(ArchetypeChanges changes, EntityQueryManager* queries)
    2.         {
    3.             Assert.AreEqual(m_ArchetypeTrackingVersion, changes.ArchetypeTrackingVersion);
    4.             if (m_Archetypes.Length - changes.StartIndex == 0)
    5.                 return;
    6.  
    7.             var changeList = new UnsafeArchetypePtrList(m_Archetypes.Ptr + changes.StartIndex, m_Archetypes.Length - changes.StartIndex);
    8.             queries->AddAdditionalArchetypes(changeList);
    9.         }
    10.  
    This function is only called when there's a structural change. And all EntityQuery's internal data pointer is pointing to internal storage Managed by EntityComponetStore.
    So every EntityQuery's matching ArchyType is updated when there is a structural change. And kept in a linear list for reuse.
    So EntityQuery iteration is just walking through a linear list of ArchyType's linear list of chunks, and test for Change/SCD/Order filter if needed.
    That's fast!
     
    brunocoimbra and charleshendry like this.