Skip to content

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.

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 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.

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); // true
profile.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.

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.