Phaser AE

Navigation Grid and Flow Field

Free packv3.0.0-52D and 3DLogic

Pathfinding in two tiers: a grid that routes one agent somewhere specific, and a flow field that answers the same question once for a thousand agents heading to the same place. Both work on a 2D tile map or over 3D terrain, and both plan for bodies with a size.

This is a Free pack. Every phaser.io account can use it in their games.

Released October 14, 2026 · updated October 19, 2026

Navigation Grid and Flow Field: a screenshot of its demo
A note about these graphics

Expansion packs don't come with graphics. This screenshot is one way of drawing what the pack does, made for its demo: none of the sprites, tiles or backgrounds in it are in the pack.

That's by design. A pack is a game's logic, mechanics and effects, and it works with whatever art you give it. Some packs draw effects, backdrops or 3D scenes of their own, but none carries a game's sprites.

So the look is yours. Make the art on phaser.io, bring your own, or let your agent draw it in code: the same pack can look like anything.

The pack

function nav(): Pack<{ nav: NavApi }>

The nav pack: A* over a walkable grid with string pulling, and a flow field for when many things head to one place, in 3D (world X/Z over a height function) or 2D (world X/Y over a tile map), for a point or a body of a given radius. Headless and pure — it holds no objects and draws nothing, so it runs in a worker or a test as happily as in a frame.

Classes

FlowField

class FlowField

A per-cell direction field toward a goal (Tier 2).

  • constructor( /** The NavGrid the field was integrated over. */ readonly grid: NavGrid, /** The integrated cost to the goal for each cell. Infinity where unreachable. */ readonly cost: Float32Array, /** The X part of the unit steering direction for each cell. */ readonly dirx: Float32Array, /** The Z part of the unit steering direction for each cell. */ readonly dirz: Float32Array, )
  • dirAt(x: number, z: number): { x: number; z: number } — Steering direction at a world XZ (zero where unreachable/at goal).
  • reachable(x: number, z: number): boolean — Whether a world XZ can reach the goal at all (finite cost).

FlowField2d

class FlowField2d

A per-cell direction field toward a goal over a NavGrid2d (Tier 2, in 2D).

  • constructor( /** The NavGrid2d the field was integrated over. */ readonly grid: NavGrid2d, /** The integrated cost to the goal for each cell. Infinity where unreachable. */ readonly cost: Float32Array, /** The X part of the unit steering direction for each cell. */ readonly dirx: Float32Array, /** The Y part of the unit steering direction for each cell. */ readonly diry: Float32Array, )
  • dirAt(x: number, y: number): NavVec2 — Steering direction at a world XY (zero where unreachable/at goal).
  • reachable(x: number, y: number): boolean — Whether a world XY can reach the goal at all (finite cost).

FlowGrid

class FlowGrid

A mutable grid carrying a flow field toward one or more goals. Build it, set goals + obstacles, then read steer() / flowVec() / distance() every frame. Edits (block, weight, goals) mark the field dirty; it rebuilds itself on the next query — call update up front if you want the cost paid at a known time.

