Navmeshes, paths, cover, and crowds of agents that walk them. Everything is
gander:: in Goose. Enemy brains stay your own code (a state machine with
enum + match); the engine answers "how do I get there", "where can I hide"
and moves bodies around walls and each other.
Build a navmesh once
var nav: gander::Nav;
fn main() {
// ... build the level (walls as MESH_CUBE boxes, models, terrain)
nav = gander::nav(gander::NavSettings { radius: 0.4, height: 1.8 });
gander::nav_add_mesh(nav, gander::MESH_CUBE, floor_xf);
for w in walls { gander::nav_add_mesh(nav, gander::MESH_CUBE, w); }
gander::nav_add_model(nav, house, house_xf); // rest pose, skinned parts skipped
gander::nav_add_terrain(nav, land, gander::v3(0.0, 0.0, 0.0), 150.0); // ground within 150 m
gander::nav_link(nav, ledge_top, ledge_bottom, false); // a drop only walked down
if !gander::nav_build(nav) { gander::fail("no walkable navmesh"); }
}
Add every walkable and every blocking surface: floors, walls, stairs, props
big enough to walk around. NavSettings: radius (how close to walls),
height (headroom), climb (tallest step), slope (degrees), cell (voxel
size: 0 = radius / 2, good indoors; 0.3 to 0.5 for large outdoor areas, it
builds much faster), tile (meters per tile, 32). nav_add_terrain samples the ground
every 4 cells (at least 1 m), too coarse for trenches and ditches a few metres
wide: nav_add_terrain_detail(nav, land, center, radius, step) takes the step
(0 = every height sample; keep the radius to where agents walk). One Nav per agent size: a
soldier and a tank need two. The log prints tiles, polygons and build time;
nav_debug(nav, color) draws the polygon edges.
Paths and queries
nav_path(nav, a, b, out: float3[>..]&) -> bool corners of the shortest walk; false when no way
(cut off: ends as near as it gets, still true)
nav_closest(nav, p) -> float3 snap a point onto the mesh
nav_on(nav, p) -> bool is p standing on it
nav_raycast(nav, a, b) -> f32 1 = a straight walk gets there; less = a wall or drop first
nav_random(nav, center, radius) -> float3 a reachable point (patrols, wandering, spawns)
nav_cover(nav, near, radius, threat, out&) -> i32 spots beside walls that hide them from threat, nearest first
nav_block(nav, center, size, yaw) -> i32 id closed doors, barricades, a burning car
nav_unblock(nav, id) (the touched tiles rebuild over the next frames)
nav_raycast is line of sight on the floor plan: a cheap "can that guard see
the player" before a real raycast against physics bodies. nav_cover uses
the navmesh's walls (tall cover).
Agents (crowds)
The engine walks agents every frame with steering, avoidance and separation; your code decides where they go.
let a = gander::agent_add(nav, spawn, 3.5); // speed m/s
gander::agent_move(nav, a, gander::camera_pos()); // re-send when the target moves (a few times a second is plenty)
let p = gander::agent_pos(nav, a); // draw the character here
let v = gander::agent_vel(nav, a); // face it: yaw = atan2(-v.x, -v.z); pick walk / run by length(v)
if gander::agent_arrived(nav, a, 1.0) { ... } // within 1 m of where it was sent
agent_stop, agent_teleport, agent_speed (sprint, limp), agent_remove
(dead: then play a ragdoll from its last position). Up to 128 agents per
Nav. Agents stand on the navmesh; for jumping or flying enemies, move them
yourself and use nav_path for the route.
Recipes
Patrol, chase, search. State machine per enemy: Patrol sends the agent
to nav_random points (or a list), Chase re-sends agent_move to the
player every 0.25 s while nav_raycast(nav, eye, player) == 1.0 or a
physics raycast sees them, Search goes to the last seen point then
patrols nearby.
Turn to face, then act. A creature standing still that must face the
player (to attack, to breathe fire) turns with its turn clips, not by a rate
over its idle: when the angle to the target passes ~40 degrees, play turn_l /
turn_r / turn_180 with anim_root_turn on and add anim_root_yaw to its yaw
(the recipe "Turning in place with turn clips" in the API sheet's animation
section); attack once it ends. Moving agents keep facing their velocity.
Take cover and shoot. On first contact: nav_cover(nav, my_pos, 12.0,
player, spots), send the agent to spots[0], and when it arrives alternate
crouched (behind cover) and standing (fire) every couple of seconds.
Circle and take turns (melee). Let only one or two enemies attack at
once; send the rest to points on a ring around the player
(player + v3(cos(a), 0, sin(a)) * 4, snapped with nav_closest), moving
their angles a little each second.
Doors. nav_block the doorway while it is shut and nav_unblock when it
opens; agents re-route by themselves.