All tutorials Mighty Professional
Build a Game Engine · AI

Game AI: Navmesh & Steering

A guard's behavior tree says "go to the bar", and pathfinding can find a route once it has a map. That still leaves three jobs: a map of where the guard can walk (the navmesh), a path that doesn't zig-zag through every doorway (the funnel), and movement that speeds up, slows down and slips past the other guards (steering, flocking and local avoidance). This page builds all three.

Time~55 min LevelSenior PrereqsThe Graph Traversal tutorial (A* and its heuristic, which this page builds on), and Behavior Trees (the NavAgent seam). StackC++ & Rust
◂ Build a Game Engine Phase 11 · AI Next · Netcode & Rollback ▸
What you'll be able to do by the end

01Why not a grid

The simplest map of where an agent can walk is a grid: chop the floor into square cells, mark each one walkable or blocked, and search from cell to neighboring cell, with 4 or 8 neighbors per cell. It's the right structure for tile-based games (the world is a grid) and is simple to build and query. For continuous 3D worlds a navmesh, a set of walkable polygons covering the floor (§2), has three advantages, though grids remain the right choice when the game itself is made of tiles:

Worked example

An empty 20 m × 20 m room at 0.25 m cells is 80 × 80 = 6,400 grid cells; as a navmesh it's one rectangle. If a 0.6 m gap elsewhere forces 0.2 m cells, the same room becomes 100 × 100 = 10,000 cells. Paths suffer too: an 8-connected grid path 10 cells across and 4 down takes 4 diagonal steps and 6 straight ones, 4 × 1.414 + 6 ≈ 11.66 cell widths, against √(10² + 4²) ≈ 10.77 for the straight line. That's about 8% longer, with a visible kink.

The guard needs to know where it can stand and how those places connect, in a form a search can walk quickly. A represents the walkable surface as convex polygons that tile it. Adjacent polygons sharing an edge are linked into the dual graph: each polygon is a node, each shared edge a link[1]. Convexity is load-bearing: for any two points inside one convex polygon, the straight segment between them stays inside, so movement within a polygon is collision-free by construction and needs no search. Search only happens between polygons.

Worked example

An L-shaped corridor can't be one convex polygon: a straight line from the end of one arm to the end of the other cuts across the inside corner, through the wall. Split it at the corner into two rectangles that share an edge and both pieces are convex. The dual graph has 2 nodes and 1 link, and an agent anywhere in either rectangle can walk straight to any other point in the same rectangle.

It's convex polygons (not just triangles), and it's per-agent

