Claude-skill-registry godot-setup-navigation
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/godot-setup-navigation" ~/.claude/skills/majiayu000-claude-skill-registry-godot-setup-navigation && rm -rf "$T"
skills/data/godot-setup-navigation/SKILL.mdSetup Navigation System
Core Principle
Navigation is a service, not a component. Use NavigationServer for runtime updates, NavigationRegion for static navmesh, and NavigationAgent for pathfinding requests. Avoid embedding navigation logic directly in character controllers.
What This Skill Does
Sets up complete navigation systems:
- NavigationRegion2D/3D - Creates navigation mesh boundaries
- NavigationPolygon - Generates walkable areas from TileMap or geometry
- NavigationAgent2D/3D - Configures pathfinding agents with obstacle avoidance
- RVO Integration - Implements Reciprocal Velocity Obstacles for dynamic avoidance
- Pathfinding Integration - Creates patterns for character movement, AI, and click-to-move
Navigation System Setup
NavigationRegion2D Configuration
Before (Manual Setup):
# No navigation setup - characters move blindly extends CharacterBody2D func _physics_process(delta): var input = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") velocity = input * 200 move_and_slide()
After (Navigation Region):
# navigation_world.gd - Scene root with navigation extends Node2D @onready var navigation_region: NavigationRegion2D = $NavigationRegion2D func _ready(): # Bake navigation mesh after level loads await get_tree().process_frame navigation_region.bake_navigation_polygon()
Generated Scene:
# navigation_world.tscn [gd_scene load_steps=2 format=3] [ext_resource type="Script" path="res://navigation_world.gd" id="1_abc123"] [sub_resource type="NavigationPolygon" id="NavigationPolygon_abc123"] vertices = PackedVector2Array(0, 0, 1024, 0, 1024, 768, 0, 768) polygons = [PackedInt32Array(0, 1, 2, 3)] outlines = [PackedVector2Array(0, 0, 1024, 0, 1024, 768, 0, 768)] [node name="NavigationWorld" type="Node2D"] script = ExtResource("1_abc123") [node name="NavigationRegion2D" type="NavigationRegion2D" parent="."] navigation_polygon = SubResource("NavigationPolygon_abc123")
NavigationRegion3D Configuration
# navigation_world_3d.gd extends Node3D @onready var navigation_region: NavigationRegion3D = $NavigationRegion3D func _ready(): # Bake 3D navigation mesh await get_tree().process_frame navigation_region.bake_navigation_mesh()
Generated Scene:
# navigation_world_3d.tscn [gd_scene load_steps=3 format=3] [ext_resource type="Script" path="res://navigation_world_3d.gd" id="1_abc123"] [sub_resource type="NavigationMesh" id="NavigationMesh_abc123"] vertices = PackedVector3Array(-10, 0, -10, 10, 0, -10, 10, 0, 10, -10, 0, 10) polygons = [PackedInt32Array(0, 1, 2), PackedInt32Array(0, 2, 3)] cell_size = 0.25 cell_height = 0.25 agent_radius = 0.5 agent_height = 2.0 [node name="NavigationWorld3D" type="Node3D"] script = ExtResource("1_abc123") [node name="NavigationRegion3D" type="NavigationRegion3D" parent="."] navigation_mesh = SubResource("NavigationMesh_abc123")
NavigationPolygon Generation
From Geometry (2D):
# Generates NavigationPolygon from StaticBody2D collision shapes func generate_from_collision_shapes(): var navigation_polygon = NavigationPolygon.new() var outline = PackedVector2Array() # Collect all collision polygon points for body in get_tree().get_nodes_in_group("navigation_obstacles"): if body is StaticBody2D: for child in body.get_children(): if child is CollisionPolygon2D: for point in child.polygon: outline.append(body.to_global(point)) # Create navigation mesh avoiding obstacles navigation_polygon.add_outline(outline) navigation_polygon.make_polygons_from_outlines() $NavigationRegion2D.navigation_polygon = navigation_polygon $NavigationRegion2D.bake_navigation_polygon()
Baking Navigation Mesh
Editor-time Baking:
@tool extends EditorScript func _run(): var nav_region = get_scene().find_child("NavigationRegion2D") if nav_region: nav_region.bake_navigation_polygon() print("Navigation mesh baked successfully")
Runtime Baking:
# For dynamic levels or procedural generation func bake_dynamic_navigation(): var navigation_polygon = NavigationPolygon.new() # Define walkable area var walkable_outline = PackedVector2Array([ Vector2(0, 0), Vector2(1024, 0), Vector2(1024, 768), Vector2(0, 768) ]) navigation_polygon.add_outline(walkable_outline) # Cut out obstacles for obstacle in obstacles: var obstacle_outline = PackedVector2Array() for point in obstacle.shape: obstacle_outline.append(obstacle.global_position + point) navigation_polygon.add_outline(obstacle_outline) navigation_polygon.make_polygons_from_outlines() $NavigationRegion2D.navigation_polygon = navigation_polygon
Runtime Navigation Updates
Using NavigationServer:
# For frequent updates without rebaking func update_navigation_obstacle(obstacle: CollisionShape2D, enabled: bool): var map_rid = NavigationServer2D.get_maps()[0] var obstacle_rid = obstacle.get_rid() if enabled: NavigationServer2D.obstacle_set_map(obstacle_rid, map_rid) else: NavigationServer2D.obstacle_set_map(obstacle_rid, RID()) NavigationServer2D.map_force_update(map_rid)
NavigationAgent Setup
2D Agent Configuration
Before (Direct Movement):
# enemy.gd - No pathfinding extends CharacterBody2D @export var target: Node2D @export var speed: float = 100.0 func _physics_process(delta): if target: var direction = (target.global_position - global_position).normalized() velocity = direction * speed move_and_slide()
After (NavigationAgent):
# enemy.gd - With pathfinding extends CharacterBody2D @export var target: Node2D @export var speed: float = 100.0 @onready var nav_agent: NavigationAgent2D = $NavigationAgent2D func _ready(): # Configure agent nav_agent.path_desired_distance = 10.0 nav_agent.target_desired_distance = 10.0 nav_agent.radius = 20.0 nav_agent.max_speed = speed nav_agent.avoidance_enabled = true # Connect signals nav_agent.velocity_computed.connect(_on_velocity_computed) func _physics_process(delta): if not target: return if nav_agent.is_navigation_finished(): velocity = Vector2.ZERO return # Update target position nav_agent.target_position = target.global_position # Get next path position var next_path_position = nav_agent.get_next_path_position() var direction = (next_path_position - global_position).normalized() # Set velocity (navigation server will compute avoidance) nav_agent.velocity = direction * speed func _on_velocity_computed(safe_velocity: Vector2): velocity = safe_velocity move_and_slide()
Generated Scene:
# enemy.tscn [gd_scene load_steps=2 format=3] [ext_resource type="Script" path="res://enemy.gd" id="1_abc123"] [node name="Enemy" type="CharacterBody2D"] script = ExtResource("1_abc123") [node name="CollisionShape2D" type="CollisionShape2D" parent="."] shape = SubResource("CircleShape2D_abc123") [node name="NavigationAgent2D" type="NavigationAgent2D" parent="."] path_desired_distance = 10.0 target_desired_distance = 10.0 radius = 20.0 max_speed = 100.0 avoidance_enabled = true time_horizon = 5.0 path_postprocessing = 1
3D Agent Configuration
# enemy_3d.gd extends CharacterBody3D @export var target: Node3D @export var speed: float = 5.0 @onready var nav_agent: NavigationAgent3D = $NavigationAgent3D func _ready(): nav_agent.path_desired_distance = 1.0 nav_agent.target_desired_distance = 1.0 nav_agent.radius = 0.5 nav_agent.height = 2.0 nav_agent.max_speed = speed nav_agent.avoidance_enabled = true nav_agent.velocity_computed.connect(_on_velocity_computed) func _physics_process(delta): if not target or nav_agent.is_navigation_finished(): velocity.x = 0 velocity.z = 0 return nav_agent.target_position = target.global_position var next_path_position = nav_agent.get_next_path_position() var direction = (next_path_position - global_position).normalized() nav_agent.velocity = direction * speed func _on_velocity_computed(safe_velocity: Vector3): velocity.x = safe_velocity.x velocity.z = safe_velocity.z move_and_slide()
Target Following
Simple Follow:
# follow_target.gd extends CharacterBody2D @export var target: Node2D @export var follow_distance: float = 50.0 @onready var nav_agent: NavigationAgent2D = $NavigationAgent2D func _physics_process(delta): if not target: return var distance_to_target = global_position.distance_to(target.global_position) if distance_to_target > follow_distance: nav_agent.target_position = target.global_position if not nav_agent.is_navigation_finished(): var next_position = nav_agent.get_next_path_position() var direction = (next_position - global_position).normalized() velocity = direction * 100.0 move_and_slide() else: velocity = Vector2.ZERO
Predictive Following:
# Predict where target will be func get_predicted_target_position(look_ahead_time: float = 0.5) -> Vector2: if target is CharacterBody2D: return target.global_position + (target.velocity * look_ahead_time) return target.global_position
Path Requests
Manual Path Query:
# Request path without NavigationAgent func request_path(from: Vector2, to: Vector2) -> PackedVector2Array: var map_rid = NavigationServer2D.get_maps()[0] return NavigationServer2D.map_get_path(map_rid, from, to, true) # Usage var path = request_path(global_position, target_position) for point in path: print("Path point: ", point)
Path Query with Optimization:
func request_optimized_path(from: Vector2, to: Vector2) -> PackedVector2Array: var map_rid = NavigationServer2D.get_maps()[0] # Query path with simplification var path = NavigationServer2D.map_get_path( map_rid, from, to, true # optimize path ) return path
Obstacle Avoidance
Dynamic Obstacles
Obstacle Node Setup:
# dynamic_obstacle.gd extends StaticBody2D @export var obstacle_radius: float = 30.0 @onready var obstacle: NavigationObstacle2D = $NavigationObstacle2D func _ready(): obstacle.radius = obstacle_radius obstacle.velocity = Vector2.ZERO # Connect to avoidance signals obstacle.avoidance_enabled = true func set_avoidance_velocity(velocity: Vector2): obstacle.velocity = velocity
Generated Scene:
# moving_obstacle.tscn [node name="MovingObstacle" type="StaticBody2D"] [node name="CollisionShape2D" type="CollisionShape2D" parent="."] shape = SubResource("CircleShape2D_abc123") [node name="NavigationObstacle2D" type="NavigationObstacle2D" parent="."] radius = 30.0 avoidance_enabled = true
RVO (Reciprocal Velocity Obstacles)
RVO Configuration:
# Configure RVO parameters for realistic avoidance func configure_rvo(agent: NavigationAgent2D): agent.avoidance_enabled = true agent.radius = 20.0 # Agent size agent.neighbor_distance = 100.0 # How far to look for neighbors agent.max_neighbors = 10 # Max agents to consider agent.time_horizon = 2.0 # How far ahead to predict collisions agent.time_horizon_obstacles = 1.0 # How far ahead for static obstacles
RVO with Priority:
# Some agents have right of way func configure_priority_agent(agent: NavigationAgent2D, priority: int): agent.avoidance_enabled = true agent.avoidance_layers = 1 agent.avoidance_mask = 1 # Higher priority agents push lower priority ones # Use time_horizon to control this match priority: 0: agent.time_horizon = 1.0 # Low priority - yields quickly 1: agent.time_horizon = 2.0 # Normal priority 2: agent.time_horizon = 5.0 # High priority - others yield
Navigation Layers
Layer Configuration:
# Define different navigation layers enum NavigationLayers { GROUND = 1, WATER = 2, AIR = 4 } # Configure agent for specific layers func configure_agent_layers(agent: NavigationAgent2D, layers: int): agent.navigation_layers = layers # Configure region for specific layers func configure_region_layers(region: NavigationRegion2D, layers: int): region.navigation_layers = layers
Layer-based Pathfinding:
# Ground units only walk on ground configure_agent_layers($GroundUnit/NavigationAgent2D, NavigationLayers.GROUND) # Flying units can use air paths configure_agent_layers($FlyingUnit/NavigationAgent2D, NavigationLayers.AIR) # Amphibious units can use ground and water configure_agent_layers($AmphibiousUnit/NavigationAgent2D, NavigationLayers.GROUND | NavigationLayers.WATER)
Integration Patterns
Character Movement
State Machine Integration:
# character_controller.gd extends CharacterBody2D enum State { IDLE, MOVING, ATTACKING } @onready var nav_agent: NavigationAgent2D = $NavigationAgent2D var current_state: State = State.IDLE var target_position: Vector2 func set_target(pos: Vector2): target_position = pos nav_agent.target_position = pos current_state = State.MOVING func _physics_process(delta): match current_state: State.IDLE: velocity = Vector2.ZERO State.MOVING: if nav_agent.is_navigation_finished(): current_state = State.IDLE velocity = Vector2.ZERO else: var next_pos = nav_agent.get_next_path_position() var direction = (next_pos - global_position).normalized() nav_agent.velocity = direction * 150.0 State.ATTACKING: # Attack logic here pass if current_state == State.MOVING: move_and_slide() func _on_velocity_computed(safe_velocity: Vector2): velocity = safe_velocity
AI Pathfinding
AI Controller with Navigation:
# ai_controller.gd extends CharacterBody2D @export var patrol_points: Array[Marker2D] @export var detection_range: float = 200.0 @onready var nav_agent: NavigationAgent2D = $NavigationAgent2D @onready var vision: Area2D = $VisionArea enum AIState { PATROL, CHASE, ATTACK, RETURN } var current_state: AIState = AIState.PATROL var current_patrol_index: int = 0 var home_position: Vector2 var target: Node2D func _ready(): home_position = global_position vision.body_entered.connect(_on_body_entered_vision) nav_agent.velocity_computed.connect(_on_velocity_computed) nav_agent.navigation_finished.connect(_on_navigation_finished) func _physics_process(delta): match current_state: AIState.PATROL: process_patrol() AIState.CHASE: process_chase() AIState.ATTACK: process_attack() AIState.RETURN: process_return() func process_patrol(): if nav_agent.is_navigation_finished(): current_patrol_index = (current_patrol_index + 1) % patrol_points.size() nav_agent.target_position = patrol_points[current_patrol_index].global_position var next_pos = nav_agent.get_next_path_position() var direction = (next_pos - global_position).normalized() nav_agent.velocity = direction * 80.0 func process_chase(): if target: nav_agent.target_position = target.global_position var next_pos = nav_agent.get_next_path_position() var direction = (next_pos - global_position).normalized() nav_agent.velocity = direction * 150.0 if global_position.distance_to(target.global_position) > detection_range * 1.5: current_state = AIState.RETURN func process_return(): nav_agent.target_position = home_position if nav_agent.is_navigation_finished(): current_state = AIState.PATROL func _on_body_entered_vision(body): if body.is_in_group("player"): target = body current_state = AIState.CHASE func _on_velocity_computed(safe_velocity: Vector2): velocity = safe_velocity move_and_slide()
Click-to-Move
RTS-style Movement:
# click_to_move.gd extends CharacterBody2D @export var speed: float = 150.0 @onready var nav_agent: NavigationAgent2D = $NavigationAgent2D @onready var selection_indicator: Sprite2D = $SelectionIndicator var is_selected: bool = false func _ready(): nav_agent.velocity_computed.connect(_on_velocity_computed) selection_indicator.visible = false func _unhandled_input(event): if not is_selected: return if event is InputEventMouseButton: if event.button_index == MOUSE_BUTTON_LEFT and event.pressed: var target = get_global_mouse_position() set_movement_target(target) func set_movement_target(target: Vector2): nav_agent.target_position = target func _physics_process(delta): if nav_agent.is_navigation_finished(): velocity = Vector2.ZERO return var next_pos = nav_agent.get_next_path_position() var direction = (next_pos - global_position).normalized() nav_agent.velocity = direction * speed func _on_velocity_computed(safe_velocity: Vector2): velocity = safe_velocity move_and_slide() func select(): is_selected = true selection_indicator.visible = true func deselect(): is_selected = false selection_indicator.visible = false
Selection Manager:
# selection_manager.gd extends Node2D var selected_units: Array[CharacterBody2D] = [] func _unhandled_input(event): if event is InputEventMouseButton: if event.button_index == MOUSE_BUTTON_LEFT and event.pressed: if not Input.is_key_pressed(KEY_SHIFT): deselect_all() var click_pos = get_global_mouse_position() select_at_position(click_pos) elif event.button_index == MOUSE_BUTTON_RIGHT and event.pressed: var target_pos = get_global_mouse_position() move_selected_to(target_pos) func select_at_position(pos: Vector2): var space_state = get_world_2d().direct_space_state var query = PhysicsPointQueryParameters2D.new() query.position = pos query.collide_with_areas = true var results = space_state.intersect_point(query) for result in results: var node = result.collider if node.has_method("select"): node.select() selected_units.append(node) func deselect_all(): for unit in selected_units: if is_instance_valid(unit) and unit.has_method("deselect"): unit.deselect() selected_units.clear() func move_selected_to(target: Vector2): for unit in selected_units: if is_instance_valid(unit) and unit.has_method("set_movement_target"): unit.set_movement_target(target)
Examples
2D Top-Down Navigation
Complete Setup:
# player_topdown.gd extends CharacterBody2D @export var speed: float = 200.0 @onready var nav_agent: NavigationAgent2D = $NavigationAgent2D func _ready(): nav_agent.path_desired_distance = 8.0 nav_agent.target_desired_distance = 8.0 nav_agent.radius = 12.0 nav_agent.max_speed = speed nav_agent.avoidance_enabled = true nav_agent.velocity_computed.connect(_on_velocity_computed) func _input(event): if event is InputEventMouseButton: if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed: nav_agent.target_position = get_global_mouse_position() func _physics_process(delta): if nav_agent.is_navigation_finished(): velocity = Vector2.ZERO return var next_pos = nav_agent.get_next_path_position() var direction = (next_pos - global_position).normalized() nav_agent.velocity = direction * speed func _on_velocity_computed(safe_velocity: Vector2): velocity = safe_velocity move_and_slide()
Scene Structure:
# topdown_level.tscn [gd_scene load_steps=4 format=3] [sub_resource type="NavigationPolygon" id="NavigationPolygon_level"] vertices = PackedVector2Array(0, 0, 1024, 0, 1024, 768, 0, 768) polygons = [PackedInt32Array(0, 1, 2, 3)] [sub_resource type="CircleShape2D" id="CircleShape2D_player"] radius = 12.0 [node name="TopdownLevel" type="Node2D"] [node name="NavigationRegion2D" type="NavigationRegion2D" parent="."] navigation_polygon = SubResource("NavigationPolygon_level") [node name="Player" type="CharacterBody2D" parent="."] position = Vector2(512, 384) [node name="CollisionShape2D" type="CollisionShape2D" parent="Player"] shape = SubResource("CircleShape2D_player") [node name="NavigationAgent2D" type="NavigationAgent2D" parent="Player"] radius = 12.0 max_speed = 200.0 avoidance_enabled = true [node name="Obstacles" type="Node2D" parent="."] [node name="Wall1" type="StaticBody2D" parent="Obstacles"] [node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="Obstacles/Wall1"] polygon = PackedVector2Array(200, 200, 300, 200, 300, 300, 200, 300)
3D Navigation
3D Character Controller:
# player_3d.gd extends CharacterBody3D @export var speed: float = 5.0 @export var jump_velocity: float = 4.5 @onready var nav_agent: NavigationAgent3D = $NavigationAgent3D @onready var camera: Camera3D = $Camera3D var gravity = ProjectSettings.get_setting("physics/3d/default_gravity") func _ready(): nav_agent.path_desired_distance = 0.5 nav_agent.target_desired_distance = 0.5 nav_agent.radius = 0.3 nav_agent.height = 1.8 nav_agent.max_speed = speed nav_agent.avoidance_enabled = true nav_agent.velocity_computed.connect(_on_velocity_computed) func _input(event): if event is InputEventMouseButton: if event.button_index == MOUSE_BUTTON_LEFT and event.pressed: var mouse_pos = event.position var from = camera.project_ray_origin(mouse_pos) var to = from + camera.project_ray_normal(mouse_pos) * 1000 var space_state = get_world_3d().direct_space_state var query = PhysicsRayQueryParameters3D.new() query.from = from query.to = to var result = space_state.intersect_ray(query) if result: nav_agent.target_position = result.position func _physics_process(delta): if not is_on_floor(): velocity.y -= gravity * delta if Input.is_action_just_pressed("ui_accept") and is_on_floor(): velocity.y = jump_velocity if nav_agent.is_navigation_finished(): velocity.x = 0 velocity.z = 0 return var next_pos = nav_agent.get_next_path_position() var direction = (next_pos - global_position).normalized() nav_agent.velocity = direction * speed func _on_velocity_computed(safe_velocity: Vector3): velocity.x = safe_velocity.x velocity.z = safe_velocity.z move_and_slide()
TileMap-Based Navigation
Automatic Navigation Generation:
# tilemap_navigation.gd extends Node2D @onready var tile_map: TileMap = $TileMap @onready var navigation_region: NavigationRegion2D = $NavigationRegion2D func _ready(): generate_navigation_from_tilemap() func generate_navigation_from_tilemap(): var navigation_polygon = NavigationPolygon.new() var used_cells = tile_map.get_used_cells(0) # Layer 0 # Get tileset navigation polygons var tile_set = tile_map.tile_set for cell in used_cells: var atlas_coords = tile_map.get_cell_atlas_coords(0, cell) var tile_data = tile_map.get_cell_tile_data(0, cell) if tile_data and tile_data.get_navigation_polygon(0): var nav_poly = tile_data.get_navigation_polygon(0) var vertices = nav_poly.vertices # Transform vertices to world space var world_vertices = PackedVector2Array() for vertex in vertices: var world_pos = tile_map.map_to_local(cell) + vertex world_vertices.append(world_pos) # Add to navigation polygon navigation_polygon.add_polygon(world_vertices) navigation_region.navigation_polygon = navigation_polygon navigation_region.bake_navigation_polygon()
Using TileMap V2 with Navigation Layers:
# tilemap_v2_navigation.gd extends Node2D @onready var tile_map: TileMapLayer = $TileMapLayer @onready var navigation_region: NavigationRegion2D = $NavigationRegion2D func _ready(): # For Godot 4.3+ TileMapLayer generate_navigation_from_layer() func generate_navigation_from_layer(): var navigation_polygon = NavigationPolygon.new() var used_cells = tile_map.get_used_cells() var tile_set = tile_map.tile_set for cell in used_cells: var tile_data = tile_map.get_cell_tile_data(cell) if tile_data: # Check if tile has navigation var nav_poly = tile_data.get_navigation_polygon() if nav_poly: var vertices = nav_poly.vertices var world_vertices = PackedVector2Array() for vertex in vertices: var world_pos = tile_map.map_to_local(cell) + vertex world_vertices.append(world_pos) navigation_polygon.add_outline(world_vertices) navigation_polygon.make_polygons_from_outlines() navigation_region.navigation_polygon = navigation_polygon
TileMap Integration (from TileMap V2)
Navigation-Enabled TileMap Setup:
# Create a tileset with navigation polygons func create_navigation_tileset() -> TileSet: var tile_set = TileSet.new() tile_set.tile_size = Vector2i(32, 32) # Add terrain atlas source var atlas_source = TileSetAtlasSource.new() atlas_source.texture = preload("res://assets/tiles.png") atlas_source.texture_region_size = Vector2i(32, 32) # Add ground tile with navigation var ground_atlas = Vector2i(0, 0) atlas_source.create_tile(ground_atlas) var ground_data = atlas_source.get_tile_data(ground_atlas, 0) var nav_poly = NavigationPolygon.new() nav_poly.vertices = PackedVector2Array([ Vector2(0, 0), Vector2(32, 0), Vector2(32, 32), Vector2(0, 32) ]) nav_poly.add_polygon(PackedInt32Array([0, 1, 2, 3])) ground_data.set_navigation_polygon(0, nav_poly) # Add wall tile without navigation var wall_atlas = Vector2i(1, 0) atlas_source.create_tile(wall_atlas) # No navigation polygon = unwalkable tile_set.add_source(atlas_source, 0) return tile_set
Runtime TileMap Navigation Update:
# Update navigation when tiles change func update_navigation_at_position(map_pos: Vector2i): # Remove old navigation data var nav_poly = NavigationPolygon.new() # Rebuild from current tilemap state var used_cells = tile_map.get_used_cells(0) for cell in used_cells: var tile_data = tile_map.get_cell_tile_data(0, cell) if tile_data and tile_data.get_navigation_polygon(0): var local_nav = tile_data.get_navigation_polygon(0) var world_verts = PackedVector2Array() for vert in local_nav.vertices: world_verts.append(tile_map.map_to_local(cell) + vert) nav_poly.add_outline(world_verts) nav_poly.make_polygons_from_outlines() navigation_region.navigation_polygon = nav_poly
Common Patterns
Multi-Agent Coordination
# Group movement with formation func move_formation_to(target: Vector2, units: Array[CharacterBody2D]): var leader = units[0] leader.nav_agent.target_position = target # Other units follow with offsets for i in range(1, units.size()): var offset = Vector2(i * 30, 0) units[i].nav_agent.target_position = target + offset
Dynamic Path Cost
# Different terrain costs func calculate_path_cost(path: PackedVector2Array, terrain_map: TileMap) -> float: var cost = 0.0 for i in range(path.size() - 1): var terrain_type = get_terrain_at(path[i], terrain_map) match terrain_type: "grass": cost += 1.0 "mud": cost += 2.0 "road": cost += 0.5 return cost
Navigation Debug Visualization
# Draw path for debugging func _draw(): if nav_agent.is_navigation_finished(): return var path = nav_agent.get_current_navigation_path() if path.size() > 0: var local_path = PackedVector2Array() for point in path: local_path.append(to_local(point)) draw_polyline(local_path, Color.GREEN, 2.0) for point in local_path: draw_circle(point, 3.0, Color.RED)
Safety
- Always check
before accessing pathis_navigation_finished() - Use
signal for RVO avoidancevelocity_computed - Verify NavigationServer map exists before queries
- Handle cases where no path exists (agent returns empty path)
- Bake navigation after level generation completes
When NOT to Use
Don't use NavigationAgent when:
- Movement is simple and direct (no obstacles)
- Performance is critical (NavigationServer has overhead)
- You need custom pathfinding algorithms (use A* directly)
- Agents don't need obstacle avoidance
Use direct movement instead:
# Simple direct movement - no navigation needed func _physics_process(delta): var direction = (target.global_position - global_position).normalized() velocity = direction * speed move_and_slide()
Integration
Works with:
- godot-migrate-tilemap - Convert TileMap V1 to V2 with navigation
- godot-refactor - Full project refactoring including navigation
- godot-add-signals - Connect navigation events via signals
Performance Tips
- Batch Navigation Updates - Don't bake every frame
- Use NavigationLayers - Separate agents by layer for efficiency
- Limit Max Neighbors - RVO performance scales with neighbor count
- Static vs Dynamic - Use NavigationRegion for static, Obstacles for dynamic
- Path Caching - Cache paths that don't change frequently