Godot pathfinding / 2026-06-23 / 13 min read
Verified as of Godot 4.7 stable docs on 2026-06-23
NavigationServer2D in Godot: the complete guide
NavigationServer2D guide for Godot 4: 2D navigation mesh setup, direct path queries, map sync, obstacles, links, and when a grid is better.
The answer, up front
NavigationServer2D is Godot's low-level 2D navigation API. It owns navigation maps, regions, links, obstacles, avoidance hooks, and direct path queries. It doesn't turn every runtime blocker into a new global route automatically, and it doesn't make a scene walkable just because the floor art is visible.
For 2D navigation mesh work, the concrete Godot shape is usually NavigationRegion2D plus a NavigationPolygon. Query the map only after the navigation data exists and the server has had a physics frame to synchronize. If the route is tile-shaped, blocker-heavy, or based on movement ranges, AStarGrid2D or a custom grid may be the cleaner model.
The trap worth remembering: NavigationObstacle2D has two different jobs. Bake-time obstacle geometry can affect the baked navigation mesh through affect_navigation_mesh and change future path queries. Runtime avoidance through avoidance_enabled steers agents locally; by itself, it doesn't rewrite the pathfinding graph.
The mental model
Think in layers. The server is the shared navigation system. Regions contribute walkable geometry. Agents consume that data and move. Obstacles affect baking with affect_navigation_mesh or runtime avoidance with avoidance_enabled, depending on how they are configured. Links add deliberate off-mesh connections. Direct path queries ask the map what it currently knows.
Most NavigationServer2D bugs come from mixing those jobs together. A path query can't see geometry that was never baked. An agent can't fix missing map data. Avoidance can keep an agent from colliding locally without proving that the global route is valid.
| Part | What it owns | Common mistake |
|---|---|---|
NavigationServer2D | Maps, queries, regions, links, obstacles, avoidance data | Expecting immediate same-frame query results after every change |
NavigationRegion2D | A region with a NavigationPolygon | Treating visible art as navigation data |
NavigationPolygon | The 2D navigation mesh resource | Forgetting to bake or assign it |
NavigationAgent2D | Path following and avoidance helper for one moving object | Debugging movement before checking whether the map query works |
NavigationObstacle2D | Bake-time mesh influence via affect_navigation_mesh or runtime avoidance via avoidance_enabled | Assuming avoidance-only setup changes global paths |
NavigationLink2D | A deliberate connection between positions on regions | Using it as a generic fix for broken mesh setup |
NavigationServer2D vs NavigationAgent2D
NavigationAgent2D is the convenient node you attach to a moving thing. It asks the navigation system for path positions, reports the next point to move toward, and can participate in avoidance. It isn't the navigation map.
When debugging, split the problem. First ask the map for a path directly. If that fails, the issue is map data, region setup, navigation layers, baking, or synchronization. If the direct query works, then debug the agent's target position, desired distance, velocity, avoidance, and movement code.
@onready var agent: NavigationAgent2D = $NavigationAgent2D
func _ready() -> void:
agent.target_position = target.global_position
func _physics_process(delta: float) -> void:
if agent.is_navigation_finished():
return
var next_position := agent.get_next_path_position()
var direction := global_position.direction_to(next_position)
global_position += direction * 120.0 * deltaSetting up a 2D navigation mesh
In Godot 2D, a navigation mesh is a NavigationPolygon. A NavigationRegion2D owns that polygon and contributes it to the navigation map. The sprite, tile art, or floor mesh you see on screen isn't the navigation mesh by itself.
The practical order is: create or assign the NavigationPolygon, bake if needed, wait until baking and server synchronization have happened, then query. That order is what prevents the classic empty path where the scene looks walkable but the navigation map is still empty.
@onready var region: NavigationRegion2D = $NavigationRegion2D
func rebake_then_query(start_position: Vector2, target_position: Vector2) -> PackedVector2Array:
region.bake_navigation_polygon()
if region.is_baking():
await region.bake_finished
await get_tree().physics_frame
return NavigationServer2D.map_get_path(
get_world_2d().get_navigation_map(),
start_position,
target_position,
true,
1
)The navmesh-not-baked empty path
This is the NavigationServer2D version of forgetting AStarGrid2D.update(). No baked or assigned NavigationPolygon means no walkable navigation data, even if the visible scene looks fine.
The triage is simple: confirm the region has a valid navigation polygon, confirm the query points are inside the walkable area, confirm layers match, then query after the sync point. If the direct query returns an empty path, don't start by tuning the agent.
For the broader checklist, see why your Godot path is failing.
Direct path queries
A direct query is the fastest way to separate map problems from movement problems. The short API is NavigationServer2D.map_get_path(). It returns a PackedVector2Array of points in world space.
Use query_path() with NavigationPathQueryParameters2D and NavigationPathQueryResult2D when you want reusable objects, metadata, or a closer match to the low-level server API.
func query_path_simple(start_position: Vector2, target_position: Vector2) -> PackedVector2Array:
return NavigationServer2D.map_get_path(
get_world_2d().get_navigation_map(),
start_position,
target_position,
true,
1
)
var query_parameters := NavigationPathQueryParameters2D.new()
var query_result := NavigationPathQueryResult2D.new()
func query_path_with_result(start_position: Vector2, target_position: Vector2) -> PackedVector2Array:
query_parameters.map = get_world_2d().get_navigation_map()
query_parameters.start_position = start_position
query_parameters.target_position = target_position
query_parameters.navigation_layers = 1
query_result.reset()
NavigationServer2D.query_path(query_parameters, query_result)
return query_result.pathMap sync and timing
NavigationServer changes aren't guaranteed to be query-ready the instant your script changes a region, obstacle, or map. The official NavigationServer docs describe synchronization at the end of the physics frame. That means a query in _ready() or immediately after changing navigation data can see an empty or stale map.
The normal fix isn't to force the server every time. Query after the correct sync point. The stable docs expose map_force_update(map), but they mark it deprecated and warn that it is incompatible with asynchronous updates. Treat it as diagnostic context or a last-resort tool, not the default production pattern.
func _ready() -> void:
_query_after_navigation_sync.call_deferred()
func _query_after_navigation_sync() -> void:
await get_tree().physics_frame
var path := NavigationServer2D.map_get_path(
get_world_2d().get_navigation_map(),
start_position,
target_position,
true,
1
)
print(path.size())The obstacle trap
The sentence "NavigationObstacle2D doesn't change paths" is too broad. The accurate version is narrower and much more useful: a NavigationObstacle2D can affect pathfinding when affect_navigation_mesh lets it contribute obstacle geometry to a baked navigation mesh. Runtime avoidance_enabled behavior is local steering and doesn't rewrite global path queries by itself.
That distinction is why tower-defense blockers, doors, and destructible terrain are easy to get wrong. If the map still says the area is walkable, a direct path query can still cross it. The agent may try to steer around a local obstacle, but that isn't the same thing as invalidating the route.
Bake-time obstacle mode can change the navigation mesh, but it belongs to the bake/update workflow. Avoidance mode is cheaper and runtime-friendly, but it is a steering layer. Pick the one you actually mean.
| Obstacle use | Changes global path query? | Use it for |
|---|---|---|
Bake-time obstacle geometry with affect_navigation_mesh | Yes, after baking the mesh | Static holes, walls, or source geometry exclusions |
Runtime avoidance with avoidance_enabled, radius, and velocity | No, not by itself | Local steering around agents or moving soft obstacles |
| Static avoidance outline | No global route rewrite | Hard local avoidance boundaries that are not moved every frame |
| Tower-defense blocker that must invalidate a route | Only if navigation data represents the block | Grid/dirty-region logic or explicit navmesh update workflow |
Links, gaps, and shortcuts
NavigationLink2D is a deliberate connection between two positions on navigation regions. Use it for jumps, doors, bridges, ladders, portals, ferries, or any off-mesh connection where geometry alone is not the whole rule.
A link shouldn't be the first patch for a broken mesh. If the mesh is disconnected because the region is missing, not baked, on the wrong layer, or not synchronized yet, fix that first. Then add links for real design shortcuts.
Performance and rebuild cost
NavigationServer2D is a good fit when the world is mostly region/navmesh shaped and mostly stable. It isn't a magic dynamic-grid system. Constantly rebuilding navigation data for every blocker can move the cost from path following into map maintenance.
For dynamic tile blockers, tactical movement ranges, cell weights, and large numbers of similar units, a grid or dirty-region model is often simpler to reason about. If the frame-time problem is repeated path queries, read the 10,000-agent Godot benchmark and the frame-spike scheduling guide.
NavigationServer2D or AStarGrid2D?
Pick by world shape and game rules. The wrong choice is usually obvious only after the game has enough special cases, so make the decision explicit early.
| Situation | Prefer | Why |
|---|---|---|
| Tile tactics, roguelike, tower defense grid | AStarGrid2D or custom grid | Cells, blockers, weights, movement range, and dirty updates are first-class rules |
| Irregular walkable floor or hand-authored regions | NavigationServer2D | Region/navmesh model fits the world |
| Dynamic tower blockers | Grid/dirty-region model | Avoidance-only obstacles do not rewrite routes; rebaking every blocker can be the wrong cost |
| Steering around moving agents | NavigationAgent2D + avoidance | Local motion problem, not global route topology |
| Need to validate the current map | NavigationServer2D.map_get_path() | Ask the map directly before debugging movement |
Implementation checklist
Use this when a 2D navigation scene fails and every object looks like it should work.
- Decide whether the game is region/navmesh-based or grid-based.
- Create or assign the
NavigationPolygonon aNavigationRegion2D. - Bake the region if source geometry is involved.
- Wait for baking and server synchronization before the first query.
- Query the map directly with
NavigationServer2D.map_get_path(). - Only debug
NavigationAgent2Dmovement after the direct query works. - Choose the obstacle mode intentionally:
affect_navigation_meshfor bake-time mesh influence oravoidance_enabledfor runtime avoidance. - Use
NavigationLink2Dfor deliberate off-mesh connections, not as a generic repair tool.
Where PathForge fits
PathForge is being built around the production side of Godot pathfinding: diagnostics, blockers, clearance, repeated-query scheduling, and visual proof. NavigationServer2D still matters for navmesh-style games. The point is choosing the model that matches the game, not forcing every movement problem through one API.
The current playground link below opens an existing diagnostic case. A dedicated NavigationServer2D guide mode can become the stronger visual companion later: region, bake, avoidance, link, and sync toggles in one surface.
Download the Godot 4.6 diagnostics scene if you want the runnable project behind the shared failure-mode examples.
Measurement-gap: this guide doesn't publish a native NavigationServer2D benchmark dataset. It's an API/reference guide backed by official Godot docs, the existing runnable diagnostics scene, and a browser illustration of the failure mode.
Frequently asked questions
What is NavigationServer2D in Godot?
NavigationServer2D is Godot's low-level 2D navigation API. It manages navigation maps, regions, links, obstacles, avoidance data, and direct path queries.
Is NavigationServer2D the same as NavigationAgent2D?
No. NavigationServer2D is the lower-level navigation system. NavigationAgent2D is a node-level helper for a moving object that consumes navigation data, reports the next path position, and can use avoidance.
What is a 2D navigation mesh in Godot?
In Godot 2D, the navigation mesh resource is NavigationPolygon. A NavigationRegion2D owns a NavigationPolygon and contributes that walkable data to the navigation map.
Why does NavigationServer2D return an empty path?
Common causes are no baked or assigned NavigationPolygon, query points outside the walkable mesh, navigation layer mismatch, disconnected regions, or querying before the NavigationServer has synchronized changes at the end of the physics frame.
Does NavigationObstacle2D change the path?
Sometimes. Bake-time obstacle geometry can affect the baked navigation mesh through affect_navigation_mesh and therefore future path queries. Runtime avoidance_enabled behavior steers agents locally but doesn't rewrite global path queries by itself.
Should I use NavigationServer2D or AStarGrid2D for a tile game?
Use AStarGrid2D or a custom grid when the game is fundamentally tile-based: cells, blockers, weights, movement ranges, and dirty-region updates. Use NavigationServer2D when the world is better represented as navigation mesh regions.