Quick answer: When A* returns no path, the navmesh is almost always the problem. Visualize the region IDs, check for disconnected areas and off-mesh link mismatches, verify no runtime obstacle is carving the corridor, and log nodes expanded against your budget. Multi-agent collisions and stale cost caches are the next layer to check.
Pathfinding bugs are visible. A player can see the enemy walking into a wall, pacing in a corner, or refusing to chase them around a corner. These failures are almost never bugs in the A* algorithm itself — A* is decades old and well-understood — but in the graph it searches. The navmesh your level has at runtime is not the navmesh your designer saw in the editor, and the delta between them is where pathfinding bugs live.
Visualize First, Theorize Second
Do not try to reason about a pathfinding bug from logs alone. Draw the navmesh in-game, color-coded by region ID and area type, with start and goal positions highlighted. The bug will usually be obvious in the first frame you look at it: a gap in the mesh, an off-mesh link that ends in midair, a region separated from the rest by a carved obstacle.
void DebugDrawNavMesh() {
for (auto& tri : navMesh.triangles) {
Color c = RegionColor(tri.regionId);
DrawDebug::Triangle(tri.a, tri.b, tri.c, c, 0.3f);
DrawDebug::Text(tri.Centroid(),
FormatString("r%d a%d",
tri.regionId, tri.areaType),
0.5f);
}
for (auto& link : navMesh.offMeshLinks) {
DrawDebug::Arrow(link.start, link.end,
link.valid ? Color::Green : Color::Red);
}
}
Toggle this on when an agent reports a failed path. Most of the time you will see the problem without reading any code.
Disconnected Regions
A navmesh is split into regions, where each region is a connected component. Two points in different regions cannot be reached except through an off-mesh link. This is the single most common source of “no path found” bugs: the navmesh baker decided a corridor was too narrow, or a staircase was too steep, and silently excluded it.
Every pathfinding failure should log the region IDs of start and goal. When they differ and no off-mesh link spans them, you know the baker is the issue. Either adjust bake settings (walkable slope, agent radius, agent height) or place a manually authored link across the gap.
Off-Mesh Link Mismatches
Off-mesh links are the glue that holds complex levels together: ladders, jumps, teleports, vehicle entry points. They are also fragile. A link has a start point and an end point, and both must be on walkable navmesh. When a level designer moves a ladder by 30 centimeters, the link’s end point can drift off the top platform and land on a non-walkable triangle. The link still exists in the scene, but A* refuses to traverse it because the destination isn’t on the graph.
Validate links automatically in a prebuild step:
func ValidateLinks(mesh NavMesh) []LinkError {
var errs []LinkError
for _, l := range mesh.Links {
if !mesh.IsWalkable(l.Start) {
errs = append(errs, LinkError{l, "start off mesh"})
}
if !mesh.IsWalkable(l.End) {
errs = append(errs, LinkError{l, "end off mesh"})
}
if mesh.AreaCost(l.End, l.AgentType) == math.MaxFloat32 {
errs = append(errs, LinkError{l, "end area disallowed"})
}
}
return errs
}
Run this every build and fail CI on any mismatch. It saves hours of QA time.
Dynamic Obstacles
Many engines support runtime carving: an obstacle like a closed door or a destructible wall temporarily removes triangles from the navmesh. When that happens, an agent whose path runs through the carved area silently loses it. Worse, if the carving is happening on a background thread, the path an agent received a frame ago may already be invalid.
Rebuild paths when the navmesh mutates within the path’s corridor. Subscribe to the navmesh’s dirty notifications and invalidate any active path whose waypoints overlap a dirty tile. Do not wait for the agent to collide with an obstacle to realize its path is broken.
Budget Exhaustion
A* is guaranteed to find a path if one exists — but only if the algorithm runs to completion. Most game engines impose a node budget (typically 2,000 to 10,000 open-list expansions) to keep queries from blowing frame time. When the budget is exceeded, the algorithm returns whatever partial path it has so far and a flag indicating the result was truncated. If you ignore that flag, a “found” path may end in the middle of nowhere.
var result = navMesh.FindPath(start, goal, agent,
maxNodes: 4000);
if (result.status == PathStatus.PartialOnly) {
Log.Warn($"Pathfinding budget exhausted: {result.nodesExpanded} nodes");
// Don't follow partial path; try again with larger budget or fail
return PathResult.None();
}
Log budget exhaustions continuously. A rising rate means your navmesh has grown denser than your queries can handle — time to repartition, increase the budget, or simplify the mesh.
Multi-Agent Conflicts
Paths for individual agents can be correct and still produce visible bugs when multiple agents share a corridor. Two enemies squeeze through a doorway and block each other. A boss tries to walk through its own minions. Standard A* does not model other agents as obstacles, so this is solved at the steering layer with RVO (Reciprocal Velocity Obstacles) or similar.
When debugging “my agent found a path but does not move,” check whether the agent’s steering is being canceled by collision avoidance. A common failure: two agents stand face to face, each yielding to the other, neither moving. This is a livelock. Add a random jitter to yield priority so one agent backs down first.
Reproducing the Bug
When a QA report says “enemy got stuck here,” you need the full state to reproduce: agent type, agent radius, start position, goal position, area mask, navmesh hash, and any dynamic carving state. Package all of this into a pathfinding bug template so QA can reproduce with one button press. Store the last 10 failed queries per session so even intermittent issues get captured.
“We spent two weeks on a bug where one specific enemy type couldn’t path across a bridge. The region visualizer showed the bridge and the far shore were the same region. The area mask was the issue: the enemy’s profile excluded the ‘wooden’ area type the bridge was painted with. Five minutes to fix, two weeks to find without the right visualizer.”
Related Issues
For related AI debugging, see how to debug AI behavior tree infinite loops. For performance considerations when pathfinding gets expensive, read how to test game performance regression in CI.
Turn on the navmesh visualizer this week. Pick any level and look for gaps, orphaned off-mesh links, and area-type overrides. You will find bugs no one has filed yet.