Spatial Query Predicates
R-Tree-backed AABB, radius, and ray filters attached directly to a fluent ECS query.
Status: โ Implemented ยท Visibility: Public ยท Level: ๐ฃ Advanced ยท Category: Querying
๐ฏ What it solves
Game-server and simulation workloads constantly ask spatial questions โ "what's in this room", "what's within aggro range", "what does this ray hit" โ that a B+Tree field index cannot answer efficiently. Brute-force scanning every entity's position component to test bounds is O(n) per query and falls apart well before 10K entities at 60Hz. Spatial Query Predicates give EcsQuery three index-driven spatial filters (AABB overlap, radius/sphere, ray intersection) that run against a dedicated R-Tree instead of a linear scan, and compose with the rest of the query builder (archetype constraints, field predicates, opaque post-filters).
โ๏ธ How it works (in brief)
Mark the field that defines an entity's bounds with [SpatialIndex]; the engine maintains an R-Tree per component table, updated as entities spawn, move, and despawn. WhereInAABB<T>, WhereNearby<T>, and WhereRay<T> attach one spatial predicate to the query โ the tree (not a table scan) produces the candidate set, which is then narrowed by archetype mask, any .Where()/.WhereField() predicate chained onto the same query (AND semantics), and visibility. Only one spatial predicate is allowed per query; static (rarely-moving) and dynamic entities are indexed in separate trees but queried transparently together.
๐ป Usage
[Component("Game.Position", 1)]
public struct Position
{
[SpatialIndex(margin: 0.5f)]
public float X, Y, Z;
}
using var t = dbe.CreateQuickTransaction();
// AABB overlap โ entities whose Position bounds intersect the box
var inRoom = t.Query<UnitArch>()
.WhereInAABB<Position>(minX: 0, minY: 0, minZ: 0, maxX: 50, maxY: 0, maxZ: 50)
.Execute(); // โ HashSet<EntityId>
// Radius โ entities within range of a point
var nearby = t.Query<UnitArch>()
.WhereNearby<Position>(centerX: 10, centerY: 0, centerZ: 10, radius: 15)
.Without<Dead>()
.Execute();
// Ray โ first-hit-style candidate set along a direction
var hit = t.Query<UnitArch>()
.WhereRay<Position>(originX: 0, originY: 1, originZ: 0, dirX: 1, dirY: 0, dirZ: 0, maxDist: 100)
.Any();
Constructor arg ([SpatialIndex]) |
Default | Effect |
|---|---|---|
margin |
required | Fat-AABB enlargement; absorbs small moves without a tree update |
cellSize |
0 (auto) |
Bucket size for the static/bulk-loaded tree variant |
Mode |
SpatialMode.Dynamic |
Static skips per-tick update maintenance for rarely-moving entities |
Category |
uint.MaxValue |
Archetype-level bitmask; lets a per-cluster broadphase skip whole archetype clusters that don't overlap the query |
โ ๏ธ Guarantees & limits
- One spatial predicate per query โ a second
WhereNearby/WhereInAABB/WhereRaycall throwsInvalidOperationException; run separate queries and combine in application code. - Composable with
Where/WhereFieldโ both are applied as an AND post-filter over the spatial candidate set, not a second index probe. - Archetype-mask filtered โ results respect
.With<T>()/.Without<T>()/.Exclude<T>()the same as any other query. - Not supported on
foreachโ iterating the query directly only applies the archetype scan +.Where(); a spatial predicate throws. Call.Execute()(or.Any()/.Count()) instead. - Not supported with
WhereFieldonToView()โ a reactive view cannot combine an indexed field predicate with a spatial predicate; a spatial-only (or spatial + opaqueWhere) view falls back to full re-query (pull mode) on everyRefresh(), not incremental delta tracking. - Not supported with
ExecuteOrderedโ spatial predicates don't carry an ordering; useExecute()and sort in application code if needed. - Sub-microsecond at 10K entities; ~600ยตs total spatial budget at 100K entities for a 60Hz tick (3.6% of frame).
- kNN-style "nearest N" is not exposed at this layer โ only set-membership filters (AABB/radius/ray).
๐งช Tests
- SpatialEcsIntegrationTests โ
WhereInAABB/Count/Any, composition withWhere/WhereField, and the one-spatial-predicate-per-query guard
๐ Related
- Source: src/Typhon.Engine/Ecs/public/EcsQuery.cs
- Sibling: Spatial Query API (AABB / Radius / Ray / Frustum / kNN / Count) โ the underlying R-Tree query algorithms this feature exposes through the fluent builder