Search Unity

Entities.ForEach and Modifying Dynamic Buffers

Discussion in 'Entity Component System' started by TheGabelle, May 23, 2020.

  1. TheGabelle

    TheGabelle

    Joined:
    Aug 23, 2013
    Posts:
    242
    I'm getting errors trying to update buffer elements. Can I do this in a Entities.ForEach?

    Code (CSharp):
    1. Assets\Skill System WIP\Health\HealthSystem.cs(34,21): error CS1612: Cannot modify the return value of 'DynamicBuffer<HealthAffect>.this[int]' because it is not a variable

    Code (CSharp):
    1. using Unity.Entities;
    2.  
    3. public struct Health : IComponentData
    4. {
    5.     public int Value;
    6.     public int MaxValue;
    7. }
    8.  

    Code (CSharp):
    1. using Unity.Entities;
    2. [InternalBufferCapacity(10)]
    3. public struct HealthAffect : IBufferElementData
    4. {
    5.     public int Value;
    6.     public int Iterations;
    7.     public int MaxIterations;
    8.     public float Delay;
    9.     public float Time;
    10. }

    Code (CSharp):
    1.  
    2. using UnityEngine;
    3. using Unity.Entities;
    4.  
    5.  
    6. public class HealthSystem : SystemBase
    7. {
    8.  
    9.     EndSimulationEntityCommandBufferSystem m_EndSimulationEcbSystem;
    10.  
    11.     protected override void OnCreate()
    12.     {
    13.         base.OnCreate();
    14.  
    15.         m_EndSimulationEcbSystem = World
    16.             .GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    17.     }
    18.  
    19.  
    20.     protected override void OnUpdate()
    21.     {
    22.         var dt = Time.DeltaTime;
    23.         var ecb = m_EndSimulationEcbSystem.CreateCommandBuffer().ToConcurrent();
    24.  
    25.         Entities.ForEach( (Entity entity, int entityInQueryIndex, ref DynamicBuffer<HealthAffect> healthAffectBuffer, ref Health health) => {
    26.            
    27.             for (var i = 0; i < healthAffectBuffer.Length; i++)
    28.             {
    29.                 var affect = healthAffectBuffer[i];
    30.                 affect.Time += dt;
    31.  
    32.                 if(affect.Time >= affect.Delay)
    33.                 {
    34.                     affect.Time -= affect.Delay;
    35.                     affect.Iterations++;
    36.                     health.Value += affect.Value;
    37.                     if(health.Value > health.MaxValue)
    38.                     {
    39.                         health.Value = health.MaxValue;
    40.                     }
    41.                 }
    42.  
    43.                 healthAffectBuffer[i] = affect;
    44.  
    45.                 if(health.Value <= 0)
    46.                 {
    47.                     ecb.DestroyEntity(entityInQueryIndex, entity);
    48.                 }
    49.  
    50.                 if(affect.Iterations >= affect.MaxIterations)
    51.                 {
    52.                     healthAffectBuffer.RemoveAt(i);
    53.                     i = (i > 0) ? i - 1 : 0;
    54.                 }
    55.             }
    56.         })
    57.         .ScheduleParallel();
    58.         m_EndSimulationEcbSystem.AddJobHandleForProducer(this.Dependency);
    59.     }
    60. }
    61.  
     
    Last edited: May 23, 2020
  2. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    I don't think you can do
    Code (CSharp):
    1.  healthAffectBuffer[i].Time += dt;
    try

    Code (CSharp):
    1. var  tmp = healthAffectBuffer[i];
    2. tmp.Time += dt;
    3. healthAffectBuffer[i] = tmp ;
    same thing for
    Code (CSharp):
    1.                     healthAffectBuffer[i].Time -= healthAffectBuffer[i].Delay;
    2.                     healthAffectBuffer[i].Iterations++;
    3.                     health.Value += healthAffectBuffer[i].Value;
     
  3. TheGabelle

    TheGabelle

    Joined:
    Aug 23, 2013
    Posts:
    242
    That fixes it! I should have known better.

    When I set 1000 entities with buffers of 1000 elements, I average 45fps. I get about the same with 100000 Entities with buffers of 10 elements. [InternalBufferCapacity(10)] seems to work best for both cases. Is there another way to approach this? The performance is nowhere near what I was hoping.


    machine:
    CPU: i5-7600K @ 3.8 GHz
    GPU: NVIDIA GeForce GTX 1070
    RAM: 16GB @ 2123 MHz

    Code (CSharp):
    1.  
    2. using UnityEngine;
    3. using Unity.Entities;
    4. using Unity.Collections;
    5.  
    6. public class SimpleTest : MonoBehaviour
    7. {
    8.  
    9.     public int entityCount = 100000;
    10.     public int affectCount = 10;
    11.  
    12.     EntityManager _em;
    13.     // Start is called before the first frame update
    14.     void Start()
    15.     {
    16.         _em = World.DefaultGameObjectInjectionWorld.EntityManager;
    17.  
    18.         EntityArchetype arch = _em.CreateArchetype(new ComponentType[] {typeof(Health), typeof(HealthAffect)});
    19.         NativeArray<Entity> entities = _em.CreateEntity(arch, entityCount, Allocator.Temp);
    20.  
    21.         for(var i = 0; i < entities.Length; i++)
    22.         {
    23.             _em.AddComponentData<Health>(entities[i], new Health {Value = 100, MaxValue = 100});
    24.             var buffer = _em.AddBuffer<HealthAffect>(entities[i]);
    25.             for(var j = 0; j < affectCount; j++)
    26.             {
    27.                 buffer.Add(new HealthAffect {Value = (j % 2 == 0) ? 1 : -1, Iterations = 0, MaxIterations = 10, Delay = 1.0f, Time = 0f});
    28.             }
    29.          
    30.         }
    31.  
    32.         entities.Dispose();
    33.  
    34.     }
    35.  
    36. }
    37.  
     
    Last edited: May 23, 2020
  4. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
    Did not may attention to the actual code and did not notice your were trying to do an effect/affect system.

    I have been strugulling too to make a syustem both performent and deingner friendly.

    here is what I get for now :
    https://forum.unity.com/threads/making-a-skill-system.894007/#post-5884673

    PS: the perf entity count are false, I actually have 4 time that number of entity (1 target and 3 "attacker") with an average of 4 times the entioty count of effect per target.
     
  5. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    One optimization for DynamicBuffer is to cast it to a NativeArray before iteration.

    var array = healthAffectBuffer.AsNativeArray();

    NOTE:
    In Burst 1.4 this optimization should be less necessary since we improved alias analysis for dynamic buffer there.
     
    MNNoxMortem and nicolasgramlich like this.
  6. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    Also I suggest opening the Timeline profiler window to see exactly where it is spending the time and how the jobs are distributed to cores.
     
  7. TheGabelle

    TheGabelle

    Joined:
    Aug 23, 2013
    Posts:
    242
    Average frame rate +30 (~70fps). I do a lot of collection switching to remove items and then write back to the buffer, so I assume there is or will be a more performant way to handle cases this.


    Code (CSharp):
    1. protected override void OnUpdate()
    2.     {
    3.         var dt = Time.DeltaTime;
    4.         var ecb = m_EndSimulationEcbSystem.CreateCommandBuffer().ToConcurrent();
    5.  
    6.         Entities.ForEach( (Entity entity, int entityInQueryIndex, ref DynamicBuffer<HealthAffect> healthAffectBuffer, ref Health health) => {
    7.          
    8.             var healthAffectList = new NativeList<HealthAffect> (healthAffectBuffer.Length, Allocator.Temp);
    9.             healthAffectList.AddRange(healthAffectBuffer.AsNativeArray());
    10.          
    11.             for (var i = 0; i < healthAffectList.Length; i++)
    12.             {
    13.                 var affect = healthAffectList[i];
    14.                 affect.Time += dt;
    15.  
    16.                 if(affect.Time >= affect.Delay)
    17.                 {
    18.                     affect.Time -= affect.Delay;
    19.                     affect.Iterations++;
    20.                     health.Value += affect.Value;
    21.                     if(health.Value > health.MaxValue)
    22.                     {
    23.                         health.Value = health.MaxValue;
    24.                     }
    25.                 }
    26.  
    27.                 healthAffectList[i] = affect;
    28.  
    29.                 if(health.Value <= 0)
    30.                 {
    31.                     ecb.DestroyEntity(entityInQueryIndex, entity);
    32.                 }
    33.  
    34.                 if(affect.Iterations >= affect.MaxIterations)
    35.                 {
    36.                     NativeListExtensions.RemoveAt(healthAffectList, i);
    37.                     i = (i > 0) ? i - 1 : 0;
    38.                 }
    39.             }
    40.             healthAffectBuffer.Clear();
    41.             healthAffectBuffer.CopyFrom(healthAffectList.AsArray());
    42.             healthAffectList.Dispose();
    43.         })
    44.         .ScheduleParallel();
    45.         m_EndSimulationEcbSystem.AddJobHandleForProducer(this.Dependency);
    46.     }
    NativeListExtensions found here

    The pink bars below the HealthSystem.OnUpdate_LamdaJob0 says:
    "MemoryManager.FallbackAllocation" 0.001ms - 0.002ms


    Capture1.PNG Capture2.PNG
     
  8. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    Code (CSharp):
    1. var healthAffectList = new NativeList<HealthAffect> (healthAffectBuffer.Length, Allocator.Temp);
    2.             healthAffectList.AddRange(healthAffectBuffer.AsNativeArray());
    3.  
    That certainly will make the code run slower than it did before...

    You are allocating a new list on each entity iteration. You should never allocate a container in an innerloop. NativeList & DynamicBuffer are roughly the same perf, so basically there was no gain just worse perf with this change... NativeArray is faster since there is less indirection & better alias information available in burst.


    Given that you mutate the DynamicBuffer in the for loop, you have to grab a new NativeArray after each array modification. You can use AsNativeArray before the loop and right after the Remove At.
     
  9. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    Code (CSharp):
    1.  
    2. protected override void OnUpdate()
    3.     {
    4.         var dt = Time.DeltaTime;
    5.         var ecb = m_EndSimulationEcbSystem.CreateCommandBuffer().ToConcurrent();
    6.  
    7.         Entities.ForEach( (Entity entity, int entityInQueryIndex, ref DynamicBuffer<HealthAffect> healthAffectBuffer, ref Health health) => {
    8.          
    9.             var healthAffectArray = healthAffectBuffer.AsNativeArray();
    10.          
    11.             for (var i = 0; i < healthAffectArray.Length;)
    12.             {
    13.                 var affect = healthAffectArray[i];
    14.                 affect.Time += dt;
    15.  
    16.                 if(affect.Time >= affect.Delay)
    17.                 {
    18.                     affect.Time -= affect.Delay;
    19.                     affect.Iterations++;
    20.                     health.Value += affect.Value;
    21.                     if(health.Value > health.MaxValue)
    22.                     {
    23.                         health.Value = health.MaxValue;
    24.                     }
    25.                 }
    26.  
    27.                 healthAffectArray[i] = affect;
    28.  
    29.                 if(health.Value <= 0)
    30.                 {
    31.                     ecb.DestroyEntity(entityInQueryIndex, entity);
    32.                 }
    33.  
    34.                 if(affect.Iterations >= affect.MaxIterations)
    35.                 {
    36.                     healthAffectBuffer.RemoveAtSwapBack(i);
    37.                     // Have to grab the array again since the buffer size has changed.
    38.                     healthAffectArray = healthAffectBuffer.AsNativeArray();
    39.                 }
    40.                 else
    41.                 {
    42.                     i++;
    43.                 }
    44.             }
    45.         })
    46.         .ScheduleParallel();
    47.         m_EndSimulationEcbSystem.AddJobHandleForProducer(this.Dependency);
    48.     }
    49.  
    Something like this. Also note the usage of RemoveAtSwapBack which avoids moving every element in the list. This makes the assumption that order is unimportant...
     
  10. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    Most importantly.... Enable burst... in Jobs menu... its on by default, but likely you turned it off at some point...

    That will probably give you a 20x speedup.
     
  11. TheGabelle

    TheGabelle

    Joined:
    Aug 23, 2013
    Posts:
    242
    That is significantly better. I switched RemoveAtSwapBack with RemoveAt, I don't think I have the swap back method in my version.

    Test 1
    Entities: 1000
    Affects per: 1000
    fps: ~110

    Test 2

    Entities: 100000
    Affects per: 10
    fps: ~80

    Typically after the buffers are cleared the frame rate is well above 1000. But, with Test 2, fps is about 250 after the buffers are cleared because the system takes ~5ms. I'm not familiar enough with ECS to determine why this would take so long. I included an early out as well:

    if(healthAffectBuffer.Length == 0) {return;}
     

    Attached Files:

  12. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    DynamicBuffer.RemoveAtSwapBack definitely exists.

    You are still not using burst. Please enable it in the jobs menu, so you can get your 20x speedup...
     
  13. TheGabelle

    TheGabelle

    Joined:
    Aug 23, 2013
    Posts:
    242
    Wow that is embarrassing. I had Enable Compilation turned off and Safety Checks turned on.

    Test 1
    fps: ~410
    system: ~1.90ms

    Test 2
    fps: ~375
    system: ~1.75ms
     
    lclemens likes this.
  14. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    To see the real performance please turn off SafetyChecks too.
     
  15. s_schoener

    s_schoener

    Unity Technologies

    Joined:
    Nov 4, 2019
    Posts:
    81
    DynamicBuffer.RemoveAtSwapBack is part of the next release (or the one after that, depending on how timings work out); I added it recently because it is such an obvious oversight.