The following code is an example of how we can do parallel spherecasts in a job (kinda untested because I simplified the code a bit for this post). It’s a projectile hit detection job.
In this case, we can’t just rely on the closest hit, because we want to be able to filter out hits based on specific gameplay considerations (ignore characters that are in a “dodging” state, ignore specific entities regardless of physics layers, ignore hits with a certain dot product threshold, do some piercing-bullets logic, etc…). So as a consequence of that need, we need a collector that can store several hits in order to manually filter them.
Code
public struct HitDetectionJob : IJobForEach<Translation, Rotation, Projectile>
{
[ReadOnly]
public PhysicsWorld PhysicsWorld;
public void Execute(ref Translation translation, ref Rotation rotation, ref Projectile projectile)
{
bool foundHit = false;
ColliderCastHit closestValidHit = default;
closestValidHit.Fraction = float.MaxValue;
// HERE
NativeArray<ColliderCastHit> castHits = new NativeArray<ColliderCastHit>(16, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
// HERE
MaxHitsCollector<ColliderCastHit> collector = new MaxHitsCollector<ColliderCastHit>(1.0f, ref castHits);
SphereGeometry sphereGeom = new SphereGeometry
{
Center = default,
Radius = projectile.radius,
};
// HERE
BlobAssetReference<Unity.Physics.Collider> sphereCollider = SphereCollider.Create(sphereGeom, projectile.filter);
ColliderCastInput input = new ColliderCastInput()
{
Collider = (Collider*)sphereCollider.GetUnsafePtr(),
Orientation = quaternion.identity,
Start = projectile.previousPos,
End = translation.Value,
};
if (PhysicsWorld.CollisionWorld.CastCollider(input, ref collector))
{
for (int i = 0; i < collector.NumHits; i++)
{
ColliderCastHit hit = collector.AllHits[i];
if (hit.Fraction > 0f && hit.Fraction < closestValidHit.Fraction)
{
if ( /* do some additional filtering here based on some game-specific rules */)
{
closestValidHit = hit;
foundHit = true;
}
}
}
}
if (foundHit)
{
// here, "closestValidHit" is our closest valid filtered hit
}
}
}
However, I get the feeling that it’s not an efficient way to do this. The places with a “// HERE” comment in the code are the parts I’m concerned with. For each projectile, we allocate new hits arrays, create a new collector, and create a new sphere collider to cast with. I don’t have a solid understanding of how costly any of these things are
I have several thoughts on alternatives:
-
Would there be a way to allocate one hits array and one MaxHitsCollector per “thread” when the job starts, instead of allocating for every single projectile in the game?
-
Would it be safe/efficient to pass just one SphereCollider to the entire IJobForEach and resize it for every entity?
-
Would it be better if every projectile already had their physicsShape on their entity, instead of creating a new sphere collider for each? Even if we have 10k projectiles in game?
-
Is it maybe a better to launch several simultaneous IJobs (based on a thread count hint), each handling hit detection for a subset of all projectiles, and each with their pre-initialized hits array, hits collector, and sphere collider? But then if two jobs try to apply damage to the same entity, we’ll have a problem…
-
Actually I’m realizing just now that I can’t safely apply damage on hit with the approach posted above, because there would be parallel writing issues. I guess I’d need to write damage events into a concurrent NativeQueue and process those events later in an IJob
How would you change this to make it more performant?