Introduzione

Godot 4 ha un sistema di navigazione robusto costruito su NavigationServer2D/3D, i nodi NavigationRegion e i nodi NavigationAgent. È un miglioramento significativo rispetto al sistema di Godot 3 — più flessibile, più performante e molto più facile da configurare per scenari complessi come ostacoli dinamici e più tipi di agente.

Panoramica rapida: NavigationRegion definisce dove gli agenti possono camminare. NavigationAgent gestisce il pathfinding per le singole entità. NavigationServer gestisce tutto dietro le quinte.

Concetti fondamentali

  • NavigationRegion2D / NavigationRegion3D — Definisce un'area percorribile tramite un NavigationPolygon (2D) o un NavigationMesh (3D). Puoi avere più regioni; il server le unisce automaticamente.
  • NavigationAgent2D / NavigationAgent3D — Da collegare a un CharacterBody per gestire il pathfinding. Calcola la posizione successiva del percorso, gestisce l'avoidance ed emette segnali quando la navigazione è terminata.
  • NavigationServer2D / NavigationServer3D — Singleton che gestisce tutti i dati di navigazione. Raramente ci interagisci direttamente, ma si occupa degli aggiornamenti della mappa, delle query di percorso e delle connessioni tra regioni.

Configurare la navigazione 2D

Passo 1: aggiungere un NavigationRegion2D

  1. Aggiungi un nodo NavigationRegion2D alla tua scena.
  2. Nell'Inspector, crea una nuova risorsa NavigationPolygon.
  3. Disegna il poligono percorribile nell'editor 2D. Il poligono definisce dove gli agenti possono camminare.

Integrazione con la TileMap: un NavigationPolygon può essere generato (baked) dai dati della TileMap. Se le tue tile hanno poligoni di navigazione definiti nel TileSet, basta aggiungere un NavigationRegion2D ed eseguire il baking — genererà automaticamente l'area percorribile dal layout delle tue tile.

Aggiungere un NavigationAgent2D

Aggiungi un NavigationAgent2D come figlio del tuo CharacterBody2D. Ecco uno script di movimento completo:

extends CharacterBody2D

@export var speed: float = 200.0
@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D

func _ready() -> void:
    # Wait for the navigation map to be ready
    await get_tree().physics_frame

    nav_agent.path_desired_distance = 4.0
    nav_agent.target_desired_distance = 4.0

func set_target(target_pos: Vector2) -> void:
    nav_agent.target_position = target_pos

func _physics_process(delta: float) -> void:
    if nav_agent.is_navigation_finished():
        return

    var next_pos := nav_agent.get_next_path_position()
    var direction := global_position.direction_to(next_pos)
    velocity = direction * speed
    move_and_slide()

Importante: il NavigationServer ha bisogno di un frame di fisica per sincronizzarsi. Esegui sempre await get_tree().physics_frame prima di impostare la prima posizione di destinazione in _ready(), altrimenti l'agente potrebbe non trovare un percorso valido.

Proprietà chiave di NavigationAgent2D

  • target_position — Dove l'agente vuole andare
  • path_desired_distance — Quanto l'agente deve essere vicino a ciascun punto del percorso per avanzare al successivo
  • target_desired_distance — Quanto l'agente deve essere vicino alla destinazione perché la navigazione sia considerata terminata
  • max_speed — Usato per i calcoli di avoidance (non limita il tuo codice di movimento)

Navigazione 3D

Il sistema di navigazione 3D funziona in modo identico al 2D, solo con tipi di nodo diversi:

  • NavigationRegion3D + NavigationMesh
  • NavigationAgent3D
  • NavigationServer3D
extends CharacterBody3D

@export var speed: float = 5.0
@onready var nav_agent: NavigationAgent3D = $NavigationAgent3D

func _ready() -> void:
    await get_tree().physics_frame
    nav_agent.path_desired_distance = 0.5
    nav_agent.target_desired_distance = 0.5

func set_target(target_pos: Vector3) -> void:
    nav_agent.target_position = target_pos

func _physics_process(delta: float) -> void:
    if nav_agent.is_navigation_finished():
        return

    var next_pos := nav_agent.get_next_path_position()
    var direction := global_position.direction_to(next_pos)
    velocity = direction * speed
    velocity.y -= 9.8 * delta  # Apply gravity
    move_and_slide()

Baking delle navigation mesh

In 3D di solito generi la navigation mesh dalla geometria del livello (baking) invece di disegnarla a mano.

Configurazione del baking

Proprietà chiave nella risorsa NavigationMesh:

  • Agent Radius — Quanto gli agenti restano lontani dalle pareti. Valori più grandi creano percorsi più conservativi.
  • Agent Height — Altezza minima del soffitto per le aree percorribili.
  • Agent Max Climb — Altezza massima del gradino che gli agenti possono salire (es. scale).
  • Agent Max Slope — Angolo massimo di pendenza percorribile in gradi.
# Bake navigation mesh at runtime
var nav_region: NavigationRegion3D = $NavigationRegion3D
nav_region.bake_navigation_mesh()

# Wait for baking to complete
await nav_region.bake_finished
print("Navigation mesh baked!")

Layer di navigazione

I layer di navigazione ti permettono di separare le aree percorribili per i diversi tipi di agente. Ad esempio, le unità di terra e le unità volanti possono avere navigation mesh differenti.

