Claude-skill-registry godot-setup-navigation

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
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"
manifest: skills/data/godot-setup-navigation/SKILL.md
source content

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:

  1. NavigationRegion2D/3D - Creates navigation mesh boundaries
  2. NavigationPolygon - Generates walkable areas from TileMap or geometry
  3. NavigationAgent2D/3D - Configures pathfinding agents with obstacle avoidance
  4. RVO Integration - Implements Reciprocal Velocity Obstacles for dynamic avoidance
  5. 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_computed
    signal 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

  1. Batch Navigation Updates - Don't bake every frame
  2. Use NavigationLayers - Separate agents by layer for efficiency
  3. Limit Max Neighbors - RVO performance scales with neighbor count
  4. Static vs Dynamic - Use NavigationRegion for static, Obstacles for dynamic
  5. Path Caching - Cache paths that don't change frequently