Ground Navigation
Terrain-backed navigation and collision-aware agents for lightweight 3D games.
Each surface owns an independent native A* grid through demi.navigation.
Movement uses CharacterController3D, including its collision, slope and gravity
rules. Game scripts choose jobs and priorities.
local Ground = require("demi.navigation.ground")
local Walker = {}
function Walker:on_start()
self.surface = Ground.create({terrain="terrain", origin={0,0}, columns=64,
rows=64, cell_size=2, agent_radius=0.4, ignored_entity=self.entity_id})
self.agent = self.surface:agent(self.entity_id, {speed=3})
end
function Walker:on_fixed_update(dt)
if self.surface:update(64) and not self.requested then
self.requested = true
self.accepted, self.diagnostic = self.agent:move_to({30,0,40}, 3)
end
self.status = self.agent:update(dt)
end
function Walker:on_destroy()
if self.agent then self.agent:stop() end
end
return Walker
This single-actor example owns its survey. For several actors in the same area, share one surface and create an agent for each actor. Issue move_to when a goal changes, not every frame. It returns success and a diagnostic; unavailable actors and unreachable routes fail explicitly. Update returns idle, moving, arrived, paused, surveying, unreachable or blocked. No failure teleports the actor.
The actor needs Transform3D, a supported collider and CharacterController3D.
The grid maps its axes to world X/Z; returned path points are {x,y,z} arrays.
One terrain surface per grid is supported; this is heightfield ground navigation,
not a multilevel navmesh, aerial navigation or crowd-avoidance system.
surface:set_obstacles(boxes) updates clearance around dynamic footprints;
each box contains min_x, max_x, min_z and max_z. Agents replan after revision
changes. Call surface:invalidate() after terrain or static collision changes;
agents stop until the incremental survey completes. agent:stop() cancels a route.
Disabled agents pause without advancing their path. Stalled controllers report
blocked instead of pretending to arrive. Reissue move_to to retry.
Survey defaults: max_slope=30 degrees, sample_height=500, sample_depth=1000. Survey cells include center and four clearance samples. Bounds and sampling heights should match your world. Physics remains authoritative between samples.
Run isolated package policy tests with demi package test packages/sources/demi.navigation.ground. Native A* and character collision are covered separately by engine and runtime tests.
Version 0.2 starts routes at the actor’s exact position, including when it is
leaving a blocked start cell. Set face_movement=true in agent options to turn
the actor toward its movement without tilting it; the default preserves rotation.
Version 0.3 removes unnecessary grid waypoints by taking clear straight segments
through the route. Movement can use arbitrary angles; the grid remains an
internal search and terrain-survey representation. Shortcut checks traverse every
touched survey cell (including corner neighbours) and test footprint boxes expanded
by agent radius. They use cached data and run when planning, not every frame.
Intermediate corners are reached precisely, without a frame of stale velocity.
With a zero goal radius, the endpoint is the requested world position rather than
a cell centre; blocked exact endpoints return PATH_GOAL_BLOCKED.
This is conservative route simplification, not a navmesh or curved steering. Turns remain at obstacle corners, and terrain sampling resolution still matters. The character controller remains responsible for collision between samples.
When an actor starts inside footprint clearance, simplification keeps the original first escape edge instead of taking a long shortcut through that footprint.