Triangles are one tessellation; Recast (§3) emits convex polygons of up to 6 vertices (Detour's per-polygon limit), not only triangles. The navmesh is an abstraction of the walkable surface for a specific agent radius and height: a fatter agent needs a different, more inset navmesh, so it isn't "the floor geometry." The polygon-adjacency links are undirected; only off-mesh links (jumps, ladders, drops) can be one-directional.

The navmesh polygon and its dual graph
// Each polygon stores its vertices AND, per edge, the neighbor polygon across
// that edge (or NO_NEIGHBOR for a wall). The neighbor array IS the dual graph A* walks.
constexpr int MAX_VERTS   = 6;    // Detour's DT_VERTS_PER_POLYGON
constexpr int NO_NEIGHBOR = -1;

struct NavPoly {
    std::array<int, MAX_VERTS> vertIndex;   // indices into the shared vertex pool
    std::array<int, MAX_VERTS> neighbor;    // neighbor poly per edge, or NO_NEIGHBOR
    int vertCount;                          // 3..6 (Recast emits up to 6-gons)
};
struct NavMesh {
    std::vector<Vec3>    vertices;             // shared vertex pool
    std::vector<NavPoly> polys;                // nodes of the dual graph; neighbor[] feeds A*
};
// Each polygon stores its vertices AND, per edge, the neighbor polygon across
// that edge (or NO_NEIGHBOR for a wall). The neighbor array IS the dual graph A* walks.
const MAX_VERTS: usize = 6;     // Detour's DT_VERTS_PER_POLYGON
const NO_NEIGHBOR: i32 = -1;

struct NavPoly {
    vert_index: [i32; MAX_VERTS],   // indices into the shared vertex pool
    neighbor:   [i32; MAX_VERTS],   // neighbor poly per edge, or NO_NEIGHBOR
    vert_count: usize,             // 3..6
}
struct NavMesh {
    vertices: Vec<Vec3>,   // shared vertex pool
    polys:    Vec<NavPoly>, // nodes of the dual graph; neighbor[] feeds A*
}

03Generating it

Nobody wants to place polygons by hand over a 2 km level, and level geometry is rarely clean: walls overlap, floors have gaps, props intersect. The common automatic approach (Mononen's Recast, which powers the navigation in Unity, Unreal, Godot, and O3DE according to its README) is a voxelization pipeline, run at bake time[2]. The stages below follow Recast's own sample build[2]; Unity's documentation describes its bake in the same terms: rasterize to voxels, extract walkable surfaces, build the mesh[11].

  1. Mark and rasterize: flag triangles too steep to walk on (the slope limit), then rasterize the collision triangles into a solid heightfield of voxel spans.
  2. Filter walkable spans: keep low obstacles the agent can step over (climb height), drop ledges higher than the climb height, and drop spans without head clearance (agent height).
  3. Build a compact heightfield, erode the walkable area by the agent radius, and build a distance field (distance to the nearest border).
  4. Region-segment the walkable area (watershed in Recast's sample; monotone or layer partitioning as faster alternatives).
  5. Trace and simplify contours around each region.
  6. Polygonize the contours into convex polygons (≤6 verts); a detail mesh adds height samples.

Worked example

Take an agent radius of 0.6 m. Step 3 erodes the walkable area by 0.6 m from every wall, so the agent's center can only go where its body fits. A 1.5 m doorway keeps a 1.5 − 2 × 0.6 = 0.3 m strip down the middle and stays open. A 1.0 m doorway would need −0.2 m, so it closes: the navmesh for this agent has no link through it, and A* routes around. A smaller agent gets a different navmesh in which that doorway is open.

Recast is the generator; Detour is the runtime; the result is quantized

Keep them distinct: Recast builds the mesh, Detour queries it (pathfind, crowd). Voxelization trades exactness for robustness: it runs on arbitrary triangle soup with no cleanup, but the navmesh edges are quantized to the cell size rather than sitting exactly on the source geometry. Hand-authored navmesh (a designer places polygons) gives exact control and is still used for fix-ups, but doesn't scale to large auto-built worlds.

04Corridor to taut path

A* hands back a list of polygons, and the obvious way to walk it, through the middle of each doorway, makes the guard weave like it's lost. Run A*, the shortest-path search guided by an estimate of the distance left, on the polygon dual graph. Each step needs a cost and a heuristic: Detour measures cost between the midpoints of the shared edges the path crosses (polygon centers are the simpler alternative) and uses straight-line distance to the goal as the heuristic, which is admissible since no path is shorter than a straight line. The Graph Traversal tutorial covers A* and the admissibility theorem, so this page builds on it. The key point: A* returns a polygon corridor (an ordered strip of polygons), not a line.

Don't walk through polygon centers: that's the zig-zag bug

Connecting polygon centroids, or the midpoints of the shared edges A* scored, gives a kinked, needlessly long path. The fix is the : pull a string taut around the corridor's inner corners. Each shared edge is a portal with a left and right vertex; the funnel keeps an apex and two feet and walks the portals, narrowing the wedge until a portal vertex crosses the opposite side, which marks a path corner. It is a sweep over the corridor, not another search. A corridor is a triangulated simple polygon, and shortest paths inside one take linear time with the classic funnel method[12]; Mononen's Simple Stupid Funnel, below, instead restarts its scan from the new apex after each corner, which re-reads some portals but is much shorter to write[3]. The result is the shortest path constrained to the corridor A* chose; a different corridor could be shorter. It also pulls the path tight against corner vertices, which is safe when the navmesh was eroded by the agent radius at build time (as Recast's is); systems that share one mesh across agent sizes inset each portal by the radius instead.

Worked example

Take the widget below on a 690 × 388 px canvas. The start sits at (55, 194) and the goal at (634, 194), and the three gaps between them alternate high, low, high. Through the gap midpoints the path is about 803 px long. The funnel path bends only at the lower corner of the first and third gaps and the upper corner of the second, and it's about 612 px: 24% shorter, from the same corridor.

The navmesh below is four rectangular polygons joined through three gaps in the walls, the portals; the corridor A* returns is all four. Drag the start (blue, in the first polygon) and the goal (amber, in the last), switch between the two paths, and press walk:

Drag the start and goal, switch between the two paths, and press walk to send an agent along the selected one. The portal-midpoint path goes through the middle of each gap, so it zig-zags farther than it needs to. The funnel path is taut: the shortest line through that corridor, bending only at the gap corners it must round. The two lengths are shown bottom left; with the default start and goal on a desktop-width canvas the funnel path is about a quarter shorter. Walk the agent to compare.
The Simple Stupid Funnel over the corridor's portals
// Twice the signed area of triangle abc, with Mononen's sign convention: in a y-up
// frame it is positive when c lies to the RIGHT of the ray a->b (the negated 2D cross
// product). Portals give left/right as seen walking along the corridor. With the
// ordinary cross product the comparisons below would all flip.
float triArea2(Vec2 a, Vec2 b, Vec2 c) { return (c.x-a.x)*(b.y-a.y) - (b.x-a.x)*(c.y-a.y); }

// portals[0] is the start point (left == right == start); the last portal is the goal.
std::vector<Vec2> stringPull(const std::vector<Portal>& portals) {
    std::vector<Vec2> path;
    Vec2 apex = portals[0].left, left = portals[0].left, right = portals[0].right;
    int apexIndex = 0, leftIndex = 0, rightIndex = 0;
    path.push_back(apex);
    for (int i = 1; i < static_cast<int>(portals.size()); ++i) {
        Vec2 newLeft = portals[i].left, newRight = portals[i].right;
        if (triArea2(apex, right, newRight) <= 0) {          // right foot moves inward (or stays)
            if (apex == right || triArea2(apex, left, newRight) > 0) {
                right = newRight; rightIndex = i;                    // narrow the funnel
            } else {                                                 // right crossed over left:
                path.push_back(left);                                // the left foot is a corner
                apex = left; apexIndex = leftIndex;                  // restart the funnel there
                left = apex; right = apex; leftIndex = apexIndex; rightIndex = apexIndex;
                i = apexIndex;                                       // ++i resumes after the new apex
                continue;
            }
        }
        if (triArea2(apex, left, newLeft) >= 0) {            // left foot: the mirror image
            if (apex == left || triArea2(apex, right, newLeft) < 0) {
                left = newLeft; leftIndex = i;
            } else {
                path.push_back(right);
                apex = right; apexIndex = rightIndex;
                left = apex; right = apex; leftIndex = apexIndex; rightIndex = apexIndex;
                i = apexIndex;
                continue;
            }
        }
    }
    if (!(path.back() == portals.back().left))   // the goal can already be the last corner
        path.push_back(portals.back().left);
    return path;
}
// Twice the signed area of triangle abc, Mononen's sign: positive when c is to the
// RIGHT of a->b in a y-up frame. Portals give left/right as seen walking the corridor.
fn tri_area2(a: Vec2, b: Vec2, c: Vec2) -> f32 { (c.x-a.x)*(b.y-a.y) - (b.x-a.x)*(c.y-a.y) }

// portals[0] is the start point (left == right == start); the last portal is the goal.
fn string_pull(portals: &[Portal]) -> Vec<Vec2> {
    let mut path = Vec::new();
    let (mut apex, mut left, mut right) = (portals[0].left, portals[0].left, portals[0].right);
    let (mut left_index, mut right_index) = (0usize, 0usize);
    let mut apex_index: usize;   // set whenever a corner is found
    path.push(apex);
    let mut i = 1;
    while i < portals.len() {
        let (new_left, new_right) = (portals[i].left, portals[i].right);
        if tri_area2(apex, right, new_right) <= 0.0 {          // right foot moves inward (or stays)
            if apex == right || tri_area2(apex, left, new_right) > 0.0 {
                right = new_right; right_index = i;                   // narrow the funnel
            } else {                                                  // right crossed over left:
                path.push(left);                                      // the left foot is a corner
                apex = left; apex_index = left_index;                 // restart the funnel there
                left = apex; right = apex; left_index = apex_index; right_index = apex_index;
                i = apex_index + 1;                                  // resume after the new apex
                continue;
            }
        }
        if tri_area2(apex, left, new_left) >= 0.0 {            // left foot: the mirror image
            if apex == left || tri_area2(apex, right, new_left) < 0.0 {
                left = new_left; left_index = i;
            } else {
                path.push(right);
                apex = right; apex_index = right_index;
                left = apex; right = apex; left_index = apex_index; right_index = apex_index;
                i = apex_index + 1;
                continue;
            }
        }
        i += 1;
    }
    let goal = portals[portals.len() - 1].left;
    if path.last() != Some(&goal) { path.push(goal); }   // the goal can already be the last corner
    path
}

05Path to motion: steering

The guard now has a list of corners. Snap its velocity straight at each one and it turns instantly, runs at full speed into the last corner and stops dead, which looks robotic. Pathfinding is global and discrete (which polygons, computed occasionally). is local and continuous (how to accelerate this frame, every frame). The path supplies the corners (from the funnel); steering produces smooth acceleration toward the current one using Reynolds' simple vehicle model[4]: a steering force clamped to a max, integrated into a velocity clamped to a max speed. In an engine, steering stops at that velocity: the character controller (§8) moves the agent and owns its position. The widgets below have no physics, so they advance position from the velocity themselves.

Don't conflate pathfinding with steering, and arrive is not seek

Pathfinding is global, discrete, and infrequent; steering is local, continuous, and per-frame. Steering executes on top of a path; it doesn't replace it. And the behaviors differ precisely: seek wants full speed at the target, so it arrives fast, overshoots, and turns back for another pass (Reynolds compares it to a moth buzzing a light bulb); arrive brakes by ramping desired speed down to zero inside a slowing radius. Wander is a coherent random walk on a circle projected ahead (not per-frame noise); pursue seeks the target's predicted position, not its current one.

Worked example

The widget below uses a max speed of 150 px/s and a slowing radius of 120 px. Arrive's desired speed is 150 × distance / 120, capped at 150: full speed beyond 120 px, 75 px/s at 60 px, 15 px/s at 12 px, and 0 on the target. Seek's desired speed is 150 px/s at every distance, so it reaches the target still moving at full speed and has to turn around.

The target hops to a new spot every few seconds (or drag it yourself). Compare seek, which overshoots, with arrive, which brakes, and with wander:

Switch between seek, arrive and wander. In seek and arrive the target hops every 3.5 s unless you've dragged it in the last 6 s. Seek reaches the target at full speed (it always wants full speed at the point), overshoots, and loops back; since the steering force is proportional to the velocity error, the loops shrink until the agent jitters on the point instead of stopping. Arrive ramps speed down inside the slowing radius and stops cleanly. Wander meanders smoothly. Drag the target mid-flight and the steering re-aims.
Steering behaviors: seek (overshoots) vs arrive (brakes)
// clampMag truncates a vector to a max length. Reynolds' simple vehicle (mass = 1).
Vec2 seek(const Agent& a, Vec2 target) {
    Vec2 desired = normalize(target - a.position) * a.maxSpeed;  // full speed at the target
    return clampMag(desired - a.velocity, a.maxForce);          // no braking -> overshoots
}
Vec2 arrive(const Agent& a, Vec2 target, float slowingRadius) {
    Vec2 offset = target - a.position; float dist = length(offset);
    if (dist < 1e-4f) return {0,0};
    float speed = min(a.maxSpeed * (dist / slowingRadius), a.maxSpeed);  // ramp down near target
    Vec2 desired = offset * (speed / dist);
    return clampMag(desired - a.velocity, a.maxForce);
}
// Steering writes velocity only (forward Euler on velocity). The physics character
// controller takes a.velocity as its desired velocity and owns the position:
// it moves and collides (collide-and-slide, slopes, step-up) and writes the result.
void applySteering(Agent& a, Vec2 steering, float dt) {
    a.velocity = clampMag(a.velocity + steering * dt, a.maxSpeed);
}
fn seek(a: &Agent, target: Vec2) -> Vec2 {
    let desired = (target - a.position).normalize() * a.max_speed;  // full speed at target
    clamp_mag(desired - a.velocity, a.max_force)                  // no braking -> overshoots
}
fn arrive(a: &Agent, target: Vec2, slowing_radius: f32) -> Vec2 {
    let offset = target - a.position; let dist = offset.length();
    if dist < 1e-4 { return Vec2::ZERO; }
    let speed = (a.max_speed * (dist / slowing_radius)).min(a.max_speed);  // ramp down
    let desired = offset * (speed / dist);
    clamp_mag(desired - a.velocity, a.max_force)
}
// Steering writes velocity only (forward Euler on velocity). The physics character
// controller takes a.velocity as its desired velocity and owns the position.
fn apply_steering(a: &mut Agent, steering: Vec2, dt: f32) {
    a.velocity = clamp_mag(a.velocity + steering * dt, a.max_speed);
}

06Combining & flocking

A sheep in a flock wants to stay near the others, not bump into them, and head the same way, all in the same frame. So each agent runs several steering behaviors at once, and their outputs have to become one acceleration. Multiple behaviors blend by weighted sum (add each steering vector × weight, truncate), priority arbitration (take the highest-priority non-trivial behavior), or context steering (each behavior writes interest and danger into directional bins, and the agent picks the best bin)[13]. (Reynolds' boids) is three behaviors over local neighbors[5]: separation (steer away from neighbors' positions), cohesion (steer toward their centroid), alignment (match their heading).

Worked example

An agent seeks a doorway to its right, steering (+1, 0) at weight 1, while separation pushes it away from a neighbor standing in the doorway, (−1, 0) at weight 1. The weighted sum is (0, 0): the agent stands still, and stays there as long as the neighbor does. Priority arbitration would let one behavior win outright; context steering would mark the doorway direction as both interesting and dangerous and pick the best remaining direction.

Weighted sum can cancel, and separation is not collision-free

Naive weighted sum has two failure modes: equal-and-opposite cancellation (two behaviors point opposite and sum to about zero, so the agent stalls) and getting stuck in local minima (wedged against a concave obstacle). Priority arbitration and context steering exist to avoid these, each with its own cost (priority arbitration can jitter when the winning behavior flips). Boids is purely local: the flock pattern emerges with no central controller, and the three rules are distinct: separation = repel from positions, cohesion = attract to the centroid, alignment = match heading. Separation gives soft spacing, not guaranteed collision-free motion; that is local avoidance's job (§7).

07Local avoidance

Two guards walk down the same corridor in opposite directions. Each one's path is fine; together they're about to walk through each other, and neither path knew the other existed. A velocity obstacle (VO) is the set of an agent's velocities that, relative to a neighbor, lead to a collision: a cone in velocity space with its apex at the neighbor's velocity, cut off at a time horizon in most game implementations. Pick a velocity outside it and you avoid the collision, provided the neighbor keeps its velocity. The concept comes from Fiorini and Shiller's robotics work[6].

Plain VO oscillates; reciprocity fixes the two-agent case; ORCA is the LP formulation

If two agents each steer fully out of the other's VO, both dodge; on the next update each sees that the other has already moved aside, finds its preferred velocity clear again, and snaps back; then both are on a collision course and dodge again. That is VO oscillation. (It is different from a reciprocal dance, where the agents disagree about which side to pass on.) RVO (van den Berg, Lin, and Manocha) has each agent take half the responsibility: its new velocity is the average of its current velocity and one outside the VO. For two agents that each pick the closest such velocity, the paper proves the motion is collision-free and oscillation-free, and both pick the same side[6]. With three or more agents the guarantee lapses and oscillation can return, though far less than with plain VO[8]. ORCA gives a half-plane velocity constraint per neighbor and picks the velocity closest to preferred inside their intersection via a small linear program (when the constraints are infeasible in a dense crowd, a three-dimensional LP picks the velocity that least violates them)[7]. RVO and ORCA are different algorithms: RVO is the averaging idea (2008), ORCA the half-plane constraints plus LP (ISRR 2009), and the reference library that implements ORCA is confusingly named RVO2[8]. RVO-family avoidance is widespread in games: Unity's navigation agents use RVO[11], and Unreal offers RVO alongside Detour's crowd manager[11].

Local avoidance does not replace pathfinding

VO/RVO/ORCA assume constant-velocity neighbors and a local horizon, so they get stuck in concave geometry and dead ends without a global path. Local avoidance is a layer on top of pathfinding, never a substitute for it.

Worked example

The widget's crossing agents are 20 px in radius (40 px combined), walk at 70 px/s, and look 1.5 s ahead. Head-on they close at 140 px/s, so their predicted collision falls inside the horizon once their centers are 40 + 140 × 1.5 = 250 px apart. At 300 px apart the predicted collision is (300 − 40) / 140 ≈ 1.9 s away, beyond the horizon, so both keep their preferred velocity. Inside 250 px each must pick a velocity outside the other's cone.

The first mode is boids with separation, cohesion, and alignment. The second has two agents crossing head-on, each choosing its velocity with a velocity obstacle; the checkbox switches between plain VO and RVO:

Pick flocking or two crossing; in two crossing, toggle the reciprocal (RVO) box and watch the trails and the heading-change count. Flocking: separation, cohesion, and alignment produce an emergent flock from local rules. Two crossing: each agent heads for the other's start and picks a new velocity six times a second. With the box unchecked (plain VO), each dodges fully, finds its way clear once the other has dodged too, snaps back, and dodges again: the trails come out as staircases and the heading-change count reaches 10 or more. With RVO, each takes half the dodge and keeps it: one smooth swerve, two heading changes. Both get past each other; the difference is the jitter.

08Crowds & the BT seam

Thirty guards in one courtyard all want the same doorway. Steering each one in isolation gives a pile-up; something has to plan their corridors, spacing and avoidance together. A crowd manager runs many agents together: path-corridor following, local avoidance, radius-based separation, and replanning. Detour's dtCrowd is the open-source reference. Its avoidance scores sampled candidate velocities against RVO-style velocity obstacles rather than solving ORCA's LP, and its documentation puts the practical maximum at 20 to 30 agents under one crowd manager, which takes over their positions entirely[9]. For hundreds to thousands of units, one documented approach is flow-field tiles (a shared vector field per goal, shipped in Supreme Commander 2) in place of per-agent pathfinding[10].

Worked example

A flow field for a 100 × 100 tile region stores 10,000 direction vectors, one per tile, pointing along the best route to the goal. Ten units or a thousand read the same field, so its cost depends on the area it covers, while per-agent pathfinding runs one search per unit: a thousand searches for a thousand units.

The decision / navigation / physics split

This module implements the NavAgent the behavior tree's MoveTo leaf already calls: requestPath(target) (A* → corridor → funnel → path) and hasArrived(), from which the leaf returns RUNNING, SUCCESS, or FAILURE. The BT decides where, navigation gets there, and steering hands a desired velocity to the physics character controller, which owns the final position (collision, slopes, step-up). Steering writes velocity, never position; that is the contract applySteering in §5 follows. Agent radius is load-bearing twice: it insets the navmesh and sets the avoidance spacing. Dynamic obstacles need either a navmesh change (for obstacles that move and then stay put, such as a crate pushed into a doorway: rebuild the affected tiles or carve a hole) or runtime avoidance (for obstacles that keep moving). Unity's navigation documentation draws the line the same way[11].

Wrong answers, and why: a zig-zag is a corridor-extraction problem fixed by the funnel (not the heuristic or polygon convexity); and agents stuck in concave geometry need a global path (local avoidance is never a planner; reciprocity and horizon don't add a map).

09Pitfalls

"Grids are obsolete"Tile games win with grids. Navmesh wins on compactness + free-form paths.
Pathing through poly centersZig-zag. Run the funnel over the corridor (O(n), not a search).
Recast = DetourRecast builds the mesh; Detour queries it. Different libraries.
Conflating pathfinding & steeringGlobal/discrete vs local/continuous. Steering runs on top of a path.
"Arrive is seek with low max speed"Arrive brakes in a slowing radius; seek overshoots and loops back.
Swapping boids rulesSeparation=position, cohesion=centroid, alignment=heading. Don't mix them.
"RVO removes oscillation"It removes plain VO's two-agent oscillation; with three or more agents it can come back.
Avoidance as navigationIt gets stuck in concave geometry. Always run a global path under it.

10What's next

Agents can think (behavior trees), find a route (pathfinding), and now move and avoid each other. The next module, Netcode & Rollback, connects players across the wire with prediction, reconciliation, interpolation, lag compensation and rollback. Tooling and the 3D-game capstone follow. The full path is on the series hub.

  1. Greg Snook. "Simplified 3D Movement and Pathfinding Using Navigation Meshes." Game Programming Gems (2000). An early, widely cited description of navigation meshes for games: convex polygons, the straight-line-traversable interior, and A* on adjacency.
  2. Mikko Mononen et al. Recast & Detour. github.com/recastnavigation. The README states that Recast "powers AI navigation features in Unity, Unreal, Godot, O3DE and countless AAA and indie games and engines." The build stages in §3 follow the repository's RecastDemo/Source/Sample_SoloMesh.cpp, and DT_VERTS_PER_POLYGON = 6 is in DetourNavMesh.h.
  3. Mikko Mononen. "The Simple Stupid Funnel Algorithm." Digesting Duck (2010). digestingduck.blogspot.com. The compact apex/left/right portal funnel the §4 code follows, including its triarea2 sign convention; the post notes that it restarts the scan from the new apex after each corner, recomputing some portals.
  4. Craig Reynolds. "Steering Behaviors For Autonomous Characters." GDC 1999. red3d.com. Seek, flee, arrive, pursue, evade, wander, and the simple vehicle model.
  5. Craig Reynolds. "Flocks, Herds, and Schools: A Distributed Behavioral Model." SIGGRAPH 1987. red3d.com. Separation, cohesion, alignment, emergent flocking from local rules.
  6. Jur van den Berg, Ming Lin, Dinesh Manocha. "Reciprocal Velocity Obstacles for Real-Time Multi-Agent Navigation." IEEE International Conference on Robotics and Automation (ICRA 2008), pp. 1928–1935. DOI: 10.1109/ROBOT.2008.4543489. PDF (archived copy of the authors' page). Reviews Fiorini and Shiller's velocity obstacle, shows it oscillates between two agents, and proves RVO's half-responsibility choice collision-free, oscillation-free, and same-sided for two agents; separates that oscillation from "reciprocal dances."
  7. Jur van den Berg, Stephen J. Guy, Ming Lin, Dinesh Manocha. "Reciprocal n-Body Collision Avoidance" (ORCA). Robotics Research: The 14th International Symposium ISRR (2009), Springer Tracts in Advanced Robotics 70, 2011, pp. 3–19. gamma.cs.unc.edu/ORCA/publications/ORCA.pdf. Half-plane per neighbor plus a low-dimensional linear program; when that LP is infeasible, a 3-D LP picks the "safest possible" velocity.
  8. Ben Sunshine-Hill. "RVO and ORCA: How They Really Work." Game AI Pro 3, ch. 19, 2017. gameaipro.com (chapter PDF). Calls RVO and its descendants the standard for collision avoidance in games; walks through VO's two-agent oscillation and RVO's three-agent failure case, and notes that the reference ORCA library is named RVO2.
  9. Mikko Mononen. DetourCrowd. recastnav.com. The crowd manager: corridor following, avoidance, and separation. The documentation gives "between 20 and 30" as the maximum agents under crowd management and requires handing position control to the crowd; DetourObstacleAvoidance.cpp scores sampled velocities (grid or adaptive sampling) against RVO-style obstacles.
  10. Elijah Emerson. "Crowd Pathfinding and Steering Using Flow Field Tiles." Game AI Pro, ch. 23, 2013. gameaipro.com (chapter PDF). The flow-field tile system used in Supreme Commander 2 for moving hundreds to thousands of units.
  11. Unity Technologies. "Inner Workings of the Navigation System." AI Navigation 2.0 manual. docs.unity3d.com. The bake "rasterizes the scene into voxels, then it extracts the walkable surfaces"; agents use reciprocal velocity obstacles; moving obstacles belong to local avoidance and stationary ones to the navmesh (carving). Also Epic Games, "Navigation System in Unreal Engine," dev.epicgames.com, which lists "Reciprocal Velocity Obstacles (RVO), and the Detour Crowd Manager" as its two avoidance methods.
  12. Leonidas Guibas, John Hershberger, Daniel Leven, Micha Sharir, Robert E. Tarjan. "Linear-time algorithms for visibility and shortest path problems inside triangulated simple polygons." Algorithmica 2:209–233, 1987. DOI: 10.1007/BF01840360. link.springer.com/article/10.1007/BF01840360. Shortest paths inside a triangulated simple polygon in linear time, the bound the funnel method achieves.
  13. Andrew Fray. "Context Steering: Behavior-Driven Steering at the Macro Scale." Game AI Pro 2, ch. 18, 2015. gameaipro.com (chapter PDF). Interest and danger maps over a ring of directions, and the cancellation problem of summed steering forces.

See also