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

  1. Añade un nodo NavigationRegion2D a tu escena.
  2. En el Inspector, crea un nuevo recurso NavigationPolygon.
  3. 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 + 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()

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