# Set navigation layers on the agent
nav_agent.set_navigation_layer_value(1, true)   # Ground layer
nav_agent.set_navigation_layer_value(2, false)  # Not flying layer

# Set navigation layers on the region
nav_region.set_navigation_layer_value(1, true)   # This region is for ground
nav_region.set_navigation_layer_value(2, false)  # Not for flying

Una configurazione tipica dei layer:

  • Layer 1 — Unità di terra (soldati, veicoli)
  • Layer 2 — Unità volanti (droni, uccelli)
  • Layer 3 — Unità grandi (solo corridoi ampi)

Avoidance

NavigationAgent dispone di un avoidance locale integrato per impedire che gli agenti si sovrappongano. Quando l'avoidance è attivo, l'agente calcola una velocità sicura che lo allontana dagli altri agenti.

extends CharacterBody2D

@export var speed: float = 200.0
@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D

func _ready() -> void:
    await get_tree().physics_frame
    nav_agent.avoidance_enabled = true
    nav_agent.radius = 20.0
    nav_agent.max_speed = speed
    nav_agent.velocity_computed.connect(_on_velocity_computed)

func set_target(target_pos: Vector2) -> void:
    nav_agent.target_position = target_pos

func _physics_process(delta: float) -> void:
    if nav_agent.is_navigation_finished():
        return

    var next_pos := nav_agent.get_next_path_position()
    var direction := global_position.direction_to(next_pos)
    # Set the desired velocity — the agent will compute a safe one
    nav_agent.velocity = direction * speed

func _on_velocity_computed(safe_velocity: Vector2) -> void:
    velocity = safe_velocity
    move_and_slide()

Come funziona: invece di usare la velocità direttamente, imposti nav_agent.velocity sulla velocità desiderata. L'agente calcola quindi una safe_velocity tramite il segnale velocity_computed che evita gli agenti vicini. Applichi questa velocità sicura nel callback.

Navigazione dinamica

Puoi modificare la navigation mesh a runtime per gestire porte, muri distruttibili o terreni che cambiano:

# Enable/disable a navigation region (e.g., open/close a door)
var door_region: NavigationRegion2D = $DoorNavigationRegion
door_region.enabled = false  # Block this path
door_region.enabled = true   # Open this path

# Add a navigation obstacle (blocks agent paths)
var obstacle := NavigationObstacle2D.new()
obstacle.radius = 30.0
add_child(obstacle)

# Re-bake after level changes (3D)
nav_region.bake_navigation_mesh()
await nav_region.bake_finished

Nota sulle prestazioni: il baking di una navigation mesh 3D a runtime è oneroso. Per gli ostacoli dinamici in 3D, preferisci usare NavigationObstacle3D invece di rieseguire il baking. In 2D puoi commutare NavigationRegion2D.enabled a basso costo.

Pattern comuni

IA nemica che insegue il giocatore

extends CharacterBody2D

@export var speed: float = 150.0
@export var chase_range: float = 300.0
@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D
var player: Node2D

func _ready() -> void:
    await get_tree().physics_frame
    player = get_tree().get_first_node_in_group("player")

func _physics_process(delta: float) -> void:
    if not player:
        return

    var distance := global_position.distance_to(player.global_position)
    if distance > chase_range:
        return  # Too far, don't chase

    # Update target every frame (or throttle to every N frames)
    nav_agent.target_position = player.global_position

    if nav_agent.is_navigation_finished():
        return

    var next_pos := nav_agent.get_next_path_position()
    var direction := global_position.direction_to(next_pos)
    velocity = direction * speed
    move_and_slide()

Rotte di pattuglia degli NPC

extends CharacterBody2D

@export var speed: float = 100.0
@export var patrol_points: Array[Vector2] = []
@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D
var current_patrol_index: int = 0

func _ready() -> void:
    await get_tree().physics_frame
    if patrol_points.size() > 0:
        nav_agent.target_position = patrol_points[0]

func _physics_process(delta: float) -> void:
    if patrol_points.size() == 0:
        return

    if nav_agent.is_navigation_finished():
        # Move to next patrol point
        current_patrol_index = (current_patrol_index + 1) % patrol_points.size()
        nav_agent.target_position = patrol_points[current_patrol_index]
        return

    var next_pos := nav_agent.get_next_path_position()
    var direction := global_position.direction_to(next_pos)
    velocity = direction * speed
    move_and_slide()

Click-to-move (top-down)

extends CharacterBody2D

@export var speed: float = 200.0
@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D

func _ready() -> void:
    await get_tree().physics_frame

func _unhandled_input(event: InputEvent) -> void:
    if event is InputEventMouseButton and event.pressed:
        nav_agent.target_position = get_global_mouse_position()

func _physics_process(delta: float) -> void:
    if nav_agent.is_navigation_finished():
        return

    var next_pos := nav_agent.get_next_path_position()
    var direction := global_position.direction_to(next_pos)
    velocity = direction * speed
    move_and_slide()

Cambiamenti da Godot 3 a Godot 4

Godot 3 Godot 4
Nodo Navigation2D Rimosso. Usa NavigationRegion2D + NavigationAgent2D
Navigation.get_simple_path() NavigationServer2D.map_get_path()
Path following manuale Se ne occupa NavigationAgent.get_next_path_position()
Nessun avoidance integrato NavigationAgent.avoidance_enabled
Nessun layer di navigazione 32 layer di navigazione per mappa
Nessun ostacolo NavigationObstacle2D/3D