Table of Contents

Field Attribute & Schema Integration

Declare a component field as spatially indexed, validated against schema rules the moment the component is registered.

Status: โœ… Implemented ยท Visibility: Public ยท Level: ๐Ÿ”ต Core ยท Category: Spatial

๐ŸŽฏ What it solves

A spatial index needs to know, before a single entity exists, which field holds an entity's bounds, which shape and precision that field uses, whether the archetype belongs in the static or the dynamic half of a cell, and which category bits it answers to. Wiring this up imperatively โ€” manual index construction, manual field-offset bookkeeping, ad-hoc checks scattered through game code โ€” is exactly the kind of boilerplate Typhon's schema reflection already removes for secondary indexes and foreign keys. [SpatialIndex] extends that same declarative path to spatial fields, and pushes every configuration mistake to startup instead of runtime.

โš™๏ธ How it works (in brief)

Decorate one field of a supported geometry type (AABB2F/AABB3F/BSphere2F/BSphere3F, plus their f64 equivalents) with [SpatialIndex(cellSize)], optionally setting Mode and Category. At component registration the engine checks two things โ€” that the field's type is a supported geometry type, and that the component is not StorageMode.Transient โ€” and any violation throws immediately, before the schema is built. On success, the attribute's values flow into the component's DBComponentDefinition.SpatialField and into a SpatialIndexState on the component table.

That state is metadata, and registration allocates nothing. A component carrying [SpatialIndex] reserves no StorageSegmentKind.Spatial segment at all, nothing about the state is persisted, and the load path builds it exactly as the create path does. Entities are indexed by the per-cell cluster broadphase, which the grid owns rather than the component.

๐Ÿ’ป Usage

[Component("Game.Ship", revision: 1, StorageMode = StorageMode.SingleVersion)]
public struct ShipComponent
{
    [Field] public String64 Name;

    // cellSize is a constructor arg; Mode and Category are named properties. Only Mode and Category drive behaviour.
    [Field] [SpatialIndex(Mode = SpatialMode.Dynamic, Category = 1u << 2)]
    public AABB3F Bounds;
}

dbe.RegisterComponentFromAccessor<ShipComponent>();   // throws here on any violation, not later

// Inspecting the reflected metadata:
DBComponentDefinition def = dbe.DBD.GetComponent("Game.Ship", revision: 1);
DBComponentDefinition.Field spatial = def.SpatialField;
SpatialFieldType type = spatial.SpatialFieldType;     // AABB3F
[SpatialIndex] arg Default Effect
cellSize (ctor) 0 Carried in schema metadata and sizes nothing. The cell size that matters is the engine-wide SpatialGridConfig.CellSize.
Mode SpatialMode.Dynamic Selects which half of every cell this archetype's clusters occupy. Static is skipped by the tick fence's bound-refresh, drift and repair passes; Dynamic is visited by all three. See Static / Dynamic Separation.
Category uint.MaxValue Archetype-level bitmask consumed by the cluster broadphase to skip whole clusters

โš ๏ธ Guarantees & limits

  • Exactly 8 supported field types: AABB2F/AABB3F/BSphere2F/BSphere3F and their f64 (...D) equivalents โ€” any other field type throws InvalidOperationException at registration.
  • One [SpatialIndex] field per component is what the engine models: DBComponentDefinition.SpatialField is a single field reference. A second one is not rejected โ€” no validation checks for it, and the last field the reflection pass visits silently wins. Treat one-per-component as a contract you keep, not one the engine enforces.
  • Not supported on StorageMode.Transient โ€” registering a [SpatialIndex] field on a Transient component throws InvalidOperationException.
  • Validation runs once, at RegisterComponentFromAccessor/RegisterComponentByType time, before any UnitOfWork exists โ€” by the time the schema is usable, the spatial configuration is already known-good.
  • BSphere* fields are accepted directly โ€” the engine converts to an enclosing AABB internally for indexing; the component's stored field stays a sphere, unchanged.
  • The attribute only declares configuration; it maintains nothing โ€” see the sub-feature below for when a write reaches the index, and Per-Cell Cluster Index Internals for the structure a dense cell promotes to.

๐Ÿงช Tests

  • SpatialFieldTypeTests โ€” [SpatialIndex] reflection (cellSize), FieldType.FromType mapping for all 8 supported types, SpatialFieldInfo.ToVariant/IsSphere
  • SpatialEcsIntegrationTests โ€” schema validation at registration (Schema_TransientWithSpatialIndex_Throws, Schema_ValidSpatialField_CreatesSpatialIndex, Schema_NoSpatialField_NullSpatialIndex)
  • EntityIndexRetirementTests โ€” ASpatialComponentAllocatesNoSpatialSegment counts segments by kind, so the no-segment guarantee above is checked rather than asserted in prose