Skip to content

Spatial Grid

The spatialGrid resource maps cells to the entities occupying them. SpatialPlugin installs it for navigation, but it is also useful on its own for coarse proximity queries.

Install GridPlugin when you only need the index:

import { GridPlugin } from '@shell/spatial';
world.addPlugin(
GridPlugin.config({
cellSize: 64,
width: 2048,
height: 2048,
}),
);

Add both Position and GridCell to every entity that should be indexed:

import { Define, Position } from '@shell/engine-2d';
import { GridCell } from '@shell/spatial';
const IndexedEntity = Define.prefab().with(Position).with(GridCell);

The plugin synchronizes GridCell and rebuilds the index during FixedPreUpdate.

Convert a world position to cell coordinates, then inspect that cell:

const grid = resources.get('spatialGrid');
const [cellX, cellY] = grid.worldToCell(position.x, position.y);
const entityIndices = grid.getEntitiesInCell(cellX, cellY);

The returned values are compact entity indices. Resolve them through the current world’s entities resource when an Entity object is needed:

for (const index of entityIndices) {
const entity = resources.get('entities').getByIndex(index);
if (!entity) continue;
// Inspect the candidate entity.
}

Entity indices can be reused after despawn. Resolve and use grid results within the current spatial snapshot rather than retaining them as durable identifiers.

getNeighborEntities() includes the center cell and an extent measured in cells:

const nearby: number[] = [];
grid.getNeighborEntities(cellX, cellY, nearby); // 3 x 3 cells
grid.getNeighborEntities(cellX, cellY, nearby, 2); // 5 x 5 cells

Pass a reusable output array in update loops. The method clears its logical contents before filling it and returns the same array.

Use getEntitiesInWorldBounds() when the search area is expressed in world units:

const candidates: number[] = [];
grid.getEntitiesInWorldBounds(
position.x - 100,
position.y - 100,
position.x + 100,
position.y + 100,
candidates,
);

Maximum bounds are exclusive. Bounds are clipped to the configured world, and a rectangle entirely outside it returns an empty array.

Grid queries return broad-phase candidates from overlapping cells. Apply an exact distance, shape, component, or gameplay test after the query when false positives matter.

The grid is a fixed-step snapshot. Changes to Position made after FixedPreUpdate appear in the index on the next fixed step. Schedule systems accordingly when they require freshly synchronized membership.

worldToCell() clamps finite coordinates to the edge of the configured grid. Cell-based methods instead require integer, in-bounds coordinates and report invalid input. The resource exposes cellSize, width, height, cols, rows, and totalCells for bounds-aware gameplay.

The package also exports FlowfieldPlugin, ObstaclePlugin, LineOfSightPlugin, and SeparationPlugin. Their dependencies install the grid automatically, but plugin configuration is first-install-wins. Install a configured GridPlugin before dependent standalone plugins when you need non-default dimensions:

import { FlowfieldPlugin, GridPlugin, SeparationPlugin } from '@shell/spatial';
world.addPlugin(
GridPlugin.config({ cellSize: 32, width: 1024, height: 1024 }),
FlowfieldPlugin,
SeparationPlugin,
);

For the complete feature set, configure SpatialPlugin instead.