Phaser AE

Recast navmesh and crowds

Free packv3.0.0-53DLogicUpdated

A true navigation mesh baked from real geometry, for the 3D routes a height grid cannot express: a bridge over a gap, a ramp to a walkway, a path under an arch. Its crowds move groups of agents round each other and the walls.

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

Released October 14, 2026 · updated October 26, 2026

Recast navmesh and crowds: 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 navMesh(): Pack<{ navMesh: NavMeshApi }>

The nav-mesh pack: recast/Detour navmeshes and crowds. It needs no world: the meshes are pure computation over the geometry it is given, in x, y (up) and z. The core is a lazy WASM chunk; nothing is fetched until the first bake(). Headless it bakes fine — recast is pure computation.

Functions

bakeConfig

function bakeConfig(o: NavMeshOptions): { cs: number; ch: number; walkableSlopeAngle: number; walkableHeight: number; walkableClimb: number; walkableRadius: number }

Turn the world-unit options into recast's config, whose walkable* dials are in VOXELS.

buildNavMesh

async function buildNavMesh(opts: NavMeshOptions): Promise<NavMesh3d>

Bake a navmesh from geometry, loading the vendored core on first use.

terrainMesh

function terrainMesh( heightAt: (x: number, z: number) => number, size: number, res = 64, cx = 0, cz = 0, ): { positions: Float32Array; indices: Uint32Array }

Triangulate a heightfield region into a navmesh source mesh, wound CCW seen from above.

The winding is the whole trap: recast decides what is walkable from the face NORMAL, so a mesh wound the other way bakes to nothing at all and the error reads as "no walkable polygons" rather than "your triangles are inside out".

Classes

Crowd3d

class Crowd3d

recast's Crowd: agents that avoid each other locally. For dense groups that must not interpenetrate.

An agent is named by the number add returns. The crowd moves it; you read it back with position and velocity. A number belongs to its agent until remove, after which recast gives it to the next agent added.

  • readonly maxAgents: number — The most agents this crowd holds at once.
  • constructor(core: NavCore, navMesh: unknown, opts: CrowdOptions, snap: (p: NavVec3) => NavVec3 | null)
  • add(pos: NavVec3, params: CrowdAgentParams = {}): number — Add an agent at a world position and return its number. The position is moved onto the mesh first, as nearest does: recast leaves an agent placed off the mesh (well above it, say) standing motionless and says nothing. params overrides the defaults: radius 0.6, height 2, maxSpeed 6, maxAcceleration 20. Throws when the point is not near the mesh, or when the crowd already holds maxAgents.
  • goto(idx: number, goal: NavVec3): boolean — Send an agent toward a world goal, moved onto the mesh first (recast alone refuses a goal more than a unit off the surface). Returns false, and leaves the agent as it was, when the goal is not near the mesh. The agent walks there over the following steps; it does not arrive at once.
  • stop(idx: number): void — Drop an agent's goal. It brakes to a halt and stays where it stops, still avoiding the others.
  • remove(idx: number): void — Take an agent out of the crowd, freeing its place for add.
  • position(idx: number): NavVec3 — Where an agent is now, in world units.
  • velocity(idx: number): NavVec3 — How fast and which way an agent is moving, world units a second.
  • get count(): number — How many agents it holds.
  • update(dt: number): void — Step the crowd by dt seconds. The owning navmesh does this on every simulation step.
  • free(): void
class NavMesh3d

A baked navmesh and its query. It satisfies the same path(start, goal) contract nav's NavGrid does, so it drops straight into navigate — which is the whole point of having two tiers rather than two APIs.

  • readonly raw: unknown — The recast mesh, for anything this wrapper does not cover.
  • dead: boolean — True after kill(). A dead navmesh has freed its memory and its crowds': it stops ticking, and using it throws.
  • constructor(core: NavCore, navMesh: unknown)
  • randomPoint(): NavVec3 | null — A uniformly random point ON the mesh — a wander or patrol goal that is guaranteed reachable. Null if the mesh is empty.
  • nearest(p: NavVec3): NavVec3 | null — Snap a point to the nearest walkable surface, or null when it is off the mesh entirely.
  • path(start: NavVec3, goal: NavVec3, opts: { partial?: boolean; radius?: number } = {}): NavVec3[] — A route start→goal as world waypoints; empty when there is none. The PathPlanner contract. A goal the mesh cannot reach from start (across a gap, behind a wall with no door) has no route; with partial: true the route goes as near to it as the mesh allows instead, which is what Detour itself answers. radius is accepted and ignored, as the contract allows: the mesh keeps the clearance it was baked with.
  • crowd(opts: CrowdOptions = {}): Crowd3d — A recast Crowd over this mesh, for dense local avoidance; the mesh steps it every simulation step. A game can make as many as it likes, each with its own maxAgents. Most games use steering instead.
  • tick(dt: number): void
  • kill(): void — Free the mesh, its query and its crowds' WebAssembly memory, stop ticking, and let the pack drop it. Using it, or one of its crowds, afterwards throws.

Types

CrowdAgentParams

type CrowdAgentParams = Partial<Record<'radius' | 'height' | 'maxSpeed' | 'maxAcceleration' | 'collisionQueryRange' | 'pathOptimizationRange' | 'separationWeight' | 'updateFlags' | 'obstacleAvoidanceType' | 'queryFilterType', number>>

The recast numbers an agent can be given. Anything unset takes recast's default, apart from the four this wrapper sets (below).

CrowdOptions

interface CrowdOptions

What a crowd takes to make: maxAgents is a hard cap (recast sizes the crowd up front); maxAgentRadius the widest agent it will hold.

  • maxAgents?: number — The most agents the crowd holds at once. Default 256. add throws past it; remove frees a place.
  • maxAgentRadius?: number — The widest agent radius the crowd is built for, world units. Default 0.6. Recast sizes its neighbour grid from it, so set it to the largest radius you will add.
type NavCore = Record<string, unknown> & { NavMeshQuery: new (mesh: unknown) => NavQuery; Crowd: new (mesh: unknown, o: object) => RawCrowd; generateSoloNavMesh(p: ArrayLike<number>, i: ArrayLike<number>, cfg: object): { success: boolean; error?: string; navMesh: unknown } }

The core module's surface is the vendored build's own; this wrapper is the contract, not the types.

interface NavMeshApi

game.navMesh.

  • bake(opts: NavMeshOptions): Promise<NavMesh3d> — Bake a navmesh from geometry, or from a heightfield. Resolves once the core has loaded and recast has run — it is a job, not a getter, so bake at load time rather than mid-level.
  • readonly meshes: ReadonlyArray<NavMesh3d> — Every live navmesh; their crowds are ticked each step.
  • readonly last: NavMesh3d | null — The last one baked, for a game that only needs one.
  • terrainMesh(heightAt: (x: number, z: number) => number, size: number, res?: number, cx?: number, cz?: number): { positions: Float32Array; indices: Uint32Array } — Triangulate a heightfield region into a CCW-up source mesh, without baking.
interface NavMeshOptions

What game.navMesh.bake() takes: explicit geometry, or a heightfield to triangulate, plus the recast bake dials.

  • positions?: ArrayLike<number> — Flat vertex xyz triples. Wind the triangles CCW seen from above, or the walkable faces point down and recast finds nothing.
  • indices?: ArrayLike<number> — Triangle indices into positions, three a triangle.
  • from?: 'terrain' — …or triangulate a heightfield into a grid mesh instead.
  • heightAt?: (x: number, z: number) => number — The height sampler for from: 'terrain'.
  • size?: number — The side of the region to triangulate, world units. Required with from: 'terrain'.
  • center?: { x: number; z: number } — The region's world centre. Default the origin.
  • res?: number — Samples along each side. Default 64.
  • cellSize?: number — Voxel cell size — the navmesh's resolution. Default 0.3.
  • cellHeight?: number — Voxel cell height. Default 0.2.
  • agentRadius?: number — The mesh is ERODED by this, so a route keeps clearance from walls. Default 0.6.
  • agentHeight?: number — Minimum head-room. Default 2.
  • agentMaxClimb?: number — The tallest step or ledge the agent can climb. Default 0.9.
  • agentMaxSlope?: number — The steepest slope that counts as walkable, degrees. Default 50.
  • module?: string — Override the core module URL in a development build, on the game's own origin; another origin throws. Rarely needed.
interface NavVec3

A point in the world. The same shape nav uses, so a planner drops into either.

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