const grid = new FlowGrid({ cols: 40, rows: 30, goals: [{ col: 39, row: 15 }], spawns: [{ col: 0, row: 15 }] });
if (grid.canBuild(c, r)) grid.build(c, r);          // place a tower
enemy.vel = scale(grid.steer(enemy.x, enemy.y, TILE), speed);  // follow the flow
  • readonly cols: number — Grid width in cells.
  • readonly rows: number — Grid height in cells.
  • diagonal: boolean — Whether diagonal movement is allowed.
  • cutCorners: boolean — Whether diagonal moves may cut blocked corners.
  • goals: Cell[] — The current goal cells (flow points toward the nearest).
  • spawns: Cell[] — The current spawn cells (used by canBuild).
  • readonly blocked: Uint8Array — 1 where a cell is blocked (tower/wall), else 0. Row-major.
  • readonly weight: Float32Array — Per-cell entry cost (terrain weight); 1 is normal, higher is slower.
  • readonly dist: Float32Array — Integration field: distance to the nearest goal per cell, Infinity when unreachable. Row-major. Rebuilt by update. @internal-ish — prefer distance.
  • readonly flow: Int8Array — Flow field: the FlowDir each cell steps toward its goal.
  • constructor(opts: FlowGridOptions)
  • index(col: number, row: number): number — Row-major cell index for (col, row). No bounds check.
  • inBounds(col: number, row: number): boolean — Is (col, row) inside the grid?
  • cellAt(x: number, y: number, cellSize = 1, originX = 0, originY = 0): Cell | null — Which cell contains world/pixel position (x, y), at cellSize units per cell from (originX, originY). null when outside the grid.
  • isBlocked(col: number, row: number): boolean — Is the cell an obstacle (tower/wall)? Out-of-bounds reads as blocked.
  • setBlocked(col: number, row: number, on: boolean): void — Block or unblock a cell (place/remove a tower or wall).
  • build(col: number, row: number): void — Place a tower/wall at (col, row) (alias for setBlocked(…, true)).
  • unbuild(col: number, row: number): void — Remove a tower/wall at (col, row) (alias for setBlocked(…, false)).
  • clearBlocked(): void — Clear every obstacle.
  • setWeight(col: number, row: number, w: number): void — Set a cell's terrain entry cost (1 normal; >1 slow mud; <1 fast road). Cells stay passable; block with setBlocked instead.
  • setGoals(goals: Cell[]): void — Replace the goal cells and mark the field dirty.
  • setSpawns(spawns: Cell[]): void — Replace the spawn cells (used only by canBuild).
  • update(): void — Rebuild the integration + flow fields (Dijkstra from all goals). Called automatically by the queries when the grid is dirty; call it yourself to pay the cost at a controlled time. Orthogonal steps cost weight, diagonal steps weight · √2.
  • distance(col: number, row: number): number — Distance (field cost) from (col, row) to the nearest goal, or Infinity if blocked/unreachable.
  • reachable(col: number, row: number): boolean — Can an enemy at (col, row) reach a goal?
  • flowDir(col: number, row: number): FlowDir — The FlowDir (col, row) flows toward its goal (-1 at a goal / blocked / unreachable cell).
  • flowVec(col: number, row: number): NavVec2 — The unit flow VECTOR at cell (col, row) — the direction to move, as a {x, y} (zero when there's no flow).
  • steer(x: number, y: number, cellSize = 1, originX = 0, originY = 0): NavVec2 — The smooth steering direction for an enemy at world position (x, y) — bilinearly interpolates the flow arrows of the four surrounding cells so units off the cell centre don't jerk between headings. Returns a unit-length {x, y} (zero at the goal / in a dead pocket). cellSize + origin map world units to the grid (same convention as cellAt).
  • canBuild(col: number, row: number): boolean — Would blocking (col, row) still leave EVERY spawn able to reach a goal? The tower-placement guard: returns false for a build that would fully wall a spawn off (or that isn't a legal, currently-open cell). Runs a connectivity check with the same movement rules — it does not mutate the grid.
  • path(from: Cell, maxSteps = this.cols * this.rows): Cell[] — Trace the route from (col, row) by following the flow field to a goal — an inclusive list of cells, for drawing a preview path or a debug overlay. Empty when unreachable; capped so a broken field can't loop forever.
class NavGrid extends NavGridCore

A walkability grid over a heightfield, for A* paths and flow fields.

  • readonly res: number — The number of cells per side of the square grid.
  • readonly ox: number — The world X of the center of cell (0,0).
  • readonly oz: number — The world Z of the center of cell (0,0).
  • readonly height: Float32Array — The sampled surface height of each cell, indexed by idx().
  • constructor(o: NavGridOptions)
  • snapshot(): NavGridSnapshot — A structured-cloneable copy of the grid, for a worker.
  • static fromSnapshot(s: NavGridSnapshot): NavGrid — Rebuilds a grid from snapshot().
  • idx(ix: number, iz: number): number — Returns the flat array index of the cell (ix, iz).
  • inBounds(ix: number, iz: number): boolean — Returns true when the cell (ix, iz) is inside the grid.
  • cellCenter(ix: number, iz: number): NavVec3 — Returns the world center of the cell, with y set to the cell's surface height.
  • worldToCell(x: number, z: number): { ix: number; iz: number } — Returns the cell nearest to a world XZ, clamped to the grid.
  • walkableCell(ix: number, iz: number): boolean — Returns true when the cell is inside the grid and walkable.
  • waypoint(i: number, radius?: number): NavVec3 | null — The world point a route for a body of radius (default the grid's) passes through in cell i: its center when the body fits there, else the cell's roomiest edge midpoint or corner the body fits on, at the cell's surface height; null when it fits nowhere in the cell.
  • clear(from: NavVec3, to: NavVec3, options?: NavQueryOptions): boolean — Can a body of options.radius (default the grid's) slide straight from one world point to another over X/Z, without touching an unwalkable cell, crossing an impassable step or leaving the grid? Exact. A follower asks it to turn for the next waypoint as soon as it can reach it directly.
  • nearestWalkable(ix: number, iz: number, radius?: number): { ix: number; iz: number } | null — The nearest cell to (ix,iz) a body of radius (default the grid's) fits in (start/goal snap). A point takes the nearest walkable cell by ring search; a body, the nearest cell it fits in that it can walk to from there.
  • astar(startX: number, startZ: number, goalX: number, goalZ: number, radius?: number): number[] | null — A* over 8-connected cells between two world XZ points, for a body of radius (default the grid's). Returns a list of cell indices start→goal, or null if unreachable (a cliff-ringed peak refuses a path).
  • path(start: NavVec3, goal: NavVec3, options?: NavQueryOptions): NavVec3[] — Full plan start→goal as WORLD waypoints at surface height, for a body of options.radius (default the grid's): cell centers, or for a body too wide for a cell's center, the point of that cell it fits on. Empty array when no path exists.
  • flowField(goal: NavVec3, options?: NavQueryOptions): FlowField — Build a flow field: cost-to-goal by Dijkstra, then a per-cell direction toward the cheapest neighbor, for bodies of options.radius (default the grid's). Every agent samples it O(1).
class NavGrid2d extends NavGridCore

A walkability grid over a tile map in world X/Y (2D, y down), for A* paths and flow fields.

  • readonly ox: number — The world X of the center of cell (0,0).
  • readonly oy: number — The world Y of the center of cell (0,0).
  • constructor(o: NavGrid2dOptions)
  • snapshot(): NavGrid2dSnapshot — A structured-cloneable copy of the grid, for a worker.
  • static fromSnapshot(s: NavGrid2dSnapshot): NavGrid2d — Rebuilds a grid from snapshot().
  • idx(col: number, row: number): number — Returns the flat array index of the cell (col, row).
  • inBounds(col: number, row: number): boolean — Returns true when the cell (col, row) is inside the grid.
  • cellCenter(col: number, row: number): NavVec2 — Returns the world center of the cell.
  • worldToCell(x: number, y: number): { col: number; row: number } — Returns the cell under a world XY, clamped to the grid.
  • walkableCell(col: number, row: number): boolean — Returns true when the cell is inside the grid and walkable.
  • waypoint(i: number, radius?: number): NavVec2 | null — The world point a route for a body of radius (default the grid's) passes through in cell i: its center when the body fits there, else the cell's roomiest edge midpoint or corner the body fits on; null when it fits nowhere in the cell.
  • clear(from: NavVec2, to: NavVec2, options?: NavQueryOptions): boolean — Can a body of options.radius (default the grid's) slide straight from one world point to another without touching an unwalkable cell or leaving the grid? Exact. A follower asks it to turn for the next waypoint as soon as it can reach it directly.
  • nearestWalkable(col: number, row: number, radius?: number): { col: number; row: number } | null — The nearest cell to (col, row) a body of radius (default the grid's) fits in. A point takes the nearest walkable cell by ring search; a body, the nearest cell it fits in that it can walk to from there.
  • astar(startX: number, startY: number, goalX: number, goalY: number, radius?: number): number[] | null — A* over 8-connected cells between two world XY points, for a body of radius (default the grid's). Returns a list of cell indices start→goal, or null if unreachable.
  • path(start: NavVec2, goal: NavVec2, options?: NavQueryOptions): NavVec2[] — Full plan start→goal as world XY waypoints, string-pulled for a body of options.radius (default the grid's): cell centers, or for a body too wide for a cell's center, the point of that cell it fits on. Empty array when no path exists.
  • flowField(goal: NavVec2, options?: NavQueryOptions): FlowField2d — Build a flow field toward one goal for bodies of options.radius (default the grid's). Every agent samples it O(1).
abstract class NavGridCore

The cell graph NavGrid and NavGrid2d share: walkability, the clearance map, A*, line of sight, string pulling and flow-field integration, over cols × rows cells indexed row * cols + col. Build a NavGrid or a NavGrid2d; this is their common part, not a grid on its own.

A route runs through one point per cell: the cell's center, or, for a body too wide to stand there, the roomiest of the cell's edge midpoints and corners it fits on. So a body fits any gap between walls that is at least twice its radius wide, as walls are whole cells and the middle of a gap between them is always a center, an edge or a corner.

  • readonly cols: number — The number of cells across.
  • readonly rows: number — The number of cells down (along Z on a NavGrid, Y on a NavGrid2d).
  • readonly cell: number — The size of one square cell, in world units.
  • readonly walk: Uint8Array — Walkability of each cell, row by row: 1 walkable, 0 blocked. Change it with setWalkable(), or call invalidate() after editing it in place.
  • readonly radius: number — The default walker radius, in world units, for queries that do not give their own. 0 is a point.
  • get clearance(): Float32Array — The distance from each cell's center to the nearest unwalkable cell or the grid's edge, in world units, row by row (0 on an unwalkable cell). A body of radius r can stand at a cell's center when its clearance is at least r. Worked out on first use, and again after a change to walk.
  • invalidate(): void — Forget the cached clearance, after walk has been edited in place. setWalkable() does this itself.
  • setWalkable(i: number, walkable: boolean): void — Make cell i walkable or not, and forget the cached clearance. Throws for an index off the grid.
  • fits(i: number, radius?: number): boolean — Returns true when a body of radius (default the grid's) can stand somewhere in cell i: at its center, an edge midpoint or a corner, that far from every unwalkable cell and the grid's edge.
  • lineOfSight(a: number, b: number, radius?: number): boolean — Walkable line of sight between two cells, by index, for a body of radius (default the grid's): true only when a body that size can slide straight from where it stands in one to where it stands in the other without touching an unwalkable cell or leaving the grid. Exact, not sampled.
  • stringPull(cells: number[], radius?: number): number[] — String-pull: drop interior cells the previous kept cell can see directly — for a body of radius (default the grid's), so a route is straightened only where the whole body fits.

Types

Cell

interface Cell

A grid cell by column and row.

  • col: number — Column, 0-based.
  • row: number — Row, 0-based.

FlowDir

type FlowDir = -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7

A flow direction as an 8-way index (0=E, 1=SE, 2=S, 3=SW, 4=W, 5=NW, 6=N, 7=NE), or -1 for "no flow" (a goal cell, a blocked cell, or unreachable).

FlowGridOptions

interface FlowGridOptions

Configuration for a new FlowGrid. cols/rows are required.

  • cols: number — Grid width in cells.
  • rows: number — Grid height in cells.
  • goals?: Cell[] — Goal cell(s) — the field flows toward the NEAREST of these (multi-goal is the "supernode" trick, seeded for free). At least one is needed for a useful field.
  • spawns?: Cell[] — Spawn cell(s). Only used by FlowGrid.canBuild to guarantee a tower never fully walls a spawn off from the goal.
  • diagonal?: boolean — Allow 8-way (diagonal) movement. Default true.
  • cutCorners?: boolean — Let diagonal moves slip between two blocked cells at a corner. Default false (units won't squeeze between two towers placed corner-to-corner).
interface NavApi

game.nav.

  • grid(opts: NavGridOptions): NavGrid — Build a walkable grid over a height function in world X/Z — the A* tier for a 3D game.
  • grid2d(opts: NavGrid2dOptions): NavGrid2d — Build a walkable grid over a tile map in world X/Y (y down) — the A* tier for a 2D game.
  • flow(grid: NavGrid, goal: NavVec3, options?: NavQueryOptions): FlowField — A flow field toward one goal over a grid — the many-agents-one-target tier: O(1) per lookup. A flow field toward one goal over a 2D grid — the many-agents-one-target tier: O(1) per lookup.
  • flow(grid: NavGrid2d, goal: NavVec2, options?: NavQueryOptions): FlowField2d — A flow field toward one goal over a grid — the many-agents-one-target tier: O(1) per lookup. A flow field toward one goal over a 2D grid — the many-agents-one-target tier: O(1) per lookup.
  • pathAsync(grid: NavGrid, start: NavVec3, goal: NavVec3, options?: NavAsyncOptions): Promise<NavVec3[]> — Run A* off-thread on a copied snapshot. Requires a started game and bakery worker output. Run 2D A* off-thread on a copied snapshot. Requires a started game and bakery worker output.
  • pathAsync(grid: NavGrid2d, start: NavVec2, goal: NavVec2, options?: NavAsyncOptions): Promise<NavVec2[]> — Run A* off-thread on a copied snapshot. Requires a started game and bakery worker output. Run 2D A* off-thread on a copied snapshot. Requires a started game and bakery worker output.
  • flowGrid(opts: FlowGridOptions): FlowGrid — A weighted flow grid for tower defence and crowds on a flat grid: per-cell weights, several goals, canBuild ("would this wall cut the last route?") and smooth steer. Its walkers are points.
  • readonly last: NavGrid | null — The last grid built by grid, for a game that only needs one.
  • readonly last2d: NavGrid2d | null — The last grid built by grid2d, for a game that only needs one.
interface NavAsyncOptions

Options for game.nav.pathAsync.

  • signal?: AbortSignal — Cancels the job.
  • radius?: number — The walker's radius for this path, in world units, in place of the grid's radius.
  • partial?: boolean — When the goal cannot be reached, a route to the reachable cell nearest it rather than none. Default false.
interface NavGrid2dOptions

Options for the NavGrid2d constructor: a tile map over world X/Y, y down.

  • cols: number — Cells across: a whole number, at least 1.
  • rows: number — Cells down: a whole number, at least 1.
  • cell?: number — The size of one square cell, in world units (default 1). With a tile map this is usually the tile size.
  • origin?: { x: number; y: number } — The world position of the grid's top-left corner (default 0,0). Cell (col, row) covers x from origin.x + col * cell and y from origin.y + row * cell, one cell each way.
  • walkable?: ArrayLike<number | boolean> — Walkability per cell, row by row (row * cols + col), exactly cols * rows long: truthy is walkable. Omit for an open floor.
  • blocked?: (x: number, y: number) => boolean — Extra per-cell mask, asked at each cell's center in world X/Y: return true to block. Combines with walkable.
  • radius?: number — The walker's radius, in world units (default 0, a point). Works as NavGridOptions.radius does.
interface NavGrid2dSnapshot

Structured-cloneable NavGrid2d. The buffer is a copy, safe to transfer.

  • kind: '2d' — Marks a 2D grid snapshot.
  • cols: number — Cells across.
  • rows: number — Cells down.
  • cell: number — The size of one cell, in world units.
  • origin: { x: number; y: number } — The world position of the grid's top-left corner.
  • radius: number — The grid's default walker radius.
  • walk: Uint8Array — Walkability per cell, row by row: 1 walkable, 0 blocked.
interface NavGridOptions

Options for the NavGrid constructor.

  • heightAt: (x: number, z: number) => number — Surface height at a world XZ (usually terrain.heightAt).
  • size: number — World span of the (square) grid.
  • center?: { x: number; z: number } — Grid center in world XZ (default origin).
  • res?: number — Cells per side (default 64). Coarser than the render mesh is fine.
  • maxStep?: number — Steeper than this height-step BETWEEN adjacent cells is a cliff — cut.
  • slopePenalty?: number — Cost multiplier for climbing (0 = ignore slope, 3 = strongly prefer flat).
  • minY?: number — Cells whose surface is below this Y are unwalkable (water/void).
  • blocked?: (x: number, z: number) => boolean — Extra per-cell mask, asked at each cell's center in world X/Z: return true to block (obstacles, no-go zones).
  • radius?: number — The walker's radius, in world units (default 0, a point). Every path and flow field on this grid plans for a body this size unless the call gives its own: A* refuses gaps narrower than the body, and string pulling straightens a route only where the whole body fits along the straight line.
interface NavGridSnapshot

Structured-cloneable sampled grid. Buffers are copies, safe to transfer.

  • res: number — Cells per side of the square grid.
  • size: number — World span of the grid along each side, in world units.
  • center: { x: number; z: number } — The grid's centre in world X and Z, in world units.
  • maxStep: number — The tallest height step between adjacent cells that still counts as walkable, in world units.
  • slopePenalty: number — Cost multiplier for climbing (0 ignores slope; larger prefers flat ground).
  • height: Float32Array — Sampled surface height of each cell, in world units, row by row (iz * res + ix): res * res long.
  • walk: Uint8Array — Walkability of each cell, row by row (iz * res + ix): 1 walkable, 0 blocked. res * res long.
  • radius?: number — The grid's default walker radius, in world units; a snapshot without one reads as 0.
type NavPathJob = | { grid: NavGridSnapshot; start: NavVec3; goal: NavVec3; radius?: number; partial?: boolean } | { grid: NavGrid2dSnapshot; start: NavVec2; goal: NavVec2; radius?: number; partial?: boolean }

One off-thread path job: a grid snapshot (3D or 2D), the two ends, an optional walker radius in place of the grid's, and whether an unreachable goal gets a partial route.

interface NavQueryOptions

Per-query options for a path or a flow field.

  • radius?: number — The walker's radius for this query, in world units, in place of the grid's radius. 0 plans for a point.
  • partial?: boolean — A path only: when the goal cannot be reached, a route to the reachable cell nearest it (as the crow flies) rather than none, so a chaser walls in as close as it can get. Default false. nav-mesh takes the same option.
interface NavVec2

A 2D world position (x and y), in world units. y points down the screen.

  • x: number — The X position, in world units.
  • y: number — The Y position, in world units, down the screen.
interface NavVec3

A world position (x, y and z), in world units. y is the surface height.

  • x: number — The X position, in world units.
  • y: number — The surface height, in world units.
  • z: number — The Z position, in world units.

PathPlanner

interface PathPlanner

What a planner must offer, so a behavior can steer over any of them. A NavGrid is one.

  • path(start: NavVec3, goal: NavVec3, options?: NavQueryOptions): NavVec3[] — A route from start to goal, world points, or an empty list when there is none. options.radius asks for a route that a body of that radius fits along; a planner that cannot vary it (a baked navmesh) ignores it.
  • clear?(from: NavVec3, to: NavVec3, options?: NavQueryOptions): boolean — Optional: can a body of options.radius slide straight from one point to another? A follower that has it turns for the next waypoint as soon as it can reach it directly. A NavGrid has it.

PathPlanner2d

interface PathPlanner2d

What a 2D planner must offer: PathPlanner in world X/Y. A NavGrid2d is one.

  • path(start: NavVec2, goal: NavVec2, options?: NavQueryOptions): NavVec2[] — A route from start to goal, world X/Y points, or an empty list when there is none, for a body of options.radius when the planner can vary it.
  • clear?(from: NavVec2, to: NavVec2, options?: NavQueryOptions): boolean — Optional: can a body of options.radius slide straight from one point to another? A NavGrid2d has it.