Hi all,
Often times you may have a situation where you need to find entities that are within a given radius. For example in my case, a turret AI that needs to search for targets within a specified radius.
My first instinct was to use OverlapSphere, but another potential solution I thought of was to just check the distance of every entity with a certain tag using an EntityQuery. In addition for every entity I also have to do a raycast to ensure that the AI actually had line-of-sight to the target entity.
I made a quick test of this and frankly the performance difference I am seeing is far more than I expected, see for yourself:
Doing an entity query and checking the distance (square distance, really) is over 23 times faster than doing an overlap sphere. For reference, here is what the scene looks like, just a random splattering of objects:
There are 50 spheres and 50 cubes. The spheres are the main entities. They are the origins of the overlap spheres and distance checks that check for all other spheres. The cubes are just there to simulate some kind of environment to raycast against.
Finally, here is my code I am using for testing.erything.
OverlapSphere system + Tag component and system group
using Unity.Collections;
using Unity.Entities;
using Unity.Physics;
using Unity.Physics.Systems;
using Unity.Transforms;
[GenerateAuthoringComponent]
public struct TestOverlapSphereVsQuerySystemTag : IComponentData { }
public class TestPerformanceSystemGroup : ComponentSystemGroup { }
[UpdateInGroup(typeof(TestPerformanceSystemGroup))]
public class TestPerformanceOverlapSphereSystem : SystemBase
{
private const float testSphereRadius = 100f;
private BuildPhysicsWorld buildPhysicsWorldSystem;
protected override void OnCreate()
{
buildPhysicsWorldSystem = World.GetOrCreateSystem<BuildPhysicsWorld>();
}
protected override void OnUpdate()
{
//Using overlap sphere
PhysicsWorld physicsWorld = buildPhysicsWorldSystem.PhysicsWorld;
NativeList<DistanceHit> outHits = new NativeList<DistanceHit>(Allocator.TempJob);
Entities
.WithAll<TestOverlapSphereVsQuerySystemTag>()
.WithReadOnly(physicsWorld)
.WithDisposeOnCompletion(outHits)
.ForEach((in LocalToWorld localToWorld) =>
{
CollisionFilter overlapSphereCollisionFilter = new CollisionFilter()
{
BelongsTo = 1u << 31,
CollidesWith = 1 << 2,
GroupIndex = 0
};
if (physicsWorld.OverlapSphere(localToWorld.Position, testSphereRadius, ref outHits, overlapSphereCollisionFilter))
{
for (int i = 0; i < outHits.Length; i++)
{
LocalToWorld l2w = GetComponent<LocalToWorld>(outHits[i].Entity);
CollisionFilter raycastCollisionFilter = new CollisionFilter()
{
BelongsTo = 1u << 31,
CollidesWith = 1 << 1,
GroupIndex = 0
};
RaycastInput raycastInput = new RaycastInput()
{
Start = localToWorld.Position,
End = l2w.Position,
Filter = raycastCollisionFilter
};
if (physicsWorld.CastRay(raycastInput, out Unity.Physics.RaycastHit raycastHit))
{
}
}
}
}).Run();
}
}
EntityQuery system:
[UpdateInGroup(typeof(TestPerformanceSystemGroup))]
public class TestPerformanceEntityQuerySystem : SystemBase
{
private const float testSphereRadius = 100f;
private BuildPhysicsWorld buildPhysicsWorldSystem;
private EntityQuery entityQuery;
protected override void OnCreate()
{
buildPhysicsWorldSystem = World.GetOrCreateSystem<BuildPhysicsWorld>();
entityQuery = GetEntityQuery(typeof(TestOverlapSphereVsQuerySystemTag));
}
protected override void OnUpdate()
{
//Using entity query
PhysicsWorld physicsWorld = buildPhysicsWorldSystem.PhysicsWorld;
NativeArray<Entity> entityQueryResults = entityQuery.ToEntityArray(Allocator.TempJob);
Entities
.WithAll<TestOverlapSphereVsQuerySystemTag>()
.WithDisposeOnCompletion(entityQueryResults)
.WithReadOnly(physicsWorld)
.ForEach((in LocalToWorld localToWorld) =>
{
for (int i = 0; i < entityQueryResults.Length; i++)
{
LocalToWorld l2w = GetComponent<LocalToWorld>(entityQueryResults[i]);
float sqrDist = Unity.Mathematics.math.lengthsq(l2w.Position - localToWorld.Position);
if (sqrDist < testSphereRadius * testSphereRadius)
{
CollisionFilter raycastCollisionFilter = new CollisionFilter()
{
BelongsTo = 1u << 31,
CollidesWith = 1 << 1,
GroupIndex = 0
};
RaycastInput raycastInput = new RaycastInput()
{
Start = localToWorld.Position,
End = l2w.Position,
Filter = raycastCollisionFilter
};
if (physicsWorld.CastRay(raycastInput, out Unity.Physics.RaycastHit raycastHit))
{
}
}
}
}).Run();
}
}
MonoBehaviour that just spawns everything:
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
public class TestOverlapSphereVsQueryController : MonoBehaviour
{
public GameObject TargetPrefab;
public GameObject EnvironmentPrefab;
public int TargetCount;
public int EnvironmentCount;
public float Radius;
private Entity targetPrefabEntity;
private Entity environmentPrefabEntity;
private EntityManager entityManager;
private BlobAssetStore blobAssetStore;
private void Awake()
{
entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
blobAssetStore = new BlobAssetStore();
GameObjectConversionSettings settings = GameObjectConversionSettings.FromWorld(World.DefaultGameObjectInjectionWorld, blobAssetStore);
targetPrefabEntity = GameObjectConversionUtility.ConvertGameObjectHierarchy(TargetPrefab, settings);
environmentPrefabEntity = GameObjectConversionUtility.ConvertGameObjectHierarchy(EnvironmentPrefab, settings);
}
private void Start()
{
Unity.Mathematics.Random r = Unity.Mathematics.Random.CreateFromIndex(0);
for (int i = 0; i < TargetCount; i++)
{
Entity targetEntity = entityManager.Instantiate(targetPrefabEntity);
Translation randomPos = new Translation()
{
Value = (float3)transform.position + r.NextFloat3Direction() * r.NextFloat(0, Radius),
};
entityManager.AddComponentData(targetEntity, randomPos);
entityManager.AddComponent<TestOverlapSphereVsQuerySystemTag>(targetEntity);
}
for (int i = 0; i < EnvironmentCount; i++)
{
Entity envEntity = entityManager.Instantiate(environmentPrefabEntity);
Translation randomPos = new Translation()
{
Value = (float3)transform.position + r.NextFloat3Direction() * r.NextFloat(0, Radius),
};
entityManager.AddComponentData(envEntity, randomPos);
}
}
private void OnDestroy()
{
blobAssetStore.Dispose();
}
}
All this may be obvious, but I was just really surprised at how massive the difference apparently is. Especially since using OverlapSphere may be people’s first choice when doing this kind of thing. I’ve tested using various numbers of objects spread over different sized areas and the EntityQuery always wins. I’d be interested to hear if anyone has any input.








