Pathfinding: A* and NavMesh
How A* works
Dijkstra expands nodes by g(n) (distance from the start) in every direction. A* adds h(n) — an estimate of "how much is left to the goal" — and pulls the search toward the goal:
The key property is admissibility: if h never overestimates the true cost, the path is optimal. On a grid you use Manhattan distance (4-connectivity) or Euclidean/octile (8-connectivity). The closer h gets to the truth without exceeding it, the fewer nodes A* expands — in the limit h = true cost → you walk straight to the goal.
NavMesh — why not cells
A grid of cells is precise but expensive: an open field is thousands of nodes about nothing. A NavMesh covers the walkable space with a small number of convex polygons; inside a polygon you can walk in a straight line. A* runs over the polygon graph (dozens, not thousands), and string-pulling (the funnel algorithm) straightens the path instead of leaving a staircase through cell centers. That is why navigation in 3D games is almost always a NavMesh (Recast/Detour, the built-in navigation in Unreal/Unity/Godot).
🕹 Games to play — and what to notice
One question — "how do I get to the goal" — with different answers under different constraints, from "no search at all" to 3D voxels. For each: how it is done, and what to play to see it with your own eyes.
The ghosts don't build a path at all. At each junction every ghost greedily picks the direction that minimizes straight-line distance to its own target tile (Blinky targets Pac-Man's tile, Pinky aims 4 tiles ahead of him, Inky and Clyde have their own rules). One tile of lookahead, no reversing, no memory — O(1), because 1980 hardware allowed nothing more.
🎮 Play: fire up Pac-Man (any browser port). Corner the ghosts and watch them split into different directions at junctions — each has its own target. This is "pathfinding" with no path in it.
A uniform grid, 4/8-connectivity — exactly what the lab above does. Early RTS, roguelikes, tactics games: the map is known and static → the classical approach beats everything.
🎮 Play: open the A* lab and draw some walls — that is literally it. Or a classic like the first Warcraft / Heroes of Might & Magic.
A* over blocks in 3D. Neighbors are "where you can step": a 1-block step up, a drop within the fall limit, a jump across a gap. Nodes are scored by a NodeEvaluator with penalties (malus): water and lava carry a huge cost and get routed around, fire/fences/doors have their own weights. The search is capped by a node budget (not the whole world) and throttled across ticks — otherwise 3D A* per mob would kill the server. Flying and swimming mobs use a volumetric variant instead of "standing on a block".
🎮 Play: in Minecraft, spawn a zombie or a pig and ring it with a lava moat or a fence — watch it route around the danger and climb block steps. Dig a 2–3 block pit and it gets stuck (jump height limit / node budget). That is 3D pathfinding in your hands.
In a continuous 3D world voxel A* is far too expensive. You bake a NavMesh (polygons of walkable surface, Recast), run A* over the polygon graph and add the funnel for a smooth path. Minecraft differs because its world already is a grid of blocks — voxels are the natural fit there.
🎮 Watch: in a Godot/Unity navigation demo (or any game with a dev console) turn on navmesh debug drawing — you will see the walkable polygons laid over the level and the funneled path.
A* per unit does not survive hundreds of them. Supreme Commander and StarCraft II use flow fields (one field computed from the goal → a direction in every cell, everyone follows it) plus local collision avoidance (boids / ORCA). One search per goal instead of one search per unit.
🎮 Play: in StarCraft II, or any RTS, select 50+ units and send them across a narrow bridge — you will see them flow as one stream and jostle each other rather than each computing its own route.
Deep end: why A* is optimal — and where consistency comes inskippable
Admissibility: h(n) ≤ h*(n) — the heuristic never overestimates the true cost to the goal. Claim: A* (tree search) with an admissible h returns an optimal path.
Proof sketch
Suppose A* is about to return a goal G₂ with g(G₂) > C* (suboptimal; C* is the optimal cost). At that moment the frontier contains a node n on an optimal path, for which
(the first step is admissibility, the second is that n lies on an optimal path). But A* picked G₂ with f(G₂) = g(G₂) > C* ≥ f(n) — so it was obliged to expand n before G₂. Contradiction. ∎
Admissibility vs consistency
Consistency (monotonicity): h(n) ≤ c(n,n′) + h(n′) for every edge. Consistency ⇒ admissibility, and ⇒ f is non-decreasing along a path ⇒ when A* expands a node its g is already optimal ⇒ graph search with a closed set never needs to reopen nodes. A heuristic that is admissible but not consistent may require reopening closed nodes, otherwise optimality is lost. So you aim for a consistent h (Manhattan on 4-connectivity, octile/Euclidean on 8-connectivity — all consistent).
Why a sharper heuristic is provably cheaper
A* expands every node with f(n) < C* and none with f(n) > C*. If h₂ ≥ h₁ everywhere (both consistent), h₂ dominates: {f₂ < C*} ⊆ {f₁ < C*} → it expands no more nodes. In the limit h = h* only the nodes on an optimal path get expanded.
Weighted A* — trading optimality for speed
With f = g + w·h (w ≥ 1) the search is greedier and faster, but suboptimal by a bounded amount: cost ≤ w·C*. One knob spans the spectrum: w=0 → Dijkstra (g only), w→∞ → greedy best-first (h only), w=1 → A*.
Deep end · engineering: pathfinding in production at scaleskippable
- Time-slicing: budget N searches per frame and spread the queue across frames — otherwise you get a spike the moment 50 units request a path at once.
- Hierarchy (HPA*): a coarse graph of regions plus local search inside them — orders of magnitude cheaper than flat A* on large maps.
- Local avoidance ≠ the global path: A* builds the route, but "don't walk into your neighbors" is a separate layer (ORCA/boids/steering). Every other junior conflates the two.
- The NavMesh is baked at build time (Recast); the runtime only queries it; dynamic obstacles → partial re-bake or local avoidance.
- Cache paths for common routes; a full recompute every frame is an antipattern.
Optimization / search: A* is everywhere in planning, routing and compilers (register allocation is graph search); the heuristic is the lower bound in branch-and-bound.
ML / AI: beam search in LLM decoding is the same informed tree search; MCTS (AlphaGo) is search plus a learned value heuristic; "when A* beats RL" is "when the classical approach beats ML". Flow field = batching: one solve from the goal for all agents ≈ one forward pass per batch instead of per example — the basis of efficient inference.
Backend / systems: shortest paths in service graphs, packet routing; time-slicing a search is the latency budget of a service.
The principle: don't compute per agent when you can compute once for all of them; and reach for the expensive tool only when the cheap one fundamentally cannot cope.
Is the Manhattan heuristic still admissible on an 8-connected grid?
Are A*, Dijkstra and greedy best-first really one algorithm?
f = g + w·h: w=0 → Dijkstra (cost so far only; optimal, but it floods the map), w→∞ → greedy (heuristic only; fast, not optimal), w=1 → A*. Turning the weight slides you between "reliable and slow" and "fast and approximate" (see the deep end on weighted A*).JPS (Jump Point Search) — why can you "jump" over nodes on a grid?
Is a flow field for 500 units still A*?
Why is RL almost never used for navigation in shipped games?
- Amit Patel, "Red Blob Games: A* / pathfinding" — the best interactive treatment on the web.
- Recast & Detour — industry-standard NavMesh generation and navigation (open source).
- Module 11, section 1.5 + Lab 11b.