Parte: 5 — Inteligencia artificial para juegos · Fuente: Documentación oficial de Godot 4 — Navigation (NavigationServer2D/3D) + Millington, "AI for Games" (3ª ed., cap. Pathfinding) ⏱️ Duración estimada: 55 min · Nivel: Intermedio
Aprender a mover agentes por un escenario usando el sistema de navegación nativo de Godot 4. Al terminar sabrás hornear (bake) una malla de navegación con NavigationRegion2D/NavigationRegion3D, mover un agente con NavigationAgent2D/NavigationAgent3D siguiendo la ruta calculada por el servidor, activar la evitación local para esquivar obstáculos, y decidir cuándo conviene un navmesh frente a una búsqueda A* sobre grilla.
Al finalizar, el alumno podrá:
NavigationAgent con target_position y consumir get_next_path_position() en _physics_process.avoidance_enabled y resolver el movimiento mediante la señal velocity_computed.NavigationObstacle y conectar zonas con NavigationLink.| # | Tema | Por qué importa |
|---|---|---|
| 1 | Malla de navegación (navmesh) | Es la representación caminable del mundo sobre la que se calcula toda ruta |
| 2 | Bake de NavigationRegion |
Sin hornear, el agente no tiene superficie donde moverse |
| 3 | NavigationAgent y rutas |
Traduce un destino en una secuencia de puntos alcanzables |
| 4 | Evitación local (avoidance) | Evita que varios agentes o obstáculos se atraviesen entre sí |
| 5 | Obstáculos dinámicos | Permite bloquear zonas en tiempo real sin re-hornear todo |
| 6 | Enlaces de navegación (links) | Conectan islas separadas: saltos, escaleras, teletransportes |
| 7 | Navmesh vs A* de grilla | Elegir la técnica correcta según el tipo de mapa |
NavigationServer. Clave: se hornea desde el editor o por código con bake_navigation_mesh().Necesitas Godot 4.x (estable). Trabajaremos con dos escenas separadas: una 2D y una 3D. Ten a mano la documentación de NavigationServer2D, NavigationAgent2D y la guía Using NavigationAgents. Crea una carpeta res://ia/navegacion/ para los scripts. En Project Settings → Navigation puedes activar la visualización de depuración para ver los polígonos horneados.
Construiremos un agente 2D que persigue al cursor esquivando un obstáculo móvil, y luego replicaremos la idea en 3D.
Crea una escena con esta jerarquía:
Mundo2D (Node2D)
├── NavigationRegion2D
│ └── (polígono horneado)
├── Agente (CharacterBody2D)
│ └── NavigationAgent2D
└── ObstaculoMovil (NavigationObstacle2D dentro de un Node2D)
En NavigationRegion2D crea un nuevo NavigationPolygon en el inspector, dibuja el contorno del área caminable (deja un hueco central) y pulsa Bake NavigationPolygon. Verás el polígono relleno: esa es la superficie transitable.
extends CharacterBody2D
@export var velocidad: float = 220.0
@onready var agente: NavigationAgent2D = $NavigationAgent2D
func _ready() -> void:
# La ruta se calcula en el servidor; esperamos un frame para que exista.
agente.avoidance_enabled = true
agente.radius = 16.0
# Al terminar el cálculo de evitación, el servidor nos da una velocidad segura.
agente.velocity_computed.connect(_on_velocidad_calculada)
func _physics_process(_delta: float) -> void:
# El destino es la posición del ratón en coordenadas globales.
agente.target_position = get_global_mouse_position()
if agente.is_navigation_finished():
velocity = Vector2.ZERO
move_and_slide()
return
var siguiente: Vector2 = agente.get_next_path_position()
var deseada: Vector2 = (siguiente - global_position).normalized() * velocidad
# En vez de mover directamente, entregamos la velocidad deseada al sistema de evitación.
agente.set_velocity(deseada)
func _on_velocidad_calculada(velocidad_segura: Vector2) -> void:
# Esta velocidad ya rodea obstáculos y otros agentes.
velocity = velocidad_segura
move_and_slide()
Ejecuta y mueve el ratón: el agente sigue la ruta horneada. Observable: al pasar cerca del obstáculo, lo rodea en vez de atravesarlo.
Da movimiento al obstáculo para comprobar la evitación en tiempo real:
extends Node2D
# Nodo que contiene un NavigationObstacle2D con su 'radius' configurado.
@export var amplitud: float = 140.0
@export var velocidad: float = 1.5
var _base: Vector2
func _ready() -> void:
_base = position
func _process(delta: float) -> void:
# Vaivén horizontal: el agente debe re-esquivarlo cada frame.
var t: float = Time.get_ticks_msec() / 1000.0
position.x = _base.x + sin(t * velocidad) * amplitud
En una escena 3D, usa NavigationRegion3D con un NavigationMesh. Añade suelo y paredes como MeshInstance3D hijos y pulsa Bake NavigationMesh. El agente cambia poco:
extends CharacterBody3D
@export var velocidad: float = 4.0
@onready var agente: NavigationAgent3D = $NavigationAgent3D
@export var objetivo: Node3D # asigna un Marker3D en el inspector
func _ready() -> void:
agente.avoidance_enabled = true
agente.velocity_computed.connect(_on_velocidad_calculada)
func _physics_process(_delta: float) -> void:
agente.target_position = objetivo.global_position
if agente.is_navigation_finished():
return
var siguiente: Vector3 = agente.get_next_path_position()
var deseada: Vector3 = (siguiente - global_position).normalized() * velocidad
agente.set_velocity(deseada)
func _on_velocidad_calculada(velocidad_segura: Vector3) -> void:
velocity = velocidad_segura
move_and_slide()
Observable: el agente 3D camina desde su posición hasta el Marker3D rodeando muros.
radius del agente 2D a 40 y observa cómo mantiene mayor distancia de los obstáculos.NavigationLink2D.target_position del ratón por clics: fija el destino solo al hacer clic izquierdo.print de agente.get_current_navigation_path().size().Crea un nivel 2D con tres salas conectadas por pasillos y un NavigationLink2D que represente una "puerta secreta" entre la primera y la tercera sala. Un agente debe navegar de la sala 1 a la sala 3, y un obstáculo móvil en el pasillo central debe forzar la evitación al menos una vez.
Criterio de aceptación: al ejecutar, el agente llega a la sala 3 usando el enlace secreto cuando este ofrece la ruta más corta, nunca atraviesa el obstáculo móvil, y is_navigation_finished() devuelve true al llegar.
| Síntoma | Causa y arreglo |
|---|---|
| El agente no se mueve | Olvidaste hornear la región o el destino cae fuera del navmesh. Re-bake y verifica que target_position esté sobre el polígono. |
| El agente atraviesa obstáculos | avoidance_enabled está en false o no usas velocity_computed. Actívalo y mueve solo dentro del callback. |
| Movimiento tembloroso | Estás moviendo con get_next_path_position y con la velocidad de evitación a la vez. Usa solo la ruta del callback. |
| La ruta ignora el enlace | El NavigationLink no comparte capas (navigation_layers) con el agente. Iguala las capas. |
get_next_path_position devuelve la posición actual |
Pediste la ruta el mismo frame que asignaste el destino. Espera un frame físico o usa await. |
¿El navmesh se re-hornea solo si muevo geometría? No. El bake es un proceso explícito; para cambios en tiempo real usa NavigationObstacle o re-hornea por código.
¿Puedo usar move_and_slide() sin avoidance? Sí. Sin evitación, mueves directamente con la velocidad hacia get_next_path_position(); los choques los resuelve la física de colisión, no el navegador.
¿Cuándo prefiero A* de grilla en vez de navmesh? En mapas de celdas (tácticos por turnos, tower defense) donde el movimiento es discreto; el navmesh brilla en espacios continuos y abiertos.
¿La evitación garantiza que nunca haya colisiones? No al 100%. Es una capa local predictiva; combínala con cuerpos de colisión para casos extremos.
Clase 113 - Pathfinding: A* explicado y aplicado