~2 мин чтения

NavMesh и AI — поиск пути и простой ИИ-противник

Navigation Mesh, NavMeshAgent, базовая стейт-машина для врага.

3D-уровень редко состоит из ровного пола. Враг должен обойти стол, спуститься по лестнице, найти короткий путь к игроку и не свалиться с обрыва. Считать это вручную через Physics.Raycast — кошмар. В Unity есть NavMesh — встроенная система навигации, которая решает 90% задач AI-движения бесплатно.

NavMesh — это запечённая упрощённая геометрия пола, по которой умеют ходить агенты. Unity сканирует вашу сцену и собирает полигональную сетку всех поверхностей, на которых можно стоять. Из этой сетки строится граф для алгоритма поиска пути (A* под капотом).

В Unity 6 рекомендованный путь — пакет AI Navigation (он же com.unity.ai.navigation). Старый встроенный NavMesh (через Window → Navigation в Unity ≤ 2021) сохранился для совместимости, но новый пакет даёт компонентный воркфлоу: NavMeshSurface, NavMeshModifier, NavMeshLink.

Базовый сценарий запекания

  1. Соберите статичную геометрию пола и стен.
  2. Добавьте на пустой GameObject компонент NavMeshSurface.
  3. В Agent Type выберите профиль (по умолчанию “Humanoid”: радиус 0.5, высота 2, max slope 45°, step height 0.4).
  4. Нажмите Bake. Появится синий полупрозрачный mesh в Scene view — это и есть NavMesh.
            __________
           /          \      ← NavMesh покрывает пол
          /  Comfy     \
         |   Room       |
          \    [agent]_/
           \__________/

   Стены, столбы, ямы → "дыры" в NavMesh
NavMeshSurface vs legacy Navigation window

В новом проекте используйте NavMeshSurface — он позволяет иметь несколько мешей в сцене, запекать рантайм (для процедурных уровней), и не зависит от глобальной настройки сцены. Старое меню Window → Navigation продолжает работать, но это deprecated-путь.

NavMeshAgent — компонент, превращающий GameObject в “пешехода” по NavMesh:

using UnityEngine;
using UnityEngine.AI;

[RequireComponent(typeof(NavMeshAgent))]
public class Chaser : MonoBehaviour
{
    [SerializeField] private Transform target;

    private NavMeshAgent _agent;

    private void Awake() => _agent = GetComponent<NavMeshAgent>();

    private void Update() {
        if (target != null) {
            _agent.SetDestination(target.position);
        }
    }
}

Главные поля NavMeshAgent:

  • Speed — скорость м/с.
  • Angular Speed — скорость поворота в градусах/с.
  • Acceleration — м/с².
  • Stopping Distance — на каком расстоянии до цели агент остановится. Для атаки в ближнем бою ставят что-то вроде 1.5 м.
  • Auto Braking — плавно тормозит при приближении к цели.
  • Obstacle Avoidance Type — приоритет в локальном расступании, когда несколько агентов встречаются.

Двери, ящики, поваленные деревья — динамические препятствия. NavMeshObstacle бывает двух режимов:

  • Без carve — агенты обходят препятствие через avoidance, но NavMesh не меняется. Дёшево.
  • С carve = true — препятствие “вырезает” дыру в NavMesh, агенты строят новый путь. Дороже, но корректно для закрытых дверей и больших препятствий.

Стандартный NavMesh не умеет в “телепорты”: лестницу с разрывом, прыжок через пропасть, лифт. NavMeshLink соединяет два места — агент пересечёт его как обычное ребро графа.

Простой стейт-машина AI

Перейдём от “ходит к цели” к минимально живому врагу: патрулирует, заметил игрока — преследует, догнал — атакует.

using UnityEngine;
using UnityEngine.AI;

[RequireComponent(typeof(NavMeshAgent))]
public class EnemyAI : MonoBehaviour
{
    private enum State { Patrol, Chase, Attack }

    [SerializeField] private Transform[] patrolPoints;
    [SerializeField] private Transform player;
    [SerializeField] private float sightRange = 12f;
    [SerializeField] private float attackRange = 2f;
    [SerializeField] private float attackCooldown = 1.2f;

    private NavMeshAgent _agent;
    private State _state = State.Patrol;
    private int _patrolIndex;
    private float _nextAttackTime;

    private void Awake() => _agent = GetComponent<NavMeshAgent>();

    private void Start() {
        if (patrolPoints.Length > 0) {
            _agent.SetDestination(patrolPoints[0].position);
        }
    }

    private void Update() {
        float distToPlayer = Vector3.Distance(transform.position, player.position);

        switch (_state) {
            case State.Patrol:
                if (distToPlayer < sightRange && HasLineOfSight()) {
                    _state = State.Chase;
                } else if (!_agent.pathPending && _agent.remainingDistance < 0.5f) {
                    _patrolIndex = (_patrolIndex + 1) % patrolPoints.Length;
                    _agent.SetDestination(patrolPoints[_patrolIndex].position);
                }
                break;

            case State.Chase:
                _agent.SetDestination(player.position);
                if (distToPlayer < attackRange) _state = State.Attack;
                else if (distToPlayer > sightRange * 1.5f) _state = State.Patrol;
                break;

            case State.Attack:
                _agent.SetDestination(transform.position); // стоим на месте
                if (Time.time >= _nextAttackTime) {
                    Attack();
                    _nextAttackTime = Time.time + attackCooldown;
                }
                if (distToPlayer > attackRange + 0.5f) _state = State.Chase;
                break;
        }
    }

    private bool HasLineOfSight() {
        Vector3 dir = player.position - transform.position;
        if (Physics.Raycast(transform.position + Vector3.up, dir.normalized, out RaycastHit hit, sightRange)) {
            return hit.transform == player;
        }
        return false;
    }

    private void Attack() {
        // Сюда — анимация атаки и нанесение урона через ваш Health
        if (player.TryGetComponent<Health>(out var hp)) hp.TakeDamage(10);
    }
}
Hysteresis в переходах состояний

Обратите внимание: переход обратно из Chase в Patrol использует sightRange * 1.5f, а из Chase в Attack — attackRange, тогда как из Attack в Chase — attackRange + 0.5f. Это гистерезис: пороги входа и выхода не совпадают, чтобы враг не “дёргался” между состояниями ровно на границе. Простой приём, заметно повышает ощущение цельности AI.

Что улучшить

  • Поведенческие деревья (Behavior Tree) — для AI сложнее 3 состояний. Готовые пакеты — Behavior Designer, или новый встроенный Unity Behavior (в Unity 6 пришла официальная Behavior Trees система).
  • Memory & senses — пары “вижу/слышу/помню”, где враг помнит последнюю позицию игрока ещё несколько секунд.
  • Group coordination — несколько врагов делят зоны атаки через NavMeshAgent.avoidancePriority.
  • Cover system — поиск точек укрытия через NavMesh.SamplePosition и Raycast.

В следующей главе — Particle System и VFX Graph, чтобы враг при смерти эффектно разлетелся осколками.