Navigation¶
Bakes a navigation mesh from a scene’s static collider geometry and answers spatial queries over it – pathfinding, walkability raycasts, projection, distance to walls and random walkable points – so agents can move around obstacles instead of in straight lines.
A navmesh is defined by a NavMesh component on a node: that node’s subtree
is the volume, and every Collider under it becomes walkable ground. The
component also carries the agent parameters the mesh is built for –
agentRadius, agentHeight, agentMaxClimb and agentMaxSlope –
together with the voxel cellSize and cellHeight.
To use this module:
require engine.navigation_core // or require engine.core
Basics of using the navmeshes (baking, pathfinding, walkability raycasts) are
demonstrated in the NavMesh showcase sample.
Baking a navmesh¶
In the editor, add a NavMesh component to a node, place the walkable
geometry (nodes with Collider components) under it, and press Bake in
the inspector. The result is cached per prefab and reloaded automatically the
next time the prefab is opened or played, so a bake survives a restart without
an explicit save; Clear removes it again.
From script, bake the same volume through its component. The outcome arrives in the block once the bake finishes; treat the bake as asynchronous and do not rely on the block running within the call:
node?.NavMesh.bakeAsync() @(result : BakeResult) {
if (result != BakeResult.Baked) {
// react to a failed or empty bake
}
}
The block receives BakeResult.Baked when a navmesh was produced,
BakeResult.Empty when the bake ran but produced no walkable navmesh (no
static Collider geometry under the node, or none of it was walkable; any
previous navmesh of the volume is cleared), or BakeResult.Failed (with the
reason logged) when the bake could not run at all. node?.NavMesh.isBaked
tells whether the volume currently has a baked navmesh.
Queries take a request struct¶
Every query takes a request struct and calls a block with the result, mirroring
the physics trace / overlap API. Struct fields have sensible defaults,
so you only set what you need; a trailing ext field on the projecting
queries is the half-extents of the box used to snap positions onto the navmesh
(float3(0) derives it from the baked agent profile – widen it when click
positions may land high above the walkable floor).
Finding a path¶
navmesh_find_path searches the baked navmeshes for a walkable route and, if
one exists, calls the block with the waypoints in order (from..to):
var path : array<float3>
let res = navmesh_find_path(NavPathQuery(from = start, to = target)) $(p) {
path := p
}
The result tells the three outcomes apart:
PathResult.Full– a complete route to the goal.PathResult.Partial– the goal is unreachable (or the route hit the search limits); the path stops at the closest reachable point.PathResult.NoPath– nothing walkable was found; the block is not called.
Following a route (NavCorridor)¶
navmesh_find_path gives waypoints to inspect or draw; to actually walk a
route, use the stateful NavCorridor. It keeps the polygon corridor between
the current position and the target and re-funnels from wherever the agent
actually is every frame, so the steering never goes stale and the agent does
not wedge on navmesh borders:
var corridor = NavCorridor()
// repath on demand
if (corridor.findPath(npc.localPosition, dest) != PathResult.NoPath) {
walking = true
}
// every frame
if (walking) {
let corners <- corridor.corners(2)
let steerTo = length(corners) > 0 ? corners[0] : corridor.target
let dir = steerTo - npc.localPosition
if (corridor.movePosition(npc.localPosition + normalize(dir) * speed * get_delta_time())) {
npc.localPosition = corridor.position
} else {
corridor.findPath(npc.localPosition, dest) // the navmesh was rebaked -- replan
}
if (corridor.distanceToGoal(1.0) <= 0.4) {
walking = false // arrived
}
}
moveTarget patches the corridor tail when the goal shifts nearby (chasing a
moving target), optimize straightens the route when called a few times per
second, isValid/reset manage the corridor lifetime. position and
target are read-only properties: they are outputs of
findPath/movePosition/moveTarget, so the corridor state can only
move through those calls. Any navmesh rebake invalidates every outstanding
corridor: movePosition returns false and the route must be rebuilt. See the
NavMesh showcase sample for the full loop.
Walkability raycast (trace)¶
navmesh_trace casts a ray along the navmesh surface from req.from
toward req.to. It answers “could an agent walk straight there”, not “what
does a 3D ray hit”. The block is called only when a wall blocks the way:
let res = navmesh_trace(NavRayCast(from = pos, to = target)) $(hit) {
// hit.position is the wall, hit.normal its XZ normal
}
if (res == NavMeshTraceResult.Reached) {
// the straight line is walkable
} // NavMeshTraceResult.NotOnNavMesh: `from` is not on any baked navmesh
Other queries¶
navmesh_project_point(NavPointQuery(pos = p))– snap a world position to the nearest point on a baked navmesh (height follows the navmesh surface); use it to validate click or spawn positions. The block receives the projected point.navmesh_distance_to_wall(NavWallQuery(pos = p, maxRadius = r))– the block receives the nearest wall (position, normal, distance); the return value is the distance (maxRadiuswhen nothing is closer).navmesh_random_points_around_circle(NavCircleQuery(center = c, radius = r, count = n))– the block receives an array of random walkable positions reachable from the center.navmesh_random_point(seed)– the block receives a random walkable position anywhere on the navmesh.
The random queries take a seed: 0 is non-deterministic (each call
differs), any nonzero seed reproduces the same points for a given baked navmesh.
NavPathQuery also carries maxPathPoints (0 = unbounded): a cap on the
waypoint count so a long query stays inside a frame budget. Hitting the cap
returns PathResult.Partial with the route so far.
Agent profiles and groups¶
The bake groups tiles by agent profile – the full set of NavMesh
parameters (agentRadius, agentHeight, agentMaxClimb,
agentMaxSlope, cellSize, cellHeight). Every distinct profile gets
its own baked navmesh. Two consequences follow:
Volumes baked with the same profile share one navmesh and are stitched together, so a path can run from one volume into an adjacent one.
Volumes baked with different profiles are separate navmeshes; a query for one profile never sees the other. Changing any parameter on a
NavMeshcomponent defines a new profile – if you tweakcellSizeafter baking, the old baked navmesh no longer matches and you must rebake.
A query selects its group by profile (see below). If no baked group matches the
requested profile, that is a usage error: the query logs an error listing the
requested profile and every baked profile (so the mismatch is obvious) and
returns its empty result (NoPath / false / …). “No path on an existing
navmesh” is a normal result and is not logged.
Selecting an agent profile¶
The module-level free functions query across every baked group. To route on a
specific profile, call the same queries as methods of the NavMesh component
whose parameters you want; they then run only on the navmesh baked for that
profile, so a large agent follows the large-agent navmesh instead of the small
one:
var path : array<float3>
let res = node?.NavMesh.findPath(NavPathQuery(from = a, to = b)) $(p) {
path := p
}
findPath, trace, projectPoint, distanceToWall,
randomPointsAroundCircle and randomPoint mirror the module-level
functions above and report an error when no navmesh is baked for that profile.
Enumerations¶
- BakeResult¶
Outcome of a navmesh bake. Delivered to the block passed to the NavMesh component method bakeAsync when the bake finishes.
- Values:
Failed = 0 - The bake could not run at all (no physics world, bad parameters, or a build error); the reason is logged
Empty = 1 - The bake ran but produced no walkable navmesh (no static Collider geometry under the node, or none of it was walkable); any previous navmesh of the volume is cleared
Baked = 2 - A walkable navmesh was produced and added
- NavMeshTraceResult¶
Outcome of a navmesh walkability raycast. Returned by navmesh_trace and the NavMesh component method trace.
- Values:
NotOnNavMesh = 0 - The start position could not be projected onto any matching baked navmesh
Reached = 1 - The ray reached the end position without hitting a wall
Blocked = 2 - The ray hit a wall; the hit position and the wall normal are reported
- PathResult¶
Outcome of a navmesh path search: tells a complete route apart from a truncated one. Returned by navmesh_find_path and the NavMesh component method findPath.
- Values:
NoPath = 0 - No walkable path was found (no navmesh baked, endpoints off-mesh, or the search failed); the output array is emptied
Full = 1 - A complete route from start to end was found
Partial = 2 - The goal is unreachable (or the route hit the search limits); the returned path stops at the closest reachable point
Structures¶
- NavCorridor¶
A stateful navmesh path corridor (Detour path-corridor semantics): holds the current position, the on-mesh target and, internally, the polygon corridor between them. This is THE primitive for moving an agent along a route: repath with findPath, then every frame pick the steering point and commit the step with movePosition - the corridor re-funnels from the current position each frame, so steering never goes stale. corners() can come back empty (stale corridor, never built, or every corner already consumed), so guard the access:
let corners <- corridor.corners(2)
let steerTo = length(corners) > 0 ? corners[0] : corridor.target
A navmesh rebake invalidates the corridor: movePosition returns false and isValid turns false - call findPath again.
position and target are read-only; they are updated only through findPath/movePosition/moveTarget.
- NavCorridor.findPath(from: float3; to: float3): PathResult¶
def NavCorridor.findPath
- Arguments:
from : float3
to : float3
- NavCorridor.movePosition(want: float3): bool¶
def NavCorridor.movePosition
- Arguments:
want : float3
- NavCorridor.corners(max_corners: int = 4): array<float3>¶
def NavCorridor.corners
- Arguments:
max_corners : int
- NavCorridor.moveTarget(want: float3): bool¶
def NavCorridor.moveTarget
- Arguments:
want : float3
- NavCorridor.optimize(next_corner: float3; max_range: float = 6f): bool¶
def NavCorridor.optimize
- Arguments:
next_corner : float3
max_range : float
- NavCorridor.distanceToGoal(max_range: float = 100f): float¶
def NavCorridor.distanceToGoal
- Arguments:
max_range : float
- NavCorridor.isValid(): bool¶
def NavCorridor.isValid
- NavCorridor.reset()¶
def NavCorridor.reset
- Properties:
- NavCorridor.position: float3¶
The corridor’s current on-mesh position. Read-only: updated by movePosition (and findPath).
- NavCorridor.target: float3¶
The corridor’s on-mesh goal. Read-only: set by findPath, slid by moveTarget.
Functions¶
navmesh_distance_to_wall (req: NavWallQuery; blk: block<(hit:NavWallHit):void>) : float
navmesh_find_path (req: NavPathQuery; blk: block<(path:array<float3>):void>) : PathResult
navmesh_project_point (req: NavPointQuery; blk: block<(projected:float3):void>) : bool
navmesh_random_point (seed: int; blk: block<(point:float3):void>) : bool
navmesh_trace (req: NavRayCast; blk: block<(hit:NavTraceHit):void>) : NavMeshTraceResult
- navmesh_distance_to_wall(req: NavWallQuery; blk: block<(hit:NavWallHit):void>): float¶
Measures the distance from a position (projected onto the navmesh) to the nearest navmesh wall within req.maxRadius, and if a wall was found, calls the provided block for it.
- Arguments:
req : NavWallQuery - the distance-to-wall request
blk : block<(hit: NavWallHit):void> - the block to call with the nearest wall (not called when no wall is within the radius)
- Returns:
float - the distance to the nearest wall, req.maxRadius when no wall is that close, or a
negative value when req.pos is not on any baked navmesh
- navmesh_find_path(req: NavPathQuery; blk: block<(path:array<float3>):void>): PathResult¶
Searches the baked navmeshes for a walkable route, and if one was found, calls the provided block with the waypoints in order (from..to).
- Arguments:
req : NavPathQuery - the pathfinding request
blk : block<(path:array<float3>):void> - the block to call with the waypoint array (not called on PathResult.NoPath)
- Returns:
PathResult - PathResult.Full for a complete route, PathResult.Partial when the goal is
unreachable (or req.maxPathPoints was hit) and the path stops at the closest reachable point, PathResult.NoPath when nothing walkable was found
Usage example:
let res = navmesh_find_path(NavPathQuery(from = start, to = target)) $(p : array<float3>) {
path := p // follow the waypoints
}
let complete = res == PathResult.Full
- navmesh_project_point(req: NavPointQuery; blk: block<(projected:float3):void>): bool¶
Projects a world position onto the nearest point of a baked navmesh (height follows the navmesh surface), and if the position is close enough, calls the provided block with the result. Useful to validate click/spawn positions before pathfinding.
- Arguments:
req : NavPointQuery - the projection request
blk : block<(projected:float3):void> - the block to call with the projected point (not called when nothing is close enough)
- Returns:
bool - true when req.pos is close enough to a baked navmesh
Usage example:
navmesh_project_point(NavPointQuery(pos = clickPos)) <| $(projected) {
spawnPos = projected
}
- navmesh_random_point(seed: int; blk: block<(point:float3):void>): bool¶
Picks a random walkable position anywhere on the baked navmesh (polygons weighted by area), and if one was produced, calls the provided block with it.
- Arguments:
seed : int - random seed; 0 is non-deterministic (each call differs), any nonzero seed reproduces the same point
blk : block<(point:float3):void> - the block to call with the random position (not called when nothing is baked)
- Returns:
bool - true when a point was produced
- navmesh_random_points_around_circle(req: NavCircleQuery; blk: block<(points:array<float3>):void>): int¶
Generates up to req.count random walkable positions reachable from req.center within req.radius (the circle limits the visited polygons, so points are not exactly distance-constrained), and if any were produced, calls the provided block with the array.
- Arguments:
req : NavCircleQuery - the random-points request
blk : block<(points:array<float3>):void> - the block to call with the generated points (not called when none were produced)
- Returns:
int - the number of points produced (0 when req.center is not on any baked navmesh)
Usage example:
navmesh_random_points_around_circle(NavCircleQuery(center = pos, radius = 8.0, count = 5)) <| $(points : array<float3>) {
for (p in points) { spawn_enemy(p); }
}
- navmesh_trace(req: NavRayCast; blk: block<(hit:NavTraceHit):void>): NavMeshTraceResult¶
Casts a “walkability” ray along the navmesh surface from req.from toward req.to - tells whether an agent could walk in a straight line, not what a 3D ray would hit - and if a wall blocks the way, calls the provided block for the resulting hit.
- Arguments:
req : NavRayCast - the raycast request
blk : block<(hit: NavTraceHit):void> - the block to call with the wall hit (called only on NavMeshTraceResult.Blocked)
- Returns:
NavMeshTraceResult - NavMeshTraceResult.Reached when the straight line is walkable, NavMeshTraceResult.Blocked
when a wall is in the way, NavMeshTraceResult.NotOnNavMesh when req.from is not on any baked navmesh
Usage example:
navmesh_trace(NavRayCast(from = pos, to = target)) <| $(hit) {
print("wall at {hit.position}, normal {hit.normal}")
}