Godot pathfinding / 2026-07-01 / 9 min read
Verified as of Godot 4.7 stable docs checked on 2026-07-01; artifact tested in Godot 4.7-stable
NavigationObstacle Avoidance vs Pathfinding in Godot
NavigationObstacle avoidance steers Godot agents locally. It does not make path queries reroute by itself. Use an explicit route query to prove what changed.
The short answer
NavigationObstacle2D avoidance does not make a Godot path query reroute by itself.
It can make agents steer around an obstacle locally, but local steering is not the same as changing the global route. If a wall, tower, closed gate, or destroyed bridge should change where a path may exist, update the navigation data that the path query reads. Then prove the route changed with an explicit spawn-to-goal query.
The practical rule is simple: avoidance changed means look at velocity; path changed means look at a route query.
The same boundary applies in 3D. NavigationObstacle3D and NavigationAgent3D use Vector3 and 3D navigation maps, but avoidance is still a safe-velocity system and pathfinding is still a route-query system.
The demo for this article makes that distinction visible. A Field Pylon makes enemies bend around it, but the chip reads PATH UNCHANGED. A Wall Tower changes the route data, and the chip reads PATH CHANGED. A sealing placement is rejected with REJECTED WOULD SEAL.
Pathfinding and avoidance are different systems
Pathfinding answers: what route exists through the current navigation data? Avoidance answers: given the velocity I wanted this frame, what velocity is safer around nearby agents and avoidance obstacles?
Those two answers can disagree visually. If an enemy follows a route down the middle of a lane but avoidance pushes it around a pylon, the enemy's visible movement changed. The route did not necessarily change.
Most bugs come from treating the avoidance layer as if it had edited the route layer. In a running game, players do not see route query and safe velocity. They see a unit go around a thing and reach the goal. That only proves movement changed; it does not prove that the path query changed.
| System | Reads | Returns | Use it for |
|---|---|---|---|
| Pathfinding | Navigation map, regions, grid, links, blockers | A path or no path | Global route decisions |
| Avoidance | Nearby agents, avoidance obstacles, desired velocity | A safer velocity | Local steering and crowd motion |
The fastest debug test: turn avoidance off
If you do not know whether your bug is pathfinding or avoidance, run one test with avoidance disabled.
If the bad behavior remains when avoidance_enabled = false, debug route data: map sync, regions, grid solids, blockers, links, layers, and the direct route query. If the behavior disappears, debug avoidance: velocity assignment, velocity_computed, radius or vertices, layers and masks, neighbor settings, and your movement code.
This is not a shipping recommendation. It is a subsystem isolation test. Turn off local steering once so you can see whether the route itself was wrong.
The route query is the source of truth
When you need to know whether the route changed, ask the pathfinding system directly. Do not infer path status from whether a unit reached the goal, curved around an obstacle, left a longer visible trail, or spread out near a bottleneck. Those are movement observations, and movement can be caused by avoidance.
Use an explicit route query instead. In the demo, one route-query primitive powers the status chip, the route length delta, and the rejected sealing placement. That is deliberate. If three different pieces of code decide path status three different ways, they eventually drift.
When using NavigationServer-backed map, region, or procedural navigation changes, wait for synchronization before classifying the result. Otherwise you can compare against stale navigation data.
2D and 3D names
The article demo is 2D, but the boundary is the same in a 3D game. The vector type and node suffix change; the diagnostic does not.
| Concept | 2D | 3D |
|---|---|---|
| Agent node | NavigationAgent2D | NavigationAgent3D |
| Obstacle node | NavigationObstacle2D | NavigationObstacle3D |
| Direct route query | NavigationServer2D.map_get_path() | NavigationServer3D.map_get_path() |
| Avoidance signal | velocity_computed(safe_velocity: Vector2) | velocity_computed(safe_velocity: Vector3) |
| Diagnostic switch | agent.avoidance_enabled = false | agent.avoidance_enabled = false |
What the demo shows
The playable artifact is a small tower-defense navigation lab. It has a zig-zag lane, enemy waves, a base, two tower types, and a status chip that reports what the path query sees.
Clean resets the board to the plain zig-zag path. Pylon places a field pylon. It has a range ring, fires at enemies, and changes local movement, but it does not change the route contract. Enemies bend around it while the chip still reads PATH UNCHANGED.
Reroute places a wall tower. It is also a weapon, but it changes the walkable representation used by the route query. The current build reports about PATH CHANGED +130px, enough for the bend to be visible instead of only numeric.
Rejected stages a sealing placement. The game shows the rejected attempt and the chip reads REJECTED WOULD SEAL.
The attached scenario receipt records the same contract in JSON: Clean 1171.27px, Pylon 0.00px route delta, Reroute +129.73px, and Rejected candidate_rejected_by_runtime = true.
The full tower-defense placement transaction belongs in Stop Players from Blocking the Path in a Godot Tower Defense. This article only needs the contrast: a blocker changes route data; an avoidance obstacle changes local movement.
Static obstacles, dynamic obstacles, and jams
Avoidance is not a hard maze solver. Godot's navigation obstacle docs distinguish static and dynamic avoidance cases. Stationary avoidance obstacles are represented with vertices. Dynamic avoidance obstacles use radius and velocity. The dynamic case is useful for things that move, but it is not a reliable way to enforce hard corridors in crowded or narrow spaces.
This matters in games because avoidance can look correct in an empty test and fail under crowd pressure. A single unit may steer around a pylon. A wave of units may bunch, jitter, or squeeze badly if you are using local avoidance as if it were a route constraint.
Avoidance also has per-agent cost because registered agents participate in the RVO-style avoidance simulation. Enable it where the agent actually needs local steering; for crowd-scale pathfinding work, see the Godot 10,000-agent pathfinding benchmark.
If the design rule is you cannot pass here, do not rely on avoidance. Change the route data. If the design rule is try not to collide while passing nearby, avoidance is the right tool.
What went wrong in the demo build
The artifact had a real bug while the zig-zag version was being built. Some enemies looked like they were stuck on pylons for a long time, and a few could be pushed outside the visible board when I added many obstacles.
The root cause was not that Godot avoidance had secretly become pathfinding. The route query, the movement clamp, and the corner-progress code did not describe exactly the same corridor at the zig-zag turns. The query still had a connected route, but movement recovery was trying to keep units inside a slightly different shape. That made local avoidance look like a pathfinding failure.
The fix was to make the route probe, placement validation, unit clamp, and progress logic share the same corridor primitive, then record a small motion receipt. In the current receipt, the pylon scenario keeps the route unchanged, spawns 24 active units in the 5-second probe, records escaped_units = 0, and caps stall recovery at 0.14s.
Grid games have the same boundary
This article uses NavigationServer2D language because NavigationObstacle2D and NavigationAgent2D live in that navigation family. The boundary also exists in grid games.
For a tile tower defense, a wall tower is usually a grid occupancy change: astar_grid.set_point_solid(cell, true). That is a pathfinding change. The route query should see a different graph.
The source of the solid rule might come from a TileSet custom data layer, covered in TileMapLayer Custom Data for Walkability in Godot. The grid API details belong in AStarGrid2D in Godot 4: Complete Reference.
The concept is the same: route blocker means edit the route data; avoidance obstacle means edit local steering.
Common failure modes
My agent goes around the obstacle, so the path must have changed. Not necessarily. That can be avoidance. Query the path before and after placement to prove route change.
My NavigationObstacle2D does not block the path. If it is only configured for runtime avoidance, that is expected. Avoidance influences safe velocity. It does not make the route query treat the obstacle as a wall.
My crowd jams near an avoidance obstacle. You may be using avoidance as a hard route constraint. Local avoidance can steer around nearby objects, but it is not a guarantee that a crowd can pass a narrow space.
The path query says unchanged, but the visible route looks different. Check whether your drawing code is rendering actual query points or the enemy's recent movement trail. A movement trail shows where an agent moved. A route line should show what the path query returned.
The query changed, but the agent still follows the old path. The agent or movement controller may be following cached route points. Update the route cache after the path query changes, and make sure NavigationServer changes have synchronized before you compare results.
Implementation checklist
Use this checklist when deciding whether an object should be a path blocker or an avoidance obstacle.
- If the object changes legal route topology, represent it in the navigation data.
- Wait for server sync when using NavigationServer-backed changes.
- Query the required route explicitly.
- Reject or revert if the route contract fails.
- Update the displayed route from the query result.
- If the object only changes local movement, enable avoidance on the agents that need it.
- Set desired velocity in the physics loop and move with
safe_velocity. - Do not claim the path changed unless a route query changed.
- Draw the path query result separately from unit trails.
- Check whether movement clamping describes the same world as the path query.
Practical rule
Use a route query for path truth. Use avoidance velocity for movement truth.
If you remember only one sentence, make it this: a NavigationObstacle2D used for avoidance can change where the agent moves this frame, but it does not prove the route changed.
Frequently asked questions
Does NavigationObstacle2D make NavigationAgent2D reroute?
Not by runtime avoidance alone. Avoidance changes the safe velocity used for local movement. A path query reroutes only when the navigation data queried by the pathfinder changes.
Why does my Godot agent move around a NavigationObstacle if the path did not change?
Because local avoidance can bend the agent's frame-by-frame velocity. The global route can stay the same while the visible movement goes around a nearby obstacle.
Why is my Godot NavigationAgent not avoiding, or why is avoidance not working?
First isolate the system: set avoidance_enabled to false for one run. If the behavior stays, debug pathfinding data. If it changes, check that the agent has avoidance_enabled true, a navigation map, velocity assigned every physics update, a velocity_computed connection that actually moves with safe_velocity, matching avoidance_layers and avoidance_mask, and a usable obstacle radius or vertices.
How do I make a NavigationObstacle block the path?
Do not rely on avoidance for hard route blockage. Represent the blocker in the navigation data used by your route query, wait for sync if needed, then query the route again. Depending on your project, that may be a grid solid cell, a region update, a navmesh bake/update, or another navigation-data change.
Does this also apply to NavigationObstacle3D and NavigationAgent3D?
Yes. In 3D, velocity_computed returns a Vector3 safe velocity and route queries use NavigationServer3D, but the boundary is the same: avoidance changes local velocity, while pathfinding changes only when the navigation data used by the route query changes.
Should a tower defense wall be a NavigationObstacle2D?
Only if the wall is meant to influence local steering. If the wall must block or reshape the legal route, represent it as pathfinding data and validate the spawn-to-goal route.
What is the difference between pathfinding and avoidance in Godot?
Pathfinding returns a route through navigation data. Avoidance returns a safer velocity for local movement around nearby agents and obstacles. Pathfinding decides where a valid route can exist; avoidance decides how an agent should move right now.
Why do avoidance obstacles cause jams in narrow corridors?
Avoidance is a local velocity solver, not a guarantee of route capacity. Dynamic avoidance obstacles are especially weak as hard constraints in crowded or narrow areas. If the corridor must be blocked or opened by game rules, change the route data instead.
How do I prove a path changed in Godot?
Run the same route query before and after the change and compare the result. Do not infer route changes from visible movement alone.