Навигация — NavigationServer и AI
NavigationRegion3D, NavigationAgent3D — поиск пути и преследующий враг.
Navigation в Godot 4 — это NavigationServer3D (низкоуровневый сервер) и три высокоуровневых узла: NavigationRegion3D, NavigationAgent3D, NavigationLink3D.
Идея
NavigationMesh — упрощённая полигональная “карта проходимости”. Запекается из коллизий сцены. Агенты ищут путь по этой карте (A* под капотом).
World
├── Ground (StaticBody3D + MeshInstance3D)
├── Walls / Platforms
└── NavigationRegion3D
└── navigation_mesh: NavigationMesh (запекается)
Enemy
├── (модель + коллайдер)
└── NavigationAgent3D (target_position = player_position)
Запекание NavigationMesh
- Соберите статичную геометрию.
- Добавьте узел NavigationRegion3D на сцену.
- В Inspector создайте новый ресурс NavigationMesh на этом узле.
- Кнопка Bake NavigationMesh в верхней панели → запекание.
В Geometry → Source Geometry Mode на NavigationMesh выбирается, из чего собирать меш:
SOURCE_GEOMETRY_ROOT_NODE_CHILDREN— обрабатывает детей NavigationRegion3D (положите внутрь региона статичную геометрию).SOURCE_GEOMETRY_GROUPS_WITH_CHILDREN— узлы из заданной группы + их дети.SOURCE_GEOMETRY_GROUPS_EXPLICIT— только узлы из группы.
Параметры NavigationMesh:
agent_radius— радиус агента (отступ от стен).agent_height— высота агента.agent_max_climb— макс высота ступеньки, которую агент перешагнёт.agent_max_slope— макс угол склона в градусах.cell_size/cell_height— точность мешa.
Если у вас несколько NavigationRegion3D — они автоматически соединяются на границах. Удобно для модульных уровней: каждая комната — свой регион.
NavigationAgent3D — движение по сетке
NavigationAgent3D — дочерний узел тела, которое должно ходить по мешу. Не двигает само,
рассчитывает следующую точку пути:
extends CharacterBody3D
@export var speed: float = 4.0
@export var target: Node3D
@onready var nav_agent: NavigationAgent3D = $NavigationAgent3D
func _ready() -> void:
nav_agent.path_desired_distance = 0.5
nav_agent.target_desired_distance = 1.0
func _physics_process(delta: float) -> void:
if target == null:
return
nav_agent.target_position = target.global_position
if nav_agent.is_navigation_finished():
return
var next_point := nav_agent.get_next_path_position()
var direction := (next_point - global_position).normalized()
velocity.x = direction.x * speed
velocity.z = direction.z * speed
if not is_on_floor():
velocity += get_gravity() * delta
move_and_slide()
Главные методы:
get_next_path_position()— куда идти на этом тике.is_navigation_finished()— путь завершён.is_target_reachable()— есть ли путь до цели.distance_to_target()— оставшееся расстояние.
Сигналы:
target_reached— достигли цели.waypoint_reached(details)— достигли промежуточной точки.navigation_finished— путь полностью пройден.velocity_computed(safe_velocity)— если включён avoidance.
Avoidance — расступание агентов
Если на сцене много агентов, идущих в одну точку, без расступания они сольются “стопкой”. Включите
avoidance_enabled = true на NavigationAgent3D — заработает RVO2 (reciprocal velocity
obstacles).
nav_agent.avoidance_enabled = true
nav_agent.radius = 0.6 # радиус расступания
nav_agent.neighbor_distance = 5.0 # видим соседей в этом радиусе
nav_agent.velocity_computed.connect(_on_safe_velocity)
func _on_safe_velocity(safe_vel: Vector3) -> void:
# Используйте safe_vel вместо raw velocity
velocity.x = safe_vel.x
velocity.z = safe_vel.z
move_and_slide()
func _physics_process(delta: float) -> void:
# ...
nav_agent.set_velocity(desired_velocity) # отдаём желаемую — получаем safe через сигнал
NavigationLink3D — прыжки и телепорты
NavigationMesh не поймёт “тут можно прыгнуть через пропасть”. Для этого — NavigationLink3D: прямая связь между двумя точками. Агенты идут по ней как по обычному ребру графа.
В Inspector:
start_position+end_position— концы линка.bidirectional— двусторонний или нет.
При прохождении такого ребра агент пройдёт геометрически прямо — это значит, что вам нужно самостоятельно сделать “прыжок” (анимация + физика) в скрипте, отловив проход через линк.
NavigationObstacle3D — динамические препятствия
Двери, ящики, поваленные деревья — динамические препятствия. Два режима:
- Без
affect_navigation_mesh— просто отталкивает агентов через avoidance. - С
affect_navigation_mesh = true— “вырезает” дыру в navmesh (требует rebake региона на лету). Дорого, но точно.
Простой враг с state machine
Полный пример из главы про Unity NavMesh, переписанный на Godot:
extends CharacterBody3D
class_name EnemyChaser
enum State { PATROL, CHASE, ATTACK }
@export var patrol_points: Array[Node3D] = []
@export var player: Node3D
@export var speed: float = 3.0
@export var sight_range: float = 12.0
@export var attack_range: float = 2.0
@export var attack_cooldown: float = 1.2
@onready var nav_agent: NavigationAgent3D = $NavigationAgent3D
var state: State = State.PATROL
var patrol_index: int = 0
var next_attack_time: float = 0.0
func _ready() -> void:
if patrol_points.size() > 0:
nav_agent.target_position = patrol_points[0].global_position
func _physics_process(delta: float) -> void:
var dist_to_player = global_position.distance_to(player.global_position)
match state:
State.PATROL:
if dist_to_player < sight_range and _has_line_of_sight():
state = State.CHASE
elif nav_agent.is_navigation_finished():
patrol_index = (patrol_index + 1) % patrol_points.size()
nav_agent.target_position = patrol_points[patrol_index].global_position
State.CHASE:
nav_agent.target_position = player.global_position
if dist_to_player < attack_range:
state = State.ATTACK
elif dist_to_player > sight_range * 1.5:
state = State.PATROL
State.ATTACK:
nav_agent.target_position = global_position # стоим
var now = Time.get_ticks_msec() / 1000.0
if now >= next_attack_time:
_attack()
next_attack_time = now + attack_cooldown
if dist_to_player > attack_range + 0.5:
state = State.CHASE
_move(delta)
func _move(delta: float) -> void:
if state != State.ATTACK and not nav_agent.is_navigation_finished():
var next_point = nav_agent.get_next_path_position()
var dir = (next_point - global_position).normalized()
velocity.x = dir.x * speed
velocity.z = dir.z * speed
else:
velocity.x = move_toward(velocity.x, 0, speed)
velocity.z = move_toward(velocity.z, 0, speed)
if not is_on_floor():
velocity += get_gravity() * delta
move_and_slide()
func _has_line_of_sight() -> bool:
var space = get_world_3d().direct_space_state
var query = PhysicsRayQueryParameters3D.create(
global_position + Vector3.UP, player.global_position
)
query.exclude = [self]
var hit = space.intersect_ray(query)
return hit.is_empty() or hit.collider == player
func _attack() -> void:
if player.has_method("take_damage"):
player.take_damage(10)
Обратите внимание на пороги: вход в CHASE на sight_range, выход — на sight_range * 1.5.
Это hysteresis, который не даёт врагу “дёргаться” между состояниями на границе. Тот же
приём, что и в Unity-главе.
В следующей главе — частицы.