Godot pathfinding / 2026-06-23 / 18 min read
Updated 2026-07-30 · Verified as of Godot 4.7.1 stable, checked 2026-07-30
NavigationServer2D in Godot 4: setup, path queries, and debugging
Set up NavigationServer2D in Godot 4, validate reachable paths, connect regions and links, handle sync and rebakes, and diagnose empty or partial paths.

Start here: symptom to cause to fix
Start with the symptom you can reproduce, then jump to the check that produces an observable verdict. Run the direct checked query before changing NavigationAgent2D movement settings; otherwise path-data failures and movement failures look identical.
A useful query has three verdicts: unsynced or empty, partial, and reached. A non-empty path is not automatically a success because its final point can stop on the closest reachable part of the start island.
Observable tell: every router destination ends in a status, count, iteration, metadata value, or debug-render feature that can confirm or reject the suspected cause.
| What you see | Most likely cause | Go here |
|---|---|---|
| Empty path on the first frame | The map has not synchronized, or the region contributes no usable polygon | Check map synchronization |
| Path has points but ends before the target | The target is disconnected, excluded by layers, or outside the reachable island | Run the checked query |
| Regions overlap but the path stops at the seam | Their polygons do not have compatible connected edges | Inspect region connections |
| A door, swim area, or special route is ignored | The query mask, region layers, or route costs do not match the intended rule | Check layers and costs |
| The path uses a link but the actor stops there | Pathfinding found the link, but gameplay code did not perform the traversal | Handle the link segment |
| An avoidance obstacle does not reroute the path | Avoidance changed local steering, not the global navigation mesh | Separate avoidance from rebaking |
| The route is stale after a runtime rebake | The bake finished but the updated map iteration is not query-ready | Wait for a new iteration |
| Large request bursts spike the frame | Too many searches, fragmented polygons, or repeated unreachable targets | Apply the performance guardrails |
Get one direct query working
The smallest scene needs a NavigationRegion2D with an assigned NavigationPolygon, plus start and target positions in global coordinates. If the polygon uses source geometry, bake it. If it is drawn or built manually, assign its vertices and polygon indices. In both cases the region must join the same navigation map that you query.
Make the first setup call deferred, wait for a physics frame, and confirm that map_get_iteration_id(map) is greater than zero. Then call the pasteable checked-query helper in the next section instead of treating any non-empty array as success.
Observable tell: the map iteration is greater than zero and the helper prints exactly one status=... verdict. If the iteration remains zero, do not tune the agent yet.
Paste this first: a checked path query
Paste this function into a Node2D that belongs to the world you are querying, then call it with global start and target positions. It prints one console verdict and returns the same evidence as a dictionary for your game or debug UI.
The last returned point is the reachability test. start_snap exposes a start far outside the mesh; target_error separates a reached target from a partial path that merely ended as close as the connected map allowed.
Observable tell: the console prints status=unsynced, empty, partial, or reached, followed by the reason and the values needed for the next check.
The mental model
Think in layers. A navigation map is an isolated navigation world identified by an RID. Regions contribute NavigationPolygon geometry to a map. Direct queries read the synchronized map. Links add deliberate graph connections. Agents follow paths and optionally participate in avoidance. Obstacles affect future baking or local avoidance depending on how they are configured.
Those jobs are independent. Visible art and physics collision are not automatically navigation data. Avoidance does not prove that the global route is valid. A region can exist on the server while contributing no polygons. Two valid regions can still be disconnected.
Scene-authored games can use NavigationRegion2D, NavigationLink2D, and NavigationAgent2D nodes. Use the server RID API directly for procedural regions, additional maps, reusable query objects, or custom resource lifetimes. Any RID you create with the server must eventually be released with NavigationServer2D.free_rid().
Observable tell: if the direct query reports reached but the actor still does not move, the map and route are no longer the first suspect; inspect the agent-following and body-movement layer.
| Part | Owns | Does not do |
|---|---|---|
NavigationServer2D | Maps, regions, queries, links, obstacles, and avoidance data | Move a game body for you |
NavigationRegion2D | One region and its NavigationPolygon | Turn visible floor art into walkable data automatically |
NavigationPathQueryParameters2D | Layer mask, metadata, post-processing, region filters, and search limits | Synchronize pending map changes |
NavigationAgent2D | Path-following state and optional avoidance for one actor | Repair missing or disconnected map geometry |
NavigationLink2D | A route connection between two navmesh positions | Play the jump, teleport, ladder, or door interaction |
Map synchronization and rebake timing
NavigationServer setters are queued. Scene nodes and scripts can change maps, regions, links, and agents during a frame, but path queries see the synchronized server state. For initial scene setup, a deferred function followed by the next physics frame is the normal pattern.
For a runtime rebake, wait for both operations: the background bake must finish, then the navigation map must synchronize the updated polygon. Capture the map iteration after bake_finished and wait for the matching map_changed signal until that iteration changes.
Do not treat one map_changed emission by itself as proof that query data is ready. Confirmed issue #112652 demonstrates the first-emission trap in tested 4.5+ NavigationServer3D builds. The defensive 2D pattern here therefore does not rely on the signal alone. Keep the map_get_iteration_id() > 0 gate for initial setup. After a rebake, require the iteration to advance beyond the value captured after baking.
Do not build production flow around map_force_update(). Since Godot 4.4 moved map synchronization to an asynchronous process, it no longer reliably forces a synchronous update. Issue #104671 documents a freshly created NavigationServer3D map remaining unsynchronized after the call. Prefer map_get_iteration_id() together with map_changed.
Observable tell: the rebake is query-ready only when the map iteration is greater than the value captured after bake_finished. A map_changed signal with the same iteration is not the finish line.
Paste this second: inspect region connections
Use this helper after the checked query reports partial and the final point stops at a region seam. It prints the connection count and every pathway door registered for that region in the synchronized map.
Overlapping regions are not automatically connected. Their navigation polygons need compatible shared edges, or nearly parallel edges close enough for the map's edge_connection_margin. Partial overlap and small floating-point differences are common reasons for a path that stops at a region boundary.
Turn on Visible Navigation and edge-connection debug rendering, then inspect the server instead of judging the artwork. region_get_connections_count() reports how many connections a synchronized region has, and the pathway start/end getters expose each connection door.
Use NavigationLink2D when the gap is intentional. Do not use a link to hide a missing polygon, wrong transform, mismatched layer, or broken edge.
Observable tell: connections=0 together with no edge-connection line in the debug render confirms that the server did not join that seam.
Use query objects when the result needs evidence
map_get_path() is the shortest API. Use NavigationPathQueryParameters2D and NavigationPathQueryResult2D when you need path length, region or link metadata, raw corridor inspection, simplification, search limits, or included/excluded region filters.
Create the parameter and result objects once and reuse them. Metadata is enabled by default; disable flags you do not consume when query volume matters. Raw-corridor output is useful for debugging, while corridor-funnel output is normally the better movement path.
Observable tell: path_types, path_rids, and path_owner_ids line up with the returned path points and identify the exact region or link used by the suspect segment.
| Option | Use it when | Trade-off |
|---|---|---|
PATH_POSTPROCESSING_CORRIDORFUNNEL | Actors move freely inside irregular polygons | Shortest movement path, but shaped by the polygon corridor |
PATH_POSTPROCESSING_NONE | You need to see the raw selected corridor | Diagnostic output, not usually the movement path |
simplify_path | Too many minor points cause steering jitter | Additional query-time processing |
included_regions / excluded_regions | A large map is partitioned into known region chunks | The caller must maintain correct RID filters |
path_search_max_polygons | Unreachable searches need a bounded cost | A low limit can return a poor partial path |
Obstacles and runtime rebaking
A NavigationObstacle2D has two separate jobs. With affect_navigation_mesh, its geometry removes walkable area the next time a navigation polygon is baked. With avoidance_enabled, it participates in local steering. Avoidance alone does not change a direct path query.
For runtime baking, source-geometry parsing and polygon baking are different costs. Parsing the SceneTree must happen on the main thread. The bake can run on a background thread. Reuse NavigationMeshSourceGeometryData2D when the same parsed geometry feeds multiple bakes, and prefer simple collision shapes over detailed visual geometry.
Large dynamic worlds should update bounded region chunks instead of rebaking one giant polygon. Align chunk baking rectangles and borders so neighboring region edges can connect consistently.
Observable tell: enabling avoidance changes the agent's safe velocity while the direct path stays identical. The path changes only after obstruction geometry participates in a bake and the map publishes a newer iteration.
| Mechanism | Changes future path queries? | Runtime note |
|---|---|---|
affect_navigation_mesh | Yes, after another bake and map sync | Removes geometry; it does not trigger the rebake itself |
| Static avoidance vertices | No | Hard local avoidance boundary; moving it requires a rebuild |
| Dynamic avoidance radius and velocity | No | Cheap to move and predictable, but unreliable as a hard blocker in narrow crowds |
| Region or polygon update | Yes, after map sync | Use bounded chunks and async baking where possible |
Performance guardrails
Path-search cost follows polygon and edge count, not the visual size of the world. A large clean polygon map can be cheaper than a small TileMap-derived navmesh fragmented into thousands of tiny polygons.
Unreachable targets are a worst case because the search may explore every connected polygon before proving that the target cannot be reached. Do not run a separate reachability query before the real path query; inspect the last returned point once. Use search limits only when a bounded partial answer is acceptable.
Avoid resetting every agent target each frame, spread large request bursts across frames, and disable metadata you do not consume. Measure map_get_path() or query_path() call sites directly—for example, with Time.get_ticks_usec() around representative queries—and correlate those measurements with frame time. Use the Editor's Navigation Process counter for map updates and avoidance work, not path-query cost. If repeated path requests are the problem, continue with the pathfinding frame-spike guide.
Measurement-gap note: the downloadable proof for this guide verifies query correctness, map synchronization, region connections, and link metadata; it is not a performance benchmark. Profile your own polygon count, query mix, and target hardware before choosing a path-request budget.
Observable tell: custom timings around the query caller rise with request bursts or repeated unreachable targets, then fall when the same requests are budgeted across frames. A flat Navigation Process monitor does not rule out query cost.
Implementation checklist
Use this order for a new scene and for a path that is empty or ends early.
Observable tell: the same reproducible query now prints reached, its final point is inside tolerance, and any remaining failure occurs in movement or special traversal rather than map data.
- Decide whether the game is navmesh-shaped or grid-shaped.
- Assign and, when source geometry is involved, bake a
NavigationPolygonon aNavigationRegion2D. - Wait for NavigationServer synchronization and confirm the map iteration is greater than zero.
- Query the map directly with
NavigationServer2D.map_get_path(). - Classify the result as empty, partial, or reached by comparing the returned endpoints with the requested positions.
- If the path ends early, inspect region connections and navigation layers.
- If the path crosses a
NavigationLink2D, hand that segment to gameplay code. - Only debug
NavigationAgent2Dmovement after the direct path verdict is correct.
Frequently asked questions
Why does NavigationServer2D return a path that does not reach the target?
Godot snaps the query endpoints to navigation mesh positions. If the requested target is outside the connected start island, the path can end at the closest reachable point. Compare the final path position with the requested target and apply an explicit gameplay tolerance.
How do I know when a NavigationServer2D map has synchronized?
NavigationServer2D.map_get_iteration_id(map) returns 0 before the map has ever synchronized. For initial setup, use a deferred function and wait for a physics frame. After runtime changes, watch the map_changed signal and confirm that the iteration ID increased.
Why do two overlapping NavigationRegion2D nodes not connect?
Overlap alone is not enough. Their polygons need compatible shared edges or nearly parallel edges within the map's edge_connection_margin. Use navigation edge debug rendering and region_get_connections_count() to verify the connection.
Does NavigationLink2D move or teleport the actor?
No. It adds a connection to pathfinding. Use path metadata to detect the link segment, then let gameplay code perform the jump, ladder, door interaction, platform ride, or teleport.
Does NavigationObstacle2D make path queries reroute?
Only after affect_navigation_mesh contributes obstruction geometry to a new bake and the map synchronizes. Runtime avoidance_enabled steers avoidance agents locally but does not rewrite the global path by itself.
Should a tile-based game use NavigationServer2D or AStarGrid2D?
Use AStarGrid2D or a custom grid when cells, blockers, weights, movement ranges, and dirty updates define the rules. Use NavigationServer2D when the world is better represented as continuous navigation mesh regions.