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

SystemBase and multiple ForEach

Discussion in 'Entity Component System' started by imaginadio, Apr 23, 2020.

  1. imaginadio

    imaginadio

    Joined:
    Apr 5, 2020
    Posts:
    53
    Im trying to adapt this tutorial

    about Unity ECS targeting system based on ComponentSystem to a SystemBase, and have some problems.
    When im trying to run multiple ForEach - getting error "Only a single Entities.ForEach Lambda expression is currently supported."
    Here is my code:
    Code (CSharp):
    1. public class FindTargetSystem : SystemBase
    2. {
    3.     EndSimulationEntityCommandBufferSystem m_EndSimulationEcbSystem;
    4.  
    5.     protected override void OnCreate()
    6.     {
    7.         base.OnCreate();
    8.         m_EndSimulationEcbSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    9.     }
    10.  
    11.     protected override void OnUpdate() {
    12.         var ecb = m_EndSimulationEcbSystem.CreateCommandBuffer().ToConcurrent();
    13.         Entities
    14.             .WithNone<HasTargetComponent>()
    15.             .WithAll<ZombieComponent_tag>()
    16.             .ForEach((Entity entity, int entityInQueryIndex, ref Translation zombieTranslation) => {
    17.                 float3 zombiePosition = zombieTranslation.Value;
    18.                 Entity closestTargetEntity = Entity.Null;
    19.                 float3 closestTargetPosition = float3.zero;
    20.  
    21.                 Entities.WithAll<PlayerComponent_tag>().ForEach((Entity targetEntity, ref Translation targetTranslation) => {
    22.                     if (closestTargetEntity == Entity.Null) {
    23.                         closestTargetEntity = targetEntity;
    24.                         closestTargetPosition = targetTranslation.Value;
    25.                     } else {
    26.                         if (math.distance(zombiePosition, targetTranslation.Value) < math.distance(zombiePosition, closestTargetPosition)) {
    27.                             closestTargetEntity = targetEntity;
    28.                             closestTargetPosition = targetTranslation.Value;
    29.                         }
    30.                     }
    31.                 }).ScheduleParallel();
    32.  
    33.                 if (closestTargetEntity != Entity.Null) {
    34.                     ecb.AddComponent(entityInQueryIndex, entity, new HasTargetComponent{targetEntity = closestTargetEntity});
    35.                 }
    36.             }).ScheduleParallel();
    37.            
    38.     }
    39. }
    What options do i have? Maby save array of positions to variable on first ForEach and then run another ForEach loop and passing this array to it?
     
  2. KwahuNashoba

    KwahuNashoba

    Joined:
    Mar 30, 2015
    Posts:
    110
    Maybe that message Only a single Entities.ForEach Lambda expression is currently supported is related to recursion depth, just thinking.

    Entities.ForEach(()=>{}).Schedule()/Run()
    does next: Execute entity query -> Instantitate+Initialize job -> Schedule/Run job.

    Here you are trying to schedule parallel job inside parallel job, I'm not sure that is even good thing to do from performance point of view, even if it was possible.
     
  3. imaginadio

    imaginadio

    Joined:
    Apr 5, 2020
    Posts:
    53
    Ok, im understand, that is bad idea. Can you give me advise how can i get access to components of entities (player) from ForEach loop of another entities (enemies)?
     
  4. Chris-Herold

    Chris-Herold

    Joined:
    Nov 14, 2011
    Posts:
    115
  5. florianhanke

    florianhanke

    Joined:
    Jun 8, 2018
    Posts:
    426
    Here's a version using the Async methods, adapted – without running it – from a targeting system I use:
    Code (CSharp):
    1. var entities =
    2.     playerQuery.ToEntityArrayAsync(Allocator.TempJob, out JobHandle entitiesJobHandle);
    3. var translations =
    4.     playerQuery.ToComponentDataArrayAsync<Translation>(Allocator.TempJob, out JobHandle translationsJobHandle);
    5. var combinedJobHandle = JobHandle.CombineDependencies(entitiesJobHandle, translationsJobHandle);
    6. Entities
    7.     .WithNone<HasTargetComponent>()
    8.     .WithAll<ZombieComponent_tag>()
    9.     .ForEach((Entity entity, int entityInQueryIndex, ref Translation zombieTranslation) => {
    10.         float3 zombiePosition = zombieTranslation.Value;
    11.         Entity closestTargetEntity = Entity.Null;
    12.         float3 closestTargetPosition = float3.zero;
    13.  
    14.         for (int i = 0; i < entities.Length; i++) {
    15.           var targetEntity = entities[i];
    16.           var targetTranslation = translations[i];
    17.           if (closestTargetEntity == Entity.Null) {
    18.               closestTargetEntity = targetEntity;
    19.               closestTargetPosition = targetTranslation.Value;
    20.           } else {
    21.               if (math.distance(zombiePosition, targetTranslation.Value) < math.distance(zombiePosition, closestTargetPosition)) {
    22.                   closestTargetEntity = targetEntity;
    23.                   closestTargetPosition = targetTranslation.Value;
    24.               }
    25.           }
    26.         }
    27.  
    28.         if (closestTargetEntity != Entity.Null) {
    29.             ecb.AddComponent(entityInQueryIndex, entity, new HasTargetComponent{targetEntity = closestTargetEntity});
    30.         }
    31.     }).ScheduleParallel(combinedJobHandle);
    Note that it's important to get the job handle dependencies right.
     
    Last edited: Apr 24, 2020
    deus0, imaginadio and SenseEater like this.
  6. imaginadio

    imaginadio

    Joined:
    Apr 5, 2020
    Posts:
    53
    Thank you guys! I make FindTargetSystem like this
    Here is my version:
    Code (CSharp):
    1. public class FindTargetSystem : SystemBase
    2. {
    3.     EndSimulationEntityCommandBufferSystem m_EndSimulationEcbSystem;
    4.     private EntityQuery m_playerQuery;
    5.  
    6.     protected override void OnCreate()
    7.     {
    8.         base.OnCreate();
    9.         m_EndSimulationEcbSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    10.         m_playerQuery = GetEntityQuery(
    11.             ComponentType.ReadOnly<PlayerComponent_tag>(),
    12.             ComponentType.ReadOnly<Translation>());
    13.     }
    14.  
    15.     protected override void OnUpdate() {
    16.         var ecb = m_EndSimulationEcbSystem.CreateCommandBuffer().ToConcurrent();
    17.  
    18.         var translationType = GetArchetypeChunkComponentType<Translation>();
    19.         var entityType = GetArchetypeChunkEntityType();
    20.         var chunks = m_playerQuery.CreateArchetypeChunkArray(Allocator.TempJob);
    21.  
    22.         Entities
    23.             .WithReadOnly(translationType)
    24.             .WithReadOnly(entityType)
    25.             .WithDeallocateOnJobCompletion(chunks)
    26.             .WithNone<HasTargetComponent>()
    27.             .WithAll<ZombieComponent_tag>()
    28.             .ForEach((Entity entity, int entityInQueryIndex, in Translation zombieTranslation) => {
    29.                 float3 zombiePosition = zombieTranslation.Value;
    30.                 Entity closestTargetEntity = Entity.Null;
    31.                 float3 closestTargetPosition = float3.zero;
    32.  
    33.                 for (int c = 0; c < chunks.Length; c++)
    34.                 {
    35.                     var chunk = chunks[c];
    36.                     var chunkEntities = chunk.GetNativeArray(entityType);
    37.                     var translations = chunk.GetNativeArray(translationType);
    38.                     for (int i = 0; i < chunk.Count; i++)
    39.                     {
    40.                         if (closestTargetEntity == Entity.Null) {
    41.                             closestTargetEntity = chunkEntities[0];
    42.                             closestTargetPosition = translations[i].Value;
    43.                         } else {
    44.                             if (math.distance(zombiePosition, translations[i].Value) < math.distance(zombiePosition, closestTargetPosition)) {
    45.                                 closestTargetEntity = chunkEntities[0];
    46.                                 closestTargetPosition = translations[i].Value;
    47.                             }
    48.                         }
    49.                     }
    50.                 }
    51.  
    52.                 if (closestTargetEntity != Entity.Null) {
    53.                     ecb.AddComponent(entityInQueryIndex, entity, new HasTargetComponent{targetEntity = closestTargetEntity});
    54.                 }
    55.             }).ScheduleParallel();
    56.            
    57.     }
    58. }
    I have one small question:
    Code (CSharp):
    1. var chunkEntities = chunk.GetNativeArray(entityType);
    is this possible to get more than one element in this NativeArray? chunk - is an element of chunks array, so it must have only 1 Entity, im right?
     
  7. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,653
    Chunk Array -> Chunk -> Component arrays per type in chunk -> Component element
    Data layout (simplified):
    World, which contains "array" of Archetypes
    Archetype represents array of Chunks for that archetype
    Chunk element contains arrays of component types which respects archetype which chunk belongs to.
    Thus one chunk can (and usually should, depends on archetype and case) contain more than one entity
     
    KwahuNashoba and imaginadio like this.
  8. imaginadio

    imaginadio

    Joined:
    Apr 5, 2020
    Posts:
    53
    Now i understand, thank you.
    I change
    Code (CSharp):
    1. closestTargetEntity = chunkEntities[0];
    to
    Code (CSharp):
    1. closestTargetEntity = chunkEntities[i];
    and everything works correct.
     
  9. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    714
    Question for you guys... how does that chunk iteration perform?

    Every single update you're searching for the closest target and then calling AddComponent() which is known to be a computationally heavy function because it changes the archetype and has to do a bunch of reallocation.

    I'm debating doing something similar, but wary of calling AddComponent() at a high rate in an update function.
     
  10. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    714
    One limitation with both approaches posted above is that the inner query must be run outside of the outer foreach loop. So you cannot change your inner-query based on values returned from your outer query. This is no bueno.

    For example, if your outer foreach is looping over targeters, your inner loop could query for only ground or only air units based on whether the targeter can shoot those types of units. Doing so, would limit your query and decrease time iterating through unnecessary items. With the above examples, you have no choice but to iterate over all target entities even if the targeter can't ever shoot them.

    Is there no clean way to do a simple thing such as a nested loop over entities in ECS????
     
  11. Enzi

    Enzi

    Joined:
    Jan 28, 2013
    Posts:
    909
    In case the structural change is more expansive than subsequent checks you can change the code so HasTargetComponent is always on the entity and you just SetComponent in ECB.

    Regarding checks and data acquisition performance. Getting data takes longer than most code ever will except when you go into heavy arithmetics. So when the data layout is clean and packed most checks won't make an impact even though it's iterating over 1000s of entities.
     
    Last edited: Jun 16, 2020
    lclemens likes this.
  12. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    714
    I was wondering about this just recently. I would like to benchmark both scenarios - one where the query is run outside of the foreach for all targets regardless of their type, and another where the query is run inside of the foreach but limits the targets based on type. If CreateArchetypeChunkArray() is slow, the second scenario could run slower even though it's doing less iterating over entities.

    I'll report back here what I find out. Unfortunately I'm having trouble getting a working elapsed time mechanism going with burst, but that's a separate topic. I'll report back here when I have some results.
     
  13. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    714
    I finally gave up on trying to use a stopwatch timer with burst and ended up using the profiler.

    Unfortunately, I wasn't able to benchmark the second scenario because no matter what I tried, I wasn't able to figure out how to run a query within a ForEach(). The majority of my attempts resulted in something like:

    InvalidOperationException: <>c__DisplayClass_OnUpdate_LambdaJob0.JobData.enemyAirQuery.__impl uses unsafe Pointers which is not allowed. Unsafe Pointers can lead to crashes and no safety against race conditions can be provided.

    I think at this point, I'm fairly convinced that it is not possible to perform a query inside of a ForEach(). Overall, it's not too bad performance-wise, but in principle it seems like a waste to iterate more than necessary. Unity ECS is the only database I know of that doesn't allow a query within a loop. Well... technically it does, but you have to disable burst... and from what I've been reading, ECS without burst and Jobs is substantially slower than not using ECS.

    Next I'm going to investigate using unity physics with the equivalent of "Physics.OverlapSphereNonAlloc" for use as a targeting system and compare performance with this version.
     
  14. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    714
    I have some benchmarks for a TargetingSystem. I performed 4 benchmarking tests (in build mode) with Profile Analyzer. Unfortunately, I'm not 100% sure how to interpret the results.

    I used 1008 targetables, and 3 targeters. Eventually my game will have many more targeters. For the physics calculations, all targetables are using sphere colliders and they filter out all units that can't be targeted via a collision filter. All valid targets within range were then sorted based on range-to-target. I used the Profile Analyzer so the numbers given are averaged over 100 frames or so.

    Method #1) Interative approach:
    This method uses a Entities.ForEach() to loop through all targeters. Then it loops through chunks and eventually examines all targetable objects. It is very similar to imaginadio's code posted above except that it filters certain targets. Since I'm unable to query within the foreach loop, I make my own "filter" by skipping over targets that can't be hit by a specific targeter (like an an air-unit). For the distance comparison, I used math.dot() and compared with the distance squared to avoid the sqrt.

    Method #2) Physics distance calculation:
    This method uses world.CalculateDistance(). It uses a PointDistanceInput.

    Method #3) Collider cast
    This method uses world.CastCollider(). It casts a non-moving sphere at the location of the targeter. It calls SphereCollider.Create().

    Method #4) OverlapAabb.
    This method uses world.OverlapAabb(). The results are an index to a rigidbody, so I get the rigidbody, and then do a math.dot() to get the distance away. For the bounding box, I used Max = targeterPos + float3(radius, radius, radius) and Min = targeterPos - float3(radius, radius, radius).


    And now for the results!

    -------------------------------------------------------------------------------------------
    ECS Iteration
    Default World TargeterSystem: 0.07ms
    -------------------------------------------------------------------------------------------
    Physics (world.CalculateDistance)
    Default World TargeterSystem: 0.02ms
    TargeterSystem:<>c__DisplayClass_OnUpdate_LambdaJob0 (Burst): 0.55ms
    -------------------------------------------------------------------------------------------
    Physics (world.CastCollider)
    Default World TargeterSystem 0.03ms
    TargeterSystem:<>c__DisplayClass_OnUpdate_LambdaJob0 (Burst): 0.69ms
    -------------------------------------------------------------------------------------------
    Physics (world.OverlapAabb)
    Default World TargeterSystem 0.03ms
    -------------------------------------------------------------------------------------------

    If you look at just the "Default World TargeterSystem" that the profiler provides, the Method 1 (Iteration) is the slowest by more than double. However, I believe (please correct me), that CalculateDistance and CastCollider spawn additional lambda burst jobs that take 0.55 and 0.69ms respectively so they are actually much worse than the Iteration method by a factor of almost 10. OverlapAabb did not seem to spawn these additional jobs - I scoured the Marker Details list and didn't see anything that looked like it was an offshoot job of the OverlapAabb() call.

    [Edit] --> I ran one more test... see below
     
    Last edited: Jun 19, 2020
    bb8_1 likes this.
  15. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    714
    I ran one last test! I decided that I wanted the targeting system to be smart enough to ignore targets behind walls, mountains, their own team members, etc. So I modified the Iteration approach slightly by performing a raycast to ignore any target that isn't in the line-of-sight between the targeter and the targetable.

    Here's the result.

    -----------------------------------------------------------------------
    Iteration+Raycasting
    Default World TargeterSystem: 0.05ms
    TargeterSystem:<>c__DisplayClass_OnUpdate_LambdaJob0 (Burst): 0.00ms
    -----------------------------------------------------------------------

    We can see that the raycast performs an on-the-side burst job similar to CastCollider() and CalculcateDistance(), however it's much faster (likely because it happens a lot less often after filtering out lots of potential targets, and also it's a simple raycast so it probably is faster). I was a little surprised that the Iteration+Raycast was faster (0.05ms vs 0.07ms) than plain iteration. I suspect the reason is that it's an extra filter so there are less targets in the final list, and since the final list gets sorted, there's less sorting. If the map had no barriers and the targetables weren't blocking each other from line-of-sight to the target the two algorithms would probably perform similarly.

    At first I was going to use the OverlapAabb method, but after this test, I have changed my mind to use Iteration+Raycasting. There are two reasons: 1) OverlapAabb() is actually a box, and I'd rather have a spherical radius, and 2) The iteration method is only slightly slower (0.05 vs 0.03) and we now have the new feature of being able to ignore targets that aren't in the line-of-sight.

    With a custom collector it might be possible to get the CastCollider() or CalculateDistance() functions to ignore items that aren't line-of-sight but currently that ICollector is very confusing to me and there's little documentation on it. As for OverlapAabb(), it doesn't have the ability to use custom collectors.
     
    florianhanke likes this.
  16. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    714
    For a long time.... a friend was getting these runtime errors on his computer, but everything worked perfectly fine on my computer.

    1) InvalidOperationException: The Unity.Entities.EntityTypeHandle <>c__DisplayClass_OnUpdate_LambdaJob0.JobData.entityType must be marked [ReadOnly] in the job TargeterSystem:<>c__DisplayClass_OnUpdate_LambdaJob0, because the container itself is marked read only.
    2) Unity.Entities.JobChunkExtensions.ScheduleInternal[T] (T& jobData, Unity.Entities.EntityQuery query, Unity.Jobs.JobHandle dependsOn, Unity.Jobs.LowLevel.Unsafe.ScheduleMode mode, System.Boolean isParallel) (at Library/PackageCache/com.unity.entities@0.13.0-preview.24/Unity.Entities/IJobChunk.cs:229)
    3) Unity.Entities.JobChunkExtensions.ScheduleParallel[T] (T jobData, Unity.Entities.EntityQuery query, Unity.Jobs.JobHandle dependsOn) (at Library/PackageCache/com.unity.entities@0.13.0-preview.24/Unity.Entities/IJobChunk.cs:139)
    4) TargeterSystem.OnUpdate () (at Assets/Scripts/Systems/TargeterSystem.cs:103)
    5) Unity.Entities.SystemBase.Update () (at Library/PackageCache/com.unity.entities@0.13.0-preview.24/Unity.Entities/SystemBase.cs:414)

    After some research and trial and error, we narrowed it down to the use of GetEntityTypeHandle() in the OnUpdate() function that was in TargeterSystem that inherits SystemBase. After adding WithReadOnly(entityType) to the Entities.ForEach() loop, my friend's errors went away.

    I honestly don't know why one computer had the errors and the other didn't. Same project, same manifest, same packages+versions, same everything. The only slight difference was that he's using VS Code and I'm using Visual Studio Pro 2019. I figured I'd post this here to help anyone who is searching and runs across a similar error.
     
    bb8_1 likes this.