Введение
В Godot 4 реализована надёжная система навигации, построенная на NavigationServer2D/3D, нодах NavigationRegion и нодах NavigationAgent. Это значительный шаг вперёд по сравнению с системой Godot 3 — она более гибкая, более производительная и намного проще в настройке для сложных сценариев вроде динамических препятствий и нескольких типов агентов.
Краткий обзор: NavigationRegion определяет, где могут ходить агенты. NavigationAgent отвечает за поиск пути для отдельных сущностей. NavigationServer управляет всем за кулисами.
Основные концепции
- NavigationRegion2D / NavigationRegion3D — Определяет проходимую область с помощью NavigationPolygon (2D) или NavigationMesh (3D). Регионов может быть несколько; сервер объединяет их автоматически.
- NavigationAgent2D / NavigationAgent3D — Прикрепляется к CharacterBody для обработки поиска пути. Вычисляет следующую позицию пути, обрабатывает уклонение и посылает сигналы по завершении навигации.
- NavigationServer2D / NavigationServer3D — Синглтон, который управляет всеми навигационными данными. С ним редко взаимодействуют напрямую, но он обрабатывает обновления карты, запросы путей и соединения регионов.
Настройка 2D-навигации
Шаг 1: Добавьте NavigationRegion2D
- Добавьте в сцену ноду NavigationRegion2D.
- В инспекторе создайте новый ресурс NavigationPolygon.
- Нарисуйте проходимый полигон в 2D-редакторе. Полигон определяет, где агенты могут ходить.
Интеграция с TileMap: NavigationPolygon можно запечь из данных TileMap. Если у ваших тайлов в TileSet определены навигационные полигоны, просто добавьте NavigationRegion2D и запеките — проходимая область будет автоматически сгенерирована из раскладки тайлов.
Добавление NavigationAgent2D
Добавьте NavigationAgent2D как дочернюю ноду вашего CharacterBody2D. Вот полный скрипт движения:
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()
Важно: NavigationServer нужен один кадр физики для синхронизации. Всегда выполняйте await get_tree().physics_frame перед установкой первой целевой позиции в _ready(), иначе агент может не найти корректный путь.
Ключевые свойства NavigationAgent2D
-
target_position— Куда агент хочет двигаться -
path_desired_distance— Насколько близко агент должен быть к каждой точке пути, чтобы перейти к следующей -
target_desired_distance— Насколько близко агент должен быть к цели, чтобы навигация считалась завершённой -
max_speed— Используется для расчётов уклонения (не ограничивает ваш код движения)
3D-навигация
Система 3D-навигации работает так же, как и 2D, только с другими типами нод:
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()
Запекание навигационных мешей
В 3D навигационный меш обычно запекается из геометрии уровня, а не рисуется вручную.
Настройка запекания
Ключевые свойства ресурса NavigationMesh:
- Agent Radius — Насколько далеко агенты держатся от стен. Большие значения создают более консервативные пути.
- Agent Height — Минимальная высота потолка для проходимых областей.
- Agent Max Climb — Максимальная высота ступеньки, на которую агенты могут подниматься (например, лестницы).
- Agent Max Slope — Максимальный проходимый угол наклона в градусах.
# 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!")
Слои навигации
Слои навигации позволяют разделять проходимые области для разных типов агентов. Например, наземные и летающие юниты могут иметь разные навигационные меши.
# 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
Типичная конфигурация слоёв:
- Layer 1 — Наземные юниты (солдаты, транспорт)
- Layer 2 — Летающие юниты (дроны, птицы)
- Layer 3 — Крупные юниты (только широкие коридоры)
Уклонение (Avoidance)
NavigationAgent имеет встроенное локальное уклонение, чтобы агенты не перекрывали друг друга. Когда уклонение включено, агент вычисляет безопасную скорость, уводящую его от других агентов.
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()
Как это работает: Вместо того чтобы использовать скорость напрямую, вы задаёте nav_agent.velocity равной желаемой скорости. Затем агент через сигнал velocity_computed вычисляет safe_velocity, уклоняющуюся от близких агентов. Эту безопасную скорость вы применяете в колбэке.
Динамическая навигация
Вы можете изменять навигационный меш во время выполнения, чтобы обрабатывать двери, разрушаемые стены или меняющийся ландшафт:
# 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
Замечание о производительности: Запекание 3D-навигационного меша во время выполнения обходится дорого. Для динамических препятствий в 3D предпочтительнее использовать NavigationObstacle3D, а не повторное запекание. В 2D можно дёшево переключать NavigationRegion2D.enabled.
Распространённые паттерны
ИИ врага, преследующий игрока
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()
Маршруты патрулирования 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()
Перемещение по клику (вид сверху)
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()
Изменения от Godot 3 к Godot 4
| Godot 3 | Godot 4 |
|---|---|
Нода Navigation2D |
Удалена. Используйте NavigationRegion2D + NavigationAgent2D |
Navigation.get_simple_path() |
NavigationServer2D.map_get_path() |
| Ручное следование по пути | Этим занимается NavigationAgent.get_next_path_position() |
| Нет встроенного уклонения | NavigationAgent.avoidance_enabled |
| Нет слоёв навигации | 32 слоя навигации на карту |
| Нет препятствий | NavigationObstacle2D/3D |