Flow Fields
A flow field stores a direction for every reachable cell. Many agents can sample the same field, so it is a good fit for crowds moving toward a shared objective.
Agents and targets
Section titled “Agents and targets”SpatialPlugin automatically tracks FlowFieldTarget entities and builds the fields needed by
FlowFieldAgent entities.
| Component | Important fields | Purpose |
|---|---|---|
FlowFieldAgent | active, group, profile, speed | Selects a field and steers the entity’s Velocity |
FlowFieldTarget | groupMask, influence | Publishes a destination to one or more agent groups |
GridCell | x, y | Places an entity in the navigation and spatial grids |
Position | x, y | Supplies the current world position |
Velocity | x, y | Receives steering and is integrated by kinematics |
Position comes from @shell/transform and Velocity comes from @shell/kinematics. Applications
using @shell/engine-2d can import both from that aggregate.
import { Define, Position, Velocity } from '@shell/engine-2d';import { FlowFieldAgent, FlowFieldTarget, GridCell } from '@shell/spatial';
export const Exit = Define.prefab().with(Position, { x: 960, y: 540 }).with(FlowFieldTarget);
export const Enemy = Define.prefab() .with(Position) .with(Velocity) .with(GridCell) .with(FlowFieldAgent, { speed: 80, active: true, });Set active to false to stop an agent from following its field. Its planar velocity decays rather
than stopping instantly. A direction of zero, such as at the target or in an unreachable cell, has
the same deceleration behavior.
Targets with Position are kept synchronized automatically. For a fixed destination that does not
need a world position, assign its cell directly:
const CellTarget = Define.prefab().with(GridCell, { x: 12, y: 8 }).with(FlowFieldTarget);Cell coordinates must be integer coordinates inside the configured grid.
Target groups
Section titled “Target groups”Groups let different agents follow different objectives. Define each group as a unique, single-bit flag:
export const NavigationGroup = { Player: 1, Fortress: 2, Resources: 4,};An agent selects exactly one group. A target mask can publish the same target to several groups:
const PlayerTarget = Define.prefab() .with(Position) .with(FlowFieldTarget, { groupMask: NavigationGroup.Player | NavigationGroup.Fortress, });
const Chaser = Define.prefab() .with(Position) .with(Velocity) .with(GridCell) .with(FlowFieldAgent, { group: NavigationGroup.Player, speed: 100 });The defaults are group 1 for both components. A target with groupMask: 0 is ignored. Agent
groups must contain exactly one bit; do not pass a combined mask to FlowFieldAgent.group or to
flowFields.get().
When a group has multiple targets, agents are directed toward the least expensive destination.
Increasing a target’s positive influence makes it more attractive and gives it a larger region of
the field. Moving, removing, regrouping, or changing the influence of a target automatically rebuilds
the affected fields.
Navigation profiles
Section titled “Navigation profiles”Profiles let agents in the same target group use different movement maps. For example, profile 0
can represent ground movement while profile 1 ignores ground obstacles for flying agents:
const flowFields = world.get('flowFields');
const ground = flowFields.getProfile(0);ground.markBlocked(10, 5);
const GroundAgent = Define.prefab() .with(Position) .with(Velocity) .with(GridCell) .with(FlowFieldAgent, { group: NavigationGroup.Player, profile: 0 });
const FlyingAgent = Define.prefab() .with(Position) .with(Velocity) .with(GridCell) .with(FlowFieldAgent, { group: NavigationGroup.Player, profile: 1 });Profiles are created lazily and share the configured dimensions and cell size. Entity-based
Obstacle and TerrainCost footprints update only profile 0; edit other profiles through their
NavigationProfile API when they need a different map.
Access a field
Section titled “Access a field”Use the flowFields resource to inspect or manually sample a field:
const manager = world.get('flowFields');const field = manager.get(0, NavigationGroup.Player);
const direction: [number, number] = [0, 0];field.sampleDirectionInto(position.x, position.y, direction);sampleDirectionInto() is the allocation-free choice for an update loop. The convenience method
sampleDirection(x, y) returns a new tuple. getDirection(cellX, cellY) reads one cell without
world-space interpolation.
The flowField resource is an alias for flowFields.get(0, 1). Prefer flowFields in code that uses
custom groups or profiles.
Normally the plugin owns building and invalidation. If gameplay changes objectives outside the ECS
target tracker, the manager also exposes markDirty(profile, group), markObjectiveDirty(group),
markProfileDirty(profile), and markAllDirty().
Build budgets
Section titled “Build budgets”Flow fields rebuild incrementally during Update. Tune the aggregate options when field changes
cause visible latency or update spikes:
world.addPlugin( SpatialPlugin.config({ cellSize: 32, width: 4096, height: 4096, flowFieldMaxHeapPopsPerUpdate: 8192, flowFieldMaxFieldsStartedPerUpdate: 8, }),);Larger budgets publish changed routes sooner but allow more work in one update. Construction is shared fairly across fields currently used by agents.