Obstacles & Terrain
SpatialPlugin can derive the default navigation map from positioned ECS entities. Use Obstacle
for the footprint and optionally add TerrainCost to make that footprint traversable but expensive.
Block navigation
Section titled “Block navigation”An obstacle is an axis-aligned rectangle centered on its Position:
import { Define, Position } from '@shell/engine-2d';import { Obstacle } from '@shell/spatial';
export const Wall = Define.prefab() .with(Position, { x: 320, y: 160 }) .with(Obstacle, { width: 128, height: 32 });The plugin updates the footprint when its position, width, or height changes. It also removes the footprint when the component or entity is removed, or when its owning scene is unmounted.
Obstacle dimensions are measured in world units. A footprint blocks every grid cell it overlaps, so small objects can still block a full navigation cell. Width or height values less than or equal to zero contribute no footprint.
Add terrain cost
Section titled “Add terrain cost”Add TerrainCost to an Obstacle entity to make the same rectangular region passable at a different
movement cost:
import { Define, Position } from '@shell/engine-2d';import { Obstacle, TerrainCost } from '@shell/spatial';
export const Mud = Define.prefab() .with(Position, { x: 600, y: 400 }) .with(Obstacle, { width: 256, height: 192 }) .with(TerrainCost, { cost: 3 });A cost of 1 is normal movement. Values above 1 make routes through the region less attractive
and reduce an agent’s speed while it occupies the terrain. Positive values below 1 are allowed and
make the terrain faster and more attractive.
When terrain footprints overlap, the highest contributed cost wins. An entity with both Obstacle
and TerrainCost contributes terrain rather than blocking cells.
Edit cells directly
Section titled “Edit cells directly”Use a navigation profile for map data that is easier to express in cells, such as a tilemap:
const profile = world.get('flowFields').getProfile(0);
profile.markBlocked(4, 7);profile.setCost(5, 7, 2.5);
profile.isBlocked(4, 7); // trueprofile.getCost(5, 7); // 2.5
profile.unblock(4, 7);These edits automatically invalidate fields that use the profile. Do not write to blockedMap or
costMap directly; direct writes bypass revision tracking.
resetMaps() clears manual base-map edits while preserving live entity footprints:
profile.resetMaps();Entity footprints and manual edits are independent. Removing an entity obstacle does not erase a manual block or cost in the same cell.
Multiple profiles
Section titled “Multiple profiles”Obstacles and terrain entities update profile 0. Use additional profiles when a class of agent
needs different passability rules:
const manager = world.get('flowFields');const ground = manager.getProfile(0);const amphibious = manager.getProfile(1);
ground.markBlocked(12, 4);amphibious.setCost(12, 4, 1.5);Set the matching profile on the agent:
const AmphibiousAgent = Define.prefab() .with(Position) .with(Velocity) .with(GridCell) .with(FlowFieldAgent, { profile: 1 });All profiles use the world dimensions and cell size configured on SpatialPlugin.