Search Unity

JobSystem not working as expected

Discussion in 'Entity Component System' started by Sarkahn, May 23, 2019.

  1. Sarkahn

    Sarkahn

    Joined:
    Jan 9, 2013
    Posts:
    440
    Hello, I'm attempting to create a simple sphere-sphere collision system, but it seems to be failing miserably when using jobs. When running everything on the main thread it appears to work fine:

    One shooter (JobComponentSystem):
    K3PBGmf3EG.gif

    Multiple shooters (JobComponentSystem):
    9gtw4MtTp2.gif

    Multiple Shooters (Main Thread):
    wcWl89XilJ.gif

    It seems to fail more and more in the JobComponentSystem as more bullets are created. I've tried testing parts of my system in isolation and it seems like the logic is sound, so I'm not sure if I'm just making a dumb error or misunderstanding something about how ECS should work.

    This is the code for my CollisionSystem:
    Code (CSharp):
    1. using Unity.Burst;
    2. using Unity.Collections;
    3. using Unity.Entities;
    4. using Unity.Jobs;
    5. using Unity.Mathematics;
    6. using Unity.Transforms;
    7. using UnityEngine;
    8.  
    9. //[DisableAutoCreation]
    10. public class CollisionSystem : JobComponentSystem
    11. {
    12.     NativeMultiHashMap<int, Entity> spatialMap_;
    13.     NativeQueue<CollisionData> dataQueue_;
    14.     NativeList<int> keysList_;
    15.  
    16.     static readonly int CellSize = 20;
    17.  
    18.     EndSimulationEntityCommandBufferSystem initBufferSystem_;
    19.  
    20.     [BurstCompile]
    21.     struct BuildSpatialMap : IJobForEachWithEntity<Translation, ECSCollider>
    22.     {
    23.         public NativeMultiHashMap<int, Entity>.Concurrent spatialMap;
    24.  
    25.         public void Execute(Entity e, int chunkIndex, [ReadOnly] ref Translation translation, [ReadOnly] ref ECSCollider coll)
    26.         {
    27.             float2 p = translation.Value.xy;
    28.          
    29.             VisitGridIndices(p, coll.radius, e, spatialMap.Add, CellSize);
    30.         }
    31.  
    32.         void VisitGridIndices<TValue>(
    33.             float2 pos, float radius, TValue val,
    34.             System.Action<int, TValue> callback, int cellSize)
    35.         {
    36.             float2 p = pos;
    37.             int2 min = (int2)math.floor((p - radius) / cellSize);
    38.             int2 max = (int2)math.ceil((p + radius) / cellSize);
    39.             int count = max.x - min.x;
    40.  
    41.             for (int x = min.x; x < max.x; ++x)
    42.             {
    43.                 for (int y = min.y; y < max.y; ++y)
    44.                 {
    45.                     int2 cell = new int2(x, y);
    46.                     int hash = cell.GetHashCode();
    47.  
    48.                     callback(hash, val);
    49.                 }
    50.             }
    51.         }
    52.     }
    53.  
    54.     [BurstCompile]
    55.     struct InitializeKeysList : IJob
    56.     {
    57.         public NativeList<int> list;
    58.  
    59.         [ReadOnly]
    60.         public NativeMultiHashMap<int, Entity> spatialMap;
    61.  
    62.         public void Execute()
    63.         {
    64.             var keys = spatialMap.GetKeyArray(Allocator.Temp);
    65.             list.ResizeUninitialized(spatialMap.Length);
    66.             for (int i = 0; i < list.Length; ++i)
    67.                 list[i] = keys[i];
    68.         }
    69.     };
    70.  
    71.     [BurstCompile]
    72.     struct GenerateCollisionData : IJobParallelForDefer
    73.     {
    74.         [ReadOnly]
    75.         public NativeMultiHashMap<int, Entity> spatialMap;
    76.  
    77.         [ReadOnly]
    78.         public NativeArray<int> keys;
    79.  
    80.         [ReadOnly]
    81.         public ComponentDataFromEntity<ECSCollider> colliderFromEntity;
    82.  
    83.         [ReadOnly]
    84.         public ComponentDataFromEntity<Translation> posFromEntity;
    85.      
    86.         [WriteOnly]
    87.         public NativeQueue<CollisionData>.Concurrent dataQueue;
    88.      
    89.         public void Execute(int index)
    90.         {
    91.             NativeMultiHashMapIterator<int> it;
    92.  
    93.             Entity curr;
    94.             if( spatialMap.TryGetFirstValue(keys[index], out curr, out it ))
    95.             {
    96.                 Entity next;
    97.                 while( spatialMap.TryGetNextValue(out next, ref it))
    98.                 {
    99.                     var aColl = colliderFromEntity[curr];
    100.                     var bColl = colliderFromEntity[next];
    101.                     var a = posFromEntity[curr].Value;
    102.                     var b = posFromEntity[next].Value;
    103.                  
    104.                     // TODO: Collision flags
    105.                  
    106.                     if( CircleOverlap(a, aColl.radius, b, bColl.radius ))
    107.                     {
    108.                         dataQueue.Enqueue(new CollisionData { a = curr, b = next });
    109.                     }
    110.  
    111.                     curr = next;
    112.                 }
    113.             }
    114.  
    115.             bool CircleOverlap(float3 aPos, float aRadius, float3 bPos, float bRadius)
    116.             {
    117.                 float r = aRadius + bRadius;
    118.                 float dx = bPos.x - aPos.x;
    119.                 float dy = aPos.y - bPos.y;
    120.  
    121.                 return dx * dx + dy * dy < r * r;
    122.             }
    123.         }
    124.     }
    125.  
    126.  
    127.     //[BurstCompile]
    128.     struct ProcessCollisionData : IJob
    129.     {
    130.         public NativeQueue<CollisionData> dataQueue;
    131.  
    132.         [ReadOnly]
    133.         public EntityCommandBuffer commandBuffer;
    134.      
    135.         public void Execute()
    136.         {
    137.             CollisionData data;
    138.             while(dataQueue.TryDequeue(out data))
    139.             {
    140.                 commandBuffer.AddComponent<CollisionTag>(data.a, new CollisionTag());
    141.                 commandBuffer.AddComponent<CollisionTag>(data.b, new CollisionTag());
    142.             }
    143.         }
    144.     }
    145.  
    146.     protected override void OnCreate()
    147.     {
    148.         spatialMap_ = new NativeMultiHashMap<int, Entity>(5000, Allocator.Persistent);
    149.         dataQueue_ = new NativeQueue<CollisionData>(Allocator.Persistent);
    150.         keysList_ = new NativeList<int>(Allocator.Persistent);
    151.         initBufferSystem_ = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    152.     }
    153.  
    154.     protected override void OnDestroy()
    155.     {
    156.         spatialMap_.Dispose();
    157.         dataQueue_.Dispose();
    158.         keysList_.Dispose();
    159.      
    160.     }
    161.  
    162.     protected override JobHandle OnUpdate(JobHandle inputDependencies)
    163.     {
    164.         var job = inputDependencies;
    165.  
    166.         spatialMap_.Clear();
    167.         dataQueue_.Clear();
    168.         keysList_.Clear();
    169.  
    170.         // Build our spatial map
    171.         job = new BuildSpatialMap
    172.         {
    173.             spatialMap = spatialMap_.ToConcurrent(),
    174.         }.Schedule(this, job);
    175.      
    176.         // Initialize the size of our keys list. We can't know the size of our list
    177.         // during schedule time so we need to use "DeferredJobArray"
    178.         // Example in Packages/Jobs/Unity.Jobs.Test/NativeListDeferredArrayTests
    179.         job = new InitializeKeysList
    180.         {
    181.             list = keysList_,
    182.             spatialMap = spatialMap_,
    183.         }.Schedule(job);
    184.      
    185.         // Create our collision data (which entities collided) and queue it for the next job
    186.         // We don't want to process immediately since that could potentially lead to
    187.         // multiple threads trying to write to one entity at the same time
    188.         job = new GenerateCollisionData
    189.         {
    190.             spatialMap = spatialMap_,
    191.             keys = keysList_.AsDeferredJobArray(),
    192.             colliderFromEntity = GetComponentDataFromEntity<ECSCollider>(true),
    193.             posFromEntity = GetComponentDataFromEntity<Translation>(true),
    194.             dataQueue = dataQueue_.ToConcurrent(),
    195.         }.Schedule(keysList_, 5, job);
    196.  
    197.         // Tags the appropriate entities for our collision handling system to deal with
    198.         job = new ProcessCollisionData
    199.         {
    200.             dataQueue = dataQueue_,
    201.             commandBuffer = initBufferSystem_.CreateCommandBuffer(),
    202.         }.Schedule(job);
    203.      
    204.         return job;
    205.     }
    206. }

    It's pretty straightforward, it builds the map, runs through it and puts the collisions into a queue, then reads the collisions out and tags the entities so another system can deal with them:
    Code (CSharp):
    1. using Unity.Burst;
    2. using Unity.Collections;
    3. using Unity.Entities;
    4. using Unity.Jobs;
    5. using Unity.Mathematics;
    6. using Unity.Transforms;
    7. using UnityEngine;
    8.  
    9. public class BulletCollisionHandlingSystem : ComponentSystem
    10. {
    11.     EntityQuery bulletCollisionQuery_;
    12.  
    13.     protected override void OnCreate()
    14.     {
    15.         bulletCollisionQuery_ = GetEntityQuery(typeof(CollisionTag), typeof(Bullet));
    16.     }
    17.  
    18.     protected override void OnUpdate()
    19.     {
    20.         EntityManager.DestroyEntity(bulletCollisionQuery_);
    21.         EntityManager.RemoveComponent<CollisionTag>(bulletCollisionQuery_);
    22.     }
    23. }

    I'm been banging my head against it for days and I can't seem to figure out what I'm doing wrong. Any advice on mistakes I'm making or how I can change things to better understand what I'm doing wrong would be appreciated.
     
  2. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    When you say running it on main thread everything is fine. How do you run it on main thread?

    one note:
    1. [ReadOnly]
      EntityCommandBuffer...

      That one is wrong. I am not sure why it doesn't give you an exception about it...
     
  3. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    I would probably start by changing all the methods to use .Run instead of Schedule, see if that fixes it.
    Also easier to debug when stepping through the code when everything is running on one thread.
     
  4. Sarkahn

    Sarkahn

    Joined:
    Jan 9, 2013
    Posts:
    440
    Same logic but in a componentsystem:
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using Unity.Collections;
    4. using Unity.Entities;
    5. using Unity.Mathematics;
    6. using Unity.Transforms;
    7. using UnityEngine;
    8.  
    9. [DisableAutoCreation]
    10. public class MainThreadCollisionSystem : ComponentSystem
    11. {
    12.     NativeMultiHashMap<int, Entity> spatialMap_;
    13.  
    14.     public const int CellSize = 20;
    15.  
    16.     protected override void OnCreate()
    17.     {
    18.         spatialMap_ = new NativeMultiHashMap<int, Entity>(20000, Allocator.Persistent);
    19.     }
    20.  
    21.     protected override void OnDestroy()
    22.     {
    23.         spatialMap_.Dispose();
    24.     }
    25.  
    26.     protected override void OnUpdate()
    27.     {
    28.         spatialMap_.Clear();
    29.  
    30.         Entities.WithAllReadOnly<Translation>().WithAllReadOnly<ECSCollider>().ForEach(
    31.             (Entity e, ref Translation translation, ref ECSCollider coll) =>
    32.             {
    33.                 float2 p = translation.Value.xy;
    34.                 SpatialHashExt.VisitGridIndices(p, coll.radius, e, spatialMap_.Add, CellSize);
    35.             });
    36.  
    37.         var keys = spatialMap_.GetKeyArray(Allocator.TempJob);
    38.  
    39.         NativeMultiHashMapIterator<int> it;
    40.      
    41.         for ( int keyIndex = 0; keyIndex < keys.Length; ++keyIndex )
    42.         {
    43.             Entity curr;
    44.             if (spatialMap_.TryGetFirstValue(keys[keyIndex], out curr, out it))
    45.             {
    46.                 Entity next;
    47.                 while (spatialMap_.TryGetNextValue(out next, ref it))
    48.                 {
    49.                     var ar = EntityManager.GetComponentData<ECSCollider>(curr).radius;
    50.                     var br = EntityManager.GetComponentData<ECSCollider>(next).radius;
    51.                     var a = EntityManager.GetComponentData<Translation>(curr).Value;
    52.                     var b = EntityManager.GetComponentData<Translation>(next).Value;
    53.  
    54.                     if( Utils.CirclesOverlap(a, ar, b, br))
    55.                     {
    56.                         EntityManager.AddComponent(curr, typeof(CollisionTag));
    57.                         EntityManager.AddComponent(next, typeof(CollisionTag));
    58.                     }
    59.                 }
    60.             }
    61.         }
    62.  
    63.         keys.Dispose();
    64.  
    65.  
    66.  
    67.     }
    68.  
    69.  
    70. }
    71.  
    This produces the results I showed in he third gif with the same CollisionHandling system.

    I will try this when I get off work and get back to you, thanks
     
  5. Sarkahn

    Sarkahn

    Joined:
    Jan 9, 2013
    Posts:
    440
    Alright after a lot of debugging and help from sngdan on the discord I was able to resolve my problem. Of course it has nothing to do with jobs and was just a dumb error on my part.

    For my collision system I want to test each entity in a cell against every other entity in a cell. What my code was originally doing instead is testing the FIRST entity in a cell against every other entity and then stopping. The reason my main thread version appeared to work is that by pure chance my large collider happened to be the first entity in the cell every frame, so it would always be tested against all bullets.

    I will say that working with the NativeMultiHashmap is very awkward, at least in my case. All I want to do is access each set of values as a list. There is the "IJobNativeMultiHashMapVisitKeyValue" job but since it only accesses each individual element one at a time it wasn't useful in my case. In fact I have a hard time imagining it's super useful in most cases, but I'll chalk that up to a lack of imagination/experience on my part. Eventually i just did it myself - for each cell I create a list, add the entities for that cell, then operate on them like normal.

    It was suggested to me that just using dynamic buffers instead of a NativeMultiHashmap is probably a better solution. It will probably work faster and with the Hashmap you need to set a hard limit on the number of values when you create it. You could always re-create the Hashmap with a larger limit if needed, but a dynamic buffers solution would just do that for you under the hood. If I did this again I would probably use dynamic buffers instead.

    In the end none of it was technically an ECS problem but here's the fixed code anyways, it's a working jobified collision system. It's very simple and naive by design and uses a spatial map for the broad phase test.

    Code (CSharp):
    1. using Unity.Burst;
    2. using Unity.Collections;
    3. using Unity.Entities;
    4. using Unity.Jobs;
    5. using Unity.Mathematics;
    6. using Unity.Transforms;
    7. using UnityEngine;
    8.  
    9. //[DisableAutoCreation]
    10. public class CollisionSystem : JobComponentSystem
    11. {
    12.     NativeMultiHashMap<int, Entity> spatialMap_;
    13.     NativeList<int> keysList_;
    14.  
    15.     static readonly int CellSize = 20;
    16.  
    17.     EndSimulationEntityCommandBufferSystem initBufferSystem_;
    18.  
    19.     [BurstCompile]
    20.     struct BuildSpatialMap : IJobForEachWithEntity<Translation, ECSCollider>
    21.     {
    22.         public NativeMultiHashMap<int, Entity>.Concurrent spatialMap;
    23.  
    24.         public void Execute(Entity e, int chunkIndex, [ReadOnly] ref Translation translation, [ReadOnly] ref ECSCollider coll)
    25.         {
    26.             float2 p = translation.Value.xy;
    27.          
    28.             Utils.VisitGridIndices(p, coll.radius, e, spatialMap.Add, CellSize);
    29.         }
    30.     }
    31.  
    32.     [BurstCompile]
    33.     struct InitializeKeysList : IJob
    34.     {
    35.         public NativeList<int> list;
    36.  
    37.         [ReadOnly]
    38.         public NativeMultiHashMap<int, Entity> spatialMap;
    39.  
    40.         public void Execute()
    41.         {
    42.             var keys = spatialMap.GetKeyArray(Allocator.Temp);
    43.  
    44.             if (keys.Length == 0)
    45.                 return;
    46.          
    47.             list.Add(keys[0]);
    48.             int lastKey = keys[0];
    49.  
    50.             for ( int i = 0; i < keys.Length; ++i )
    51.             {
    52.                 if (lastKey == keys[i] || list.Contains(keys[i]))
    53.                     continue;
    54.                 lastKey = keys[i];
    55.                 list.Add(keys[i]);
    56.             }
    57.          
    58.         }
    59.     };
    60.  
    61.     //[BurstCompile]
    62.     struct GenerateCollisionData : IJobParallelForDefer
    63.     {
    64.         [ReadOnly]
    65.         public NativeMultiHashMap<int, Entity> spatialMap;
    66.  
    67.         [ReadOnly]
    68.         public NativeArray<int> keys;
    69.  
    70.         [ReadOnly]
    71.         public ComponentDataFromEntity<ECSCollider> colliderFromEntity;
    72.  
    73.         [ReadOnly]
    74.         public ComponentDataFromEntity<Translation> posFromEntity;
    75.      
    76.         [WriteOnly]
    77.         public NativeQueue<CollisionData>.Concurrent dataQueue;
    78.  
    79.         public EntityCommandBuffer.Concurrent commandBuffer;
    80.      
    81.         public void Execute(int index)
    82.         {
    83.             NativeList<Entity> entities = new NativeList<Entity>(Allocator.Temp);
    84.  
    85.             NativeMultiHashMapIterator<int> it;
    86.  
    87.             Entity curr;
    88.             if( spatialMap.TryGetFirstValue(keys[index], out curr, out it ))
    89.             {
    90.                 entities.Add(curr);
    91.                 Entity next;
    92.                 while (spatialMap.TryGetNextValue(out next, ref it))
    93.                 {
    94.                     var aColl = colliderFromEntity[curr];
    95.                     var bColl = colliderFromEntity[next];
    96.                     var a = posFromEntity[curr].Value;
    97.                     var b = posFromEntity[next].Value;
    98.  
    99.                     entities.Add(next);
    100.                 }
    101.             }
    102.  
    103.             for( int i = 0; i < entities.Length; ++i )
    104.             {
    105.                 for( int j = i + 1; j < entities.Length; ++j )
    106.                 {
    107.                     var a = entities[i];
    108.                     var b = entities[j];
    109.  
    110.                     var aColl = colliderFromEntity[a];
    111.                     var bColl = colliderFromEntity[b];
    112.                     var aPos = posFromEntity[a].Value;
    113.                     var bPos = posFromEntity[b].Value;
    114.  
    115.                     if( Utils.CirclesOverlap(aPos, aColl.radius, bPos, bColl.radius) )
    116.                     {
    117.                         commandBuffer.AddComponent(index, a, new CollisionTag());
    118.                         commandBuffer.AddComponent(index, b, new CollisionTag());
    119.                     }
    120.                 }
    121.             }
    122.         }
    123.     }
    124.  
    125.     protected override void OnCreate()
    126.     {
    127.         spatialMap_ = new NativeMultiHashMap<int, Entity>(5000, Allocator.Persistent);
    128.         keysList_ = new NativeList<int>(Allocator.Persistent);
    129.         initBufferSystem_ = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    130.     }
    131.  
    132.     protected override void OnDestroy()
    133.     {
    134.         spatialMap_.Dispose();
    135.         keysList_.Dispose();
    136.      
    137.     }
    138.  
    139.     protected override JobHandle OnUpdate(JobHandle inputDependencies)
    140.     {
    141.         var job = inputDependencies;
    142.  
    143.         spatialMap_.Clear();
    144.         keysList_.Clear();
    145.  
    146.         // Build our spatial map
    147.         job = new BuildSpatialMap
    148.         {
    149.             spatialMap = spatialMap_.ToConcurrent(),
    150.         }.Schedule(this, job);
    151.  
    152.         // Initialize the size of our keys list. We can't know the size of our list
    153.         // during schedule time so we need to use "DeferredJobArray"
    154.         // Example in Packages/Jobs/Unity.Jobs.Test/NativeListDeferredArrayTests
    155.         job = new InitializeKeysList
    156.         {
    157.             list = keysList_,
    158.             spatialMap = spatialMap_,
    159.         }.Schedule(job);
    160.  
    161.         // Check for collisions and tag entities
    162.         job = new GenerateCollisionData
    163.         {
    164.             spatialMap = spatialMap_,
    165.             keys = keysList_.AsDeferredJobArray(),
    166.             colliderFromEntity = GetComponentDataFromEntity<ECSCollider>(true),
    167.             posFromEntity = GetComponentDataFromEntity<Translation>(true),
    168.             commandBuffer = initBufferSystem_.CreateCommandBuffer().ToConcurrent(),
    169.         }.Schedule(keysList_, 5, job);
    170.  
    171.         return job;
    172.     }
    173. }

    And the results:
    ftQpUq4EH9.gif
     
  6. Sarkahn

    Sarkahn

    Joined:
    Jan 9, 2013
    Posts:
    440
    Also I'm not sure if this is a bug or not but the keys returned by NativeMultiHashMap.GetKeyArray will contain duplicates if you've performed multiple adds with the same key. IE:

    Code (CSharp):
    1.     [Test]
    2.     public void GetKeysReturnsDuplicates()
    3.     {
    4.         NativeMultiHashMap<int, int> map = new NativeMultiHashMap<int, int>(100, Allocator.Temp);
    5.  
    6.         map.Add(0, 10);
    7.         map.Add(0, 11);
    8.         map.Add(0, 12);
    9.         map.Add(0, 13);
    10.         map.Add(0, 14);
    11.  
    12.         var keys = map.GetKeyArray(Allocator.Temp);
    13.  
    14.         Assert.AreEqual(keys.Length, 1); // Fails wth was 5 expected 1
    15.     }
     
  7. sngdan

    sngdan

    Joined:
    Feb 7, 2014
    Posts:
    1,154
    Glad you got it working - I had a quick look and well, it's not the cleanest nor fastest implementation, but as long as it works, congrats !

    .GetKeyArray is one version after the last API I used, but I looked into this in the release notes back then. If I recall correctly you have also something that gets you an array of values? Then those two arrays align, I think there is also a version without duplicates....

    keyArray = 0, 0, 0, 0, 0, 1, 1, 2
    valueArray = 10, 11, 12, 13, 14, 100, 101, 201

    The red additions expand on your example to illustrate ---
     
    Sarkahn likes this.
  8. Sarkahn

    Sarkahn

    Joined:
    Jan 9, 2013
    Posts:
    440
    Ahh okay, that makes sense. And you're right, I never noticed it but there is NativeMultiHashMap.GetUniqueKeyArray. Good to know, thank you.

    Edit: Just tested this and while GetUniqeKeyArray works it unfortunately returns a System.Tuple, which is a reference type, so it will prevent burst jobs from compiling. I'm not sure why it doesn't return a ValueTuple.
     
    Last edited: May 25, 2019