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):

Multiple shooters (JobComponentSystem):

Multiple Shooters (Main Thread):

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:
CollisionSystem
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
//[DisableAutoCreation]
public class CollisionSystem : JobComponentSystem
{
NativeMultiHashMap<int, Entity> spatialMap_;
NativeQueue<CollisionData> dataQueue_;
NativeList<int> keysList_;
static readonly int CellSize = 20;
EndSimulationEntityCommandBufferSystem initBufferSystem_;
[BurstCompile]
struct BuildSpatialMap : IJobForEachWithEntity<Translation, ECSCollider>
{
public NativeMultiHashMap<int, Entity>.Concurrent spatialMap;
public void Execute(Entity e, int chunkIndex, [ReadOnly] ref Translation translation, [ReadOnly] ref ECSCollider coll)
{
float2 p = translation.Value.xy;
VisitGridIndices(p, coll.radius, e, spatialMap.Add, CellSize);
}
void VisitGridIndices<TValue>(
float2 pos, float radius, TValue val,
System.Action<int, TValue> callback, int cellSize)
{
float2 p = pos;
int2 min = (int2)math.floor((p - radius) / cellSize);
int2 max = (int2)math.ceil((p + radius) / cellSize);
int count = max.x - min.x;
for (int x = min.x; x < max.x; ++x)
{
for (int y = min.y; y < max.y; ++y)
{
int2 cell = new int2(x, y);
int hash = cell.GetHashCode();
callback(hash, val);
}
}
}
}
[BurstCompile]
struct InitializeKeysList : IJob
{
public NativeList<int> list;
[ReadOnly]
public NativeMultiHashMap<int, Entity> spatialMap;
public void Execute()
{
var keys = spatialMap.GetKeyArray(Allocator.Temp);
list.ResizeUninitialized(spatialMap.Length);
for (int i = 0; i < list.Length; ++i)
list[i] = keys[i];
}
};
[BurstCompile]
struct GenerateCollisionData : IJobParallelForDefer
{
[ReadOnly]
public NativeMultiHashMap<int, Entity> spatialMap;
[ReadOnly]
public NativeArray<int> keys;
[ReadOnly]
public ComponentDataFromEntity<ECSCollider> colliderFromEntity;
[ReadOnly]
public ComponentDataFromEntity<Translation> posFromEntity;
[WriteOnly]
public NativeQueue<CollisionData>.Concurrent dataQueue;
public void Execute(int index)
{
NativeMultiHashMapIterator<int> it;
Entity curr;
if( spatialMap.TryGetFirstValue(keys[index], out curr, out it ))
{
Entity next;
while( spatialMap.TryGetNextValue(out next, ref it))
{
var aColl = colliderFromEntity[curr];
var bColl = colliderFromEntity[next];
var a = posFromEntity[curr].Value;
var b = posFromEntity[next].Value;
// TODO: Collision flags
if( CircleOverlap(a, aColl.radius, b, bColl.radius ))
{
dataQueue.Enqueue(new CollisionData { a = curr, b = next });
}
curr = next;
}
}
bool CircleOverlap(float3 aPos, float aRadius, float3 bPos, float bRadius)
{
float r = aRadius + bRadius;
float dx = bPos.x - aPos.x;
float dy = aPos.y - bPos.y;
return dx * dx + dy * dy < r * r;
}
}
}
//[BurstCompile]
struct ProcessCollisionData : IJob
{
public NativeQueue<CollisionData> dataQueue;
[ReadOnly]
public EntityCommandBuffer commandBuffer;
public void Execute()
{
CollisionData data;
while(dataQueue.TryDequeue(out data))
{
commandBuffer.AddComponent<CollisionTag>(data.a, new CollisionTag());
commandBuffer.AddComponent<CollisionTag>(data.b, new CollisionTag());
}
}
}
protected override void OnCreate()
{
spatialMap_ = new NativeMultiHashMap<int, Entity>(5000, Allocator.Persistent);
dataQueue_ = new NativeQueue<CollisionData>(Allocator.Persistent);
keysList_ = new NativeList<int>(Allocator.Persistent);
initBufferSystem_ = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
}
protected override void OnDestroy()
{
spatialMap_.Dispose();
dataQueue_.Dispose();
keysList_.Dispose();
}
protected override JobHandle OnUpdate(JobHandle inputDependencies)
{
var job = inputDependencies;
spatialMap_.Clear();
dataQueue_.Clear();
keysList_.Clear();
// Build our spatial map
job = new BuildSpatialMap
{
spatialMap = spatialMap_.ToConcurrent(),
}.Schedule(this, job);
// Initialize the size of our keys list. We can't know the size of our list
// during schedule time so we need to use "DeferredJobArray"
// Example in Packages/Jobs/Unity.Jobs.Test/NativeListDeferredArrayTests
job = new InitializeKeysList
{
list = keysList_,
spatialMap = spatialMap_,
}.Schedule(job);
// Create our collision data (which entities collided) and queue it for the next job
// We don't want to process immediately since that could potentially lead to
// multiple threads trying to write to one entity at the same time
job = new GenerateCollisionData
{
spatialMap = spatialMap_,
keys = keysList_.AsDeferredJobArray(),
colliderFromEntity = GetComponentDataFromEntity<ECSCollider>(true),
posFromEntity = GetComponentDataFromEntity<Translation>(true),
dataQueue = dataQueue_.ToConcurrent(),
}.Schedule(keysList_, 5, job);
// Tags the appropriate entities for our collision handling system to deal with
job = new ProcessCollisionData
{
dataQueue = dataQueue_,
commandBuffer = initBufferSystem_.CreateCommandBuffer(),
}.Schedule(job);
return job;
}
}
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:
CollisionHandling
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
public class BulletCollisionHandlingSystem : ComponentSystem
{
EntityQuery bulletCollisionQuery_;
protected override void OnCreate()
{
bulletCollisionQuery_ = GetEntityQuery(typeof(CollisionTag), typeof(Bullet));
}
protected override void OnUpdate()
{
EntityManager.DestroyEntity(bulletCollisionQuery_);
EntityManager.RemoveComponent<CollisionTag>(bulletCollisionQuery_);
}
}
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.
