Introducción
Godot 4 cuenta con un robusto sistema de navegación basado en NavigationServer2D/3D, los nodos NavigationRegion y los nodos NavigationAgent. Es una mejora significativa respecto al sistema de Godot 3 — más flexible, más eficiente y mucho más fácil de configurar para escenarios complejos como obstáculos dinámicos y múltiples tipos de agentes.
Resumen rápido: NavigationRegion define dónde pueden caminar los agentes. NavigationAgent se encarga del pathfinding de cada entidad. NavigationServer gestiona todo entre bastidores.
Conceptos fundamentales
- NavigationRegion2D / NavigationRegion3D — Define un área transitable mediante un NavigationPolygon (2D) o un NavigationMesh (3D). Puedes tener varias regiones; el servidor las fusiona automáticamente.
- NavigationAgent2D / NavigationAgent3D — Se adjunta a un CharacterBody para gestionar el pathfinding. Calcula la siguiente posición del camino, gestiona la evasión y emite señales cuando la navegación finaliza.
- NavigationServer2D / NavigationServer3D — Singleton que gestiona todos los datos de navegación. Rara vez interactúas con él directamente, pero se encarga de las actualizaciones del mapa, las consultas de caminos y las conexiones entre regiones.
Configurar la navegación 2D
Paso 1: Añadir un NavigationRegion2D
- Añade un nodo NavigationRegion2D a tu escena.
- En el Inspector, crea un nuevo recurso NavigationPolygon.
- Dibuja el polígono transitable en el editor 2D. El polígono define dónde pueden caminar los agentes.
Integración con TileMap: Un NavigationPolygon puede hornearse (bake) a partir de datos de TileMap. Si tus tiles tienen polígonos de navegación definidos en el TileSet, basta con añadir un NavigationRegion2D y hornear — generará automáticamente el área transitable a partir del diseño de tus tiles.
Añadir un NavigationAgent2D
Añade un NavigationAgent2D como hijo de tu CharacterBody2D. Aquí tienes un script de movimiento 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: El NavigationServer necesita un frame de física para sincronizarse. Ejecuta siempre await get_tree().physics_frame antes de establecer la primera posición de destino en _ready(), o el agente podría no encontrar un camino válido.
Propiedades clave de NavigationAgent2D
-
target_position— Adónde quiere ir el agente -
path_desired_distance— Cuán cerca debe estar el agente de cada punto del camino para avanzar al siguiente -
target_desired_distance— Cuán cerca debe estar el agente del destino para considerar la navegación finalizada -
max_speed— Se usa para los cálculos de evasión (no limita tu código de movimiento)
Navegación 3D
El sistema de navegación 3D funciona de forma idéntica al 2D, solo con distintos tipos de nodos:
NavigationRegion3D+NavigationMeshNavigationAgent3DNavigationServer3D
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()
Hornear mallas de navegación
En 3D normalmente horneas la malla de navegación a partir de la geometría del nivel en lugar de dibujarla a mano.
Configuración del horneado
Propiedades clave del recurso NavigationMesh:
- Agent Radius — Cuánto se mantienen los agentes alejados de las paredes. Valores más grandes generan caminos más conservadores.
- Agent Height — Altura mínima del techo para las áreas transitables.
- Agent Max Climb — Altura máxima de escalón que los agentes pueden subir (por ejemplo, escaleras).
- Agent Max Slope — Ángulo máximo de pendiente transitable, en grados.
# 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!")
Capas de navegación
Las capas de navegación te permiten separar áreas transitables para distintos tipos de agentes. Por ejemplo, las unidades terrestres y las voladoras pueden tener mallas de navegación diferentes.
# 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 configuración de capas típica:
- Layer 1 — Unidades terrestres (soldados, vehículos)
- Layer 2 — Unidades voladoras (drones, aves)
- Layer 3 — Unidades grandes (solo corredores anchos)
Evasión (Avoidance)
NavigationAgent incorpora evasión local para evitar que los agentes se solapen. Cuando la evasión está activada, el agente calcula una velocidad segura que lo aparta de los demás agentes.
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()
Cómo funciona: En lugar de usar la velocidad directamente, asignas a nav_agent.velocity tu velocidad deseada. Entonces el agente calcula una safe_velocity mediante la señal velocity_computed que esquiva a los agentes cercanos. Aplicas esta velocidad segura en el callback.
Navegación dinámica
Puedes modificar la malla de navegación en tiempo de ejecución para gestionar puertas, paredes destructibles o terreno cambiante:
# 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 sobre rendimiento: Hornear una malla de navegación 3D en tiempo de ejecución es costoso. Para obstáculos dinámicos en 3D, es preferible usar NavigationObstacle3D en lugar de volver a hornear. En 2D, puedes alternar NavigationRegion2D.enabled de forma económica.
Patrones habituales
IA enemiga que persigue al jugador
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()
Rutas de patrulla de 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()
Clic para moverse (vista cenital)
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()
Cambios de Godot 3 a Godot 4
| Godot 3 | Godot 4 |
|---|---|
Nodo Navigation2D |
Eliminado. Usa NavigationRegion2D + NavigationAgent2D |
Navigation.get_simple_path() |
NavigationServer2D.map_get_path() |
| Seguimiento manual de caminos | NavigationAgent.get_next_path_position() se encarga de ello |
| Sin evasión integrada | NavigationAgent.avoidance_enabled |
| Sin capas de navegación | 32 capas de navegación por mapa |
| Sin obstáculos | NavigationObstacle2D/3D |