Godot NavigationServer2D: 2D Navigation Mesh & Paths | Vav LabsSkip to main contentGet in touch
Back to blogInteractive demo and data<br>Try the interactive playground<br>PathForge navigation diagnostics playground<br>Open the existing diagnostics playground on a region-bake-missing case. It's an interactive illustration of the concepts, not the Godot engine running and not a native NavigationServer2D benchmark.<br>Open diagnostics playground<br>Godot NavigationServer2D diagnostics scene<br>Runnable Godot 4.6 development diagnostics scene covering NavigationServer2D empty-path cases including REGION_BAKE_MISSING and UNSYNCED_NAVIGATION_MAP.<br>Download Godot scene
The answer, up front<br>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.<br>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.<br>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<br>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.<br>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.<br>PartWhat it ownsCommon mistakeNavigationServer2DMaps, queries, regions, links, obstacles, avoidance dataExpecting immediate same-frame query results after every changeNavigationRegion2DA region with a NavigationPolygonTreating visible art as navigation dataNavigationPolygonThe 2D navigation mesh resourceForgetting to bake or assign itNavigationAgent2DPath following and avoidance helper for one moving objectDebugging movement before checking whether the map query worksNavigationObstacle2DBake-time mesh influence via affect_navigation_mesh or runtime avoidance via avoidance_enabledAssuming avoidance-only setup changes global pathsNavigationLink2DA deliberate connection between positions on regionsUsing it as a generic fix for broken mesh setup
NavigationServer2D vs NavigationAgent2D<br>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.<br>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.<br>@onready var agent: NavigationAgent2D = $NavigationAgent2D
func _ready() -> void:<br>agent.target_position = target.global_position
func _physics_process(delta: float) -> void:<br>if agent.is_navigation_finished():<br>return
var next_position := agent.get_next_path_position()<br>var direction := global_position.direction_to(next_position)<br>global_position += direction * 120.0 * delta<br>Setting up a 2D navigation mesh<br>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.<br>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.<br>@onready var region: NavigationRegion2D = $NavigationRegion2D
func rebake_then_query(start_position: Vector2, target_position: Vector2) -> PackedVector2Array:<br>region.bake_navigation_polygon()
if region.is_baking():<br>await region.bake_finished
await get_tree().physics_frame
return NavigationServer2D.map_get_path(<br>get_world_2d().get_navigation_map(),<br>start_position,<br>target_position,<br>true,<br>The navmesh-not-baked empty path<br>This is the NavigationServer2D version of forgetting...