name: godot-setup-navigation version: 1.0.0 displayName: Setup Navigation System description: > Use when setting up NavigationServer API, creating NavigationRegion2D/3D scenes, generating NavigationPolygon from TileMap, configuring NavigationAgent2D/3D, implementing obstacle avoidance with RVO, or creating pathfinding integration patterns. Supports 2D top-down, 3D navigation, and TileMap-based navigation. author: Asreonn license: MIT category: game-development type: tool difficulty: advanced audience: [developers] keywords:
- godot
- navigation
- pathfinding
- navmesh
- navigationagent
- obstacle-avoidance
- a-star
- 2d
- 3d platforms: [macos, linux, windows] repository: https://github.com/asreonn/godot-superpowers homepage: https://github.com/asreonn/godot-superpowers#readme
permissions: filesystem: read: [".gd", ".tscn", ".tres", ".tilemap"] write: [".tscn", ".tres", ".gd"] git: true
behavior: auto_rollback: true validation: true git_commits: true
outputs: "NavigationRegion2D/3D scenes, NavigationPolygon resources, NavigationAgent2D/3D configurations, pathfinding integration scripts, git commits" requirements: "Git repository, Godot 4.x, NavigationServer API" execution: "Fully automatic with scene generation and script updates" integration: "Works with godot-migrate-tilemap for TileMap-based navigation"
Setup 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
is_navigation_finished()before accessing path - Use
velocity_computedsignal for RVO avoidance - 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