Spatial Queries
PhysicsPlugin exposes high-level overlap and raycast resources backed by the collider spatial index. Use these resources for gameplay queries instead of reading the low-level PhysicsWorld directly.
Collision query filters
Section titled “Collision query filters”Overlap queries and raycasts share optional layer, mask, and filter fields.
| Filter | Behavior |
|---|---|
CollisionQueryFilter.Mutual | The query mask accepts the collider layer and the collider mask accepts the query layer |
CollisionQueryFilter.OneWay | Only the query mask must accept the collider layer |
Mutual filtering is the default when both layer and mask are present. One-way filtering is usually more convenient for searches such as “find an enemy”:
const filter = { mask: Layers.Enemy, filter: CollisionQueryFilter.OneWay,};Filtering is disabled when the fields required by the selected mode are omitted.
Circle overlaps
Section titled “Circle overlaps”Access the physicsOverlap resource inside a system:
const overlap = resources.get('physicsOverlap');Use circle() to process hits incrementally. Call cancel() when the query has found enough:
import { CollisionQueryFilter } from '@shell/physics';
let target;
overlap.circle( { x: position.x, y: position.y, radius: 64, mask: Layers.Enemy, filter: CollisionQueryFilter.OneWay, }, (hit, cancel) => { if (hit.entity === player) return; target = hit.entity; cancel(); },);Use circleAll() to collect every hit into a reusable array:
import type { ColliderHit } from '@shell/physics';
const hits: ColliderHit[] = [];
overlap.circleAll( { x: position.x, y: position.y, radius: 64, mask: Layers.Pickup, filter: CollisionQueryFilter.OneWay, }, hits,);circleAll() clears the supplied array before filling it and returns the same array. The query performs exact circle-circle and circle-box tests after broad-phase lookup. Touching shapes count as overlaps.
ColliderHit objects belong to the spatial index and are reused by later rebuilds. Read them during the query, or copy the entity and values that must survive beyond the current spatial snapshot.
CircleOverlapQuery
Section titled “CircleOverlapQuery”| Field | Type | Description |
|---|---|---|
x, y | number | Center of the query circle |
radius | number | Query radius; negative values are treated as 0 |
layer | number | Optional query layer for mutual filtering |
mask | number | Optional accepted collider layers |
filter | CollisionQueryFilter | Mutual or one-way filtering |
Cone overlaps
Section titled “Cone overlaps”Use cone() to process colliders intersecting a filled circular sector. The direction vector is normalized internally, and angle is the full aperture in radians:
overlap.cone( { x: position.x, y: position.y, directionX: aim.x, directionY: aim.y, radius: 96, angle: Math.PI / 2, mask: Layers.Enemy, filter: CollisionQueryFilter.OneWay, }, (hit, cancel) => { target = hit.entity; cancel(); },);Use coneAll() to collect every hit into a reusable array:
overlap.coneAll( { x: position.x, y: position.y, directionX: aim.x, directionY: aim.y, radius: 96, angle: Math.PI / 2, }, hits,);Cone queries perform exact intersections against circle and axis-aligned box colliders, so a collider can overlap even when its center is outside the cone. Touching either radial edge or the outer arc counts as an overlap. A zero angle is a line segment, angles of 2π or greater are full circles, and negative angles and radii are treated as 0.
ConeOverlapQuery
Section titled “ConeOverlapQuery”| Field | Type | Description |
|---|---|---|
x, y | number | Cone origin |
directionX, directionY | number | Non-zero direction, normalized internally |
radius | number | Query radius; negative values are treated as 0 |
angle | number | Full aperture in radians, clamped to 0–2π |
layer | number | Optional query layer for mutual filtering |
mask | number | Optional accepted collider layers |
filter | CollisionQueryFilter | Mutual or one-way filtering |
Raycasts
Section titled “Raycasts”Access the physicsRaycaster resource:
const raycaster = resources.get('physicsRaycaster');Use closest() for the nearest intersection:
const hit = raycaster.closest({ x: position.x, y: position.y, directionX: aim.x, directionY: aim.y, maxDistance: 500, mask: Layers.World | Layers.Enemy, filter: CollisionQueryFilter.OneWay, exclude: player,});
if (hit) { console.log(hit.entity, hit.distance); console.log(hit.pointX, hit.pointY); console.log(hit.normalX, hit.normalY);}The direction is normalized internally, so distance and maxDistance are always measured in world units.
Use all() to collect intersections sorted from nearest to farthest:
import type { RaycastHit } from '@shell/physics';
const hits: RaycastHit[] = [];
raycaster.all( { x: 0, y: 0, directionX: 1, directionY: 0, maxDistance: 500, mask: Layers.World, filter: CollisionQueryFilter.OneWay, }, hits,);Like circleAll(), all() clears and reuses the supplied array.
RaycastQuery
Section titled “RaycastQuery”| Field | Type | Description |
|---|---|---|
x, y | number | Ray origin |
directionX, directionY | number | Non-zero ray direction |
maxDistance | number | Maximum distance in world units |
exclude | Entity | Optional entity to ignore |
layer | number | Optional query layer for mutual filtering |
mask | number | Optional accepted collider layers |
filter | CollisionQueryFilter | Mutual or one-way filtering |
RaycastHit
Section titled “RaycastHit”| Field | Description |
|---|---|
entity | Entity owning the intersected collider |
shape | 'circle' or 'box' |
distance | Distance from the ray origin |
pointX, pointY | World-space intersection point |
normalX, normalY | Outward surface normal |
Rays starting inside a collider return a hit at distance 0. Intersections exactly at maxDistance are included.
Collider hits
Section titled “Collider hits”Overlap hits include the indexed shape data:
type ColliderHit = | { shape: 'circle'; entity: Entity; x: number; y: number; radius: number; layer: number; mask: number; } | { shape: 'box'; entity: Entity; x: number; y: number; width: number; height: number; layer: number; mask: number; };Snapshot timing
Section titled “Snapshot timing”The spatial index is rebuilt during the fixed-step collision pipeline. Overlaps and raycasts use the latest completed rebuild, not live component storage.
If a system changes a position or collider after the rebuild, queries do not see that change until the next fixed step. Run query systems after PhysicsSet.Collide when they need the current fixed-step snapshot:
export const FindTargetsSystem = Define.system() .after(PhysicsSet.Collide) .update(({ resources }) => { const overlap = resources.get('physicsOverlap'); // Queries now use the spatial index rebuilt this fixed step. });