image

Следование по пути и управление движением


Иногда нам нужно, чтобы ИИ-персонажи бродили по игровому миру, следуя по грубо очерченному или точно заданному пути. Например в гоночной игре ИИ-противники должны ехать по дороге, а в RTS юниты должны уметь перемещаться в нужную точку, двигаясь по рельефу и учитывая положение друг друга.

Чтобы казаться умными, ИИ-агенты должны уметь определять, что они делают, и если не могут достичь нужной точки, то они должны уметь прокладывать наиболее эффективный маршрут и изменять свой путь, когда на пути появляются препятствия.

Избегание препятствий — это простое поведение, позволяющее ИИ-сущностям достигать целевых точек. Важно заметить, что поведение, реализуемое в этом посте, предназначается для таких поведений, как симуляция толпы, в которой основной целью каждого агента является избегание других агентов и достижение цели. Они не определяют наиболее эффективный и кратчайший путь.

Технические требования


Необходима версия Unity 2017, установленная в системе с Windows 7 SP1+, 8, 10 или с Mac OS X 10.9+. Код из данной статьи не будет работать на Windows XP и Vista, а серверные версии Windows и OS X не тестировались.

Файлы кода для этого поста можно найти на GitHub.

Чтобы изучить код в действии, посмотрите это видео.

Навигационный меш


Давайте узнаем, как использовать встроенный генератор навигационных мешей Unity, который может сильно упростить поиск путей для ИИ-агентов. На ранних этапах Unity 5.x функция NavMesh стала доступна всем пользователям, в том числе и с лицензиями personal edition, хотя раньше это была функция только для Unity Pro. Перед релизом 2017.1 система была обновлена, чтобы обеспечить рабочий процесс на основе компонентов, но поскольку для него требуется дополнительный скачиваемый пакет, который на момент написания статьи доступен только в preview-версии, мы будем придерживаться стандартного рабочего процесса на основе сцен. Не волнуйтесь, концепции обоих подходов похожи, и когда готовая реализация наконец доберётся до 2017.x, значительных изменений быть не должно.

Подробнее о системе компонентов NavMesh в Unity можно узнать на GitHub.

Теперь мы изучим все возможности, которые нам может предложить эта система. Для поиска путей ИИ сцена должна быть представлена в определённом формате; на 2D-карте используется двухмерная сетка (массив) для поиска путей алгоритмом A*. ИИ-агенты должны знать, где находятся препятствия, особенно статические. Работа с избеганием коллизий между динамически движущимися объектами — это другой вопрос, обычно называемый steering behavior (управлением движением). В Unity есть встроенный инструмент для генерации NavMesh, представляющий сцену в контексте, удобном для поиска ИИ-агентами оптимального пути к цели. Чтобы приступить к работе, откройте демо-проект и перейдите к сцене NavMesh.

Изучение карты


После открытия демо-сцены NavMesh она должна выглядеть, как на скриншоте:


Сцена с препятствиями и склонами

Это будет наша песочница для объяснения и тестирования функционала системы NavMesh. Общая схема похожа на игру в жанре RTS (real-time strategy). Мы управляем синим танком. Нажимайте на разные точки, чтобы танк двигался к ним. Жёлтый индикатор — это текущая цель танка.

Navigation Static


Во-первых, нужно сказать, что следует пометить всю геометрию в сцене, запекаемую в NavMesh, как Navigation Static. Вы уже могли встречаться с этим раньше, например, в системе карт освещения Unity. Сделать игровые объекты статическими очень просто, достаточно поставить флажок Static для всех их свойств (navigation, lighting, culling, batching и так далее), или воспользоваться раскрывающимся списком для конкретного указания свойств. Флажок находится в правом верхнем углу инспектора выбранных объектов.


Свойство Navigation Static

Это можно делать по отдельности для разных объектов или, если у вас есть встроенная иерархия игровых объектов, применить параметр к родительскому объекту, после чего Unity предложит применить его ко всем дочерним.

Запекание навигационного меша


Для всей сцены параметры навигации navmesh применяются с помощью окна Navigation. Это окно можно открыть, перейдя в панели меню к Window|Navigation. Как и любое другое окно, его можно отсоединить для свободного перемещения или закрепить. На наших скриншотах оно показано как закреплённая вкладка рядом с иерархией, но вы можете поместить это окно в любое удобное место.

Открыв окно, вы увидите отдельные вкладки. Это будет выглядеть примерно так:


Окно Navigation

В нашем случае на предыдущем скриншоте показана вкладка Bake, но в вашем редакторе может быть по умолчанию выбрана любая вкладка.

Давайте посмотрим на каждую из вкладок, начиная слева и двигаясь направо. Начнём со вкладки Agents, которая выглядит, как показано на скриншоте:


Вкладка Agents

Если вы работаете над другим проектом, то можете обнаружить, что некоторые из настроек отличаются от тех, которые мы задали для примера проекта, показанного на скриншоте. В верхней части вкладки есть список, в который можно добавить новые типы агентов, нажав на кнопку +. Удалить дополнительных агентов можно, выбрав их и нажав на кнопку -. В окне в наглядной форме показано, что делают различные настройки при их изменении. Давайте посмотрим, что делает каждая из настроек:

  • Name: название типа агента, отображаемое в раскрывающемся списке Agent Types.
  • Radius: можно воспринимать его как «личное пространство» агента. Агенты будут стараться избегать слишком близкого контакта с другими агентами на основании этого значения, потому что оно используется при избегании.
  • Height: как можно догадаться, эта настройка задаёт высоту агента, которую он использует для вертикального избегания (например, при прохождении под объектами).
  • Step Height: это значение определяет, на препятствия какой высоты может взобраться агент.
  • Max Slope: как мы увидим из следующего раздела, это значение определяет максимальный угол, на который может взбираться агент. С помощью этого параметра можно сделать крутые склоны карты недоступными для агента.

Далее у нас идёт вкладка Areas, которая выглядит, как показано на этом скриншоте:


Как видно на скриншоте, Unity предоставляет несколько типов областей, которые невозможно изменять: Walkable, Not Walkable и Jump. В дополнение к присвоению названий и созданию новых областей можно назначать этим областям стоимость перемещения по ним.

Области (Areas) служат двум целям: делают области доступными или недоступными для агента, а также помечают области как менее желательные с точки зрения затрат на перемещение. Например, вы можете разрабатывать RPG, в которой враги-демоны не могут заходить в области, помеченные как «освящённая земля». Также можно пометить некоторые области карты как «трясина» или «болото», которых агент будет избегать из-за высокой стоимости перемещения.

Третья вкладка Bake — наверно, самая важная. Она позволяет создавать сам NavMesh для сцены. Вам уже должны быть знакомы некоторые из параметров. Вкладка Bake выглядит так:


Вкладка Bake

Параметры размера агента на этой вкладке определяют, как агенты будут взаимодействовать со средой, в то время как параметры во вкладке Agents управляют взаимодействием с другими агентами и подвижными объектами. Но они управляют одинаковыми параметрами, поэтому мы пропустим их. Drop Height и Jump Distance управляют тем, насколько далеко агент может «запрыгнуть», чтобы достичь части NavMesh, которая не связан напрямую с той, в которой в данный момент находится агент. Подробнее мы рассмотрим это ниже, поэтому если вы не уверены, то пока можете не изучать эти параметры.

Кроме того, есть расширенные параметры, которые обычно по умолчанию скрыты. Чтобы развернуть эти опции, просто нажмите на треугольник раскрывающегося списка рядом с заголовком Advanced. Параметр Manual Voxel Size можно воспринимать как настройку «качества». Чем меньше размер, тем больше деталей будет сохранено в меше. Min Region Area используется для пропуска запекания платформ или поверхностей ниже выбранного порога. Height Mesh даёт нам более подробные вертикальные данные при запекании меша. Например, этот параметр позволяет сохранить правильное расположение агента при поднимании по лестницам.

Кнопка Clear удаляет все данные NavMesh сцены, а кнопка Bake создаёт меш для сцены. Процесс запекания довольно быстр. Пока у вас выбрано окно, вы можете наблюдать за генерированием NavMesh кнопкой Bake в окне сцены. Давайте нажмём на кнопку Bake, чтобы посмотреть на результаты. В нашем примере сцены мы в результате получим нечто, похожее на такой скриншот:


Голубыми областями показан NavMesh. Ниже мы вернёмся к этому. А пока давайте перейдём к последней вкладке — Object, которая выглядит так:


Три показанные на предыдущем скриншоте кнопки — All, Mesh Renderers и Terrains — используются как фильтры сцены. Они полезны при работе в сложных сценах со множеством объектов в иерархии. Выбор опции отфильтровывает соответствующий тип из иерархии, что упрощает их выбор. Можно использовать кнопки при исследовании своей сцены в поисках объектов, которые нужно пометить как navigation static.

Использование Nav Mesh Agent


Теперь, когда мы настроили сцену с NavMesh, нам нужен способ, которым агент сможет использовать эту информацию. К счастью для нас, в Unity есть компонент Nav Mesh Agent, который можно перетащить на персонажа. В нашем примере сцены есть игровой объект с названием Tank, к которому уже прикреплён компонент. Посмотрите на иерархию, и вы увидите нечто подобное:


Здесь довольно много параметров, и мы не будем рассматривать все, потому что они довольно понятны, а описание можно найти в официальной документации Unity. Но мы упомянем основные вещи:

  • Agent Type: помните вкладку Agents в окне Navigation? Назначаемые типы агентов можно выбрать здесь.
  • Auto Traverse Off Mesh Link: этот параметр позволяет агентам автоматически использовать функцию Off Mesh Links, которую мы рассмотрим ниже.
  • Area Mask: здесь можно выбрать области, настроенные во вкладке Areas окна Navigation.

Вот и всё. Этот компонент выполняет за нас 90% трудной работы: прокладывание пути, избегание препятствий и так далее. Единственное, что нужно — передать агенту целевую точку. Давайте рассмотрим эту задачу.

Задание целевой точки


После настройки ИИ-агента нам нужен способ сообщить ему, куда двигаться. В нашем примере проекта есть скрипт с названием Target.cs, выполняющий именно эту задачу.

Это простой класс, выполняющий три действия:

  • «Выстреливает» луч из камеры к позиции мыши в мире
  • Обновляет позицию маркера
  • Обновляет свойство точки назначения для всех агентов NavMesh

Код достаточно прост. Весь класс выглядит следующим образом:

using UnityEngine;
using UnityEngine.AI;

public class Target : MonoBehaviour
{
    private NavMeshAgent[] navAgents;
    public Transform targetMarker;

    private void Start ()
    {
      navAgents = FindObjectsOfType(typeof(NavMeshAgent)) as NavMeshAgent[];
    }

    private void UpdateTargets ( Vector3 targetPosition )
    {
      foreach(NavMeshAgent agent in navAgents) 
      {
        agent.destination = targetPosition;
      }
    }

    private void Update ()
    {
        if(GetInput()) 
        {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hitInfo;

            if (Physics.Raycast(ray.origin, ray.direction, out hitInfo)) 
            {
                Vector3 targetPosition = hitInfo.point;
                UpdateTargets(targetPosition);
                targetMarker.position = targetPosition;
            }
        }
    }

    private bool GetInput() 
    {
        if (Input.GetMouseButtonDown(0)) 
        {
            return true;
        }
        return false;
    }

    private void OnDrawGizmos() 
    {
        Debug.DrawLine(targetMarker.position, targetMarker.position + Vector3.up * 5, Color.red);
    }
}

Здесь происходят следующие действия: в методе Start мы инициализируем массив navAgents с помощью метода FindObjectsOfType().

Метод UpdateTargets() проходит по нашему массиву navAgents и задаёт для них целевую точку в заданном Vector3. Это ключ к работе кода. Для получения целевой точки вы можете использовать любой механизм, и чтобы агент переместился туда, достаточно задать поле NavMeshAgent.destination; всё остальное сделает агент.

В нашем примере для перемещения используются клики, поэтому когда игрок нажимает мышью, мы выпускаем луч из камеры в мир по направлению курсора мыши, и если он с чем-то пересечётся, то мы назначаем точку столкновения новой targetPosition агента. Также мы соответствующим образом настраиваем маркер цели для простой визуализации точки назначения в игре.

Чтобы протестировать работу, необходимо запечь NavMesh в соответствии с описанием из предыдущего раздела, затем запустить режим Play и выбрать любую область на карте. Если пощёлкать много раз, то можно заметить, что некоторых областей агент достичь не может — вершин красных кубов, самой верхней платформы и платформы внизу экрана.

Красные кубы находятся слишком высоко. Наклон, ведущий к самой верхней платформе, слишком резок для наших настроек Max Slope, и агент не может взобраться на него. На следующих скриншотах показано, как настройки Max Slope влияют на NavMesh:


NavMesh со значением max slope = 45

Если изменить значение Max Slope на что-то типа 51, а затем снова нажать на кнопку Bake, чтобы заново запечь NavMesh, то результаты будут такими:


NavMesh со значением max slope = 51

Как видите, мы можем настраивать дизайн уровней, делая целые области недоступными с помощью изменения единственного параметра. Это может быть полезно, например, когда у вас есть платформа или выступ, для подъёма на которые необходимы верёвка, лестница или лифт. А может быть, особый навык, например, способность лазать?

Применение Off Mesh Links


Вы можете заметить, что на нашей сцене есть два разрыва. В первый наш агент может попасть, но находящийся в нижней части экрана находится слишком далеко. Эти расчёты не полностью произвольны. Off Mesh Links по сути создают «мост» через пробелы между несвязанными сегментами NavMesh. Эти связи можно увидеть в редакторе:


Синие круги с соединительными линиями — это связи.

Unity может генерировать эти связи двумя способами. Первый мы уже рассмотрели. Помните значение Jump Distance во вкладке Bake окна Navigation? Unity автоматически использует то значение для генерирования этих связей при запекании NavMesh. Попробуйте изменить значение в нашей тестовой сцене на 5 и выполнить повторное запекание. Видите — платформы теперь связаны? Так произошло потому, что меши теперь находятся в пределах нового заданного порогового значения.

Снова измените значение на 2 и выполните запекание. Теперь давайте рассмотрим второй способ. Создайте сферы, которые будут использоваться для соединения двух платформ. Разместите их приблизительно так, как показано на скриншоте:


Вы уже можете увидеть, что происходит, но давайте разберём процесс, позволяющий их соединить. В нашем случае я назвал сферу справа start, а сферу слева end. Скоро вы поймёте, почему. Далее я добавил к платформе справа (относительно предыдущего скриншота) компонент Off Mesh Link. Вы заметите, что у компонента есть поля start и end. Как можно догадаться, мы перетащим созданные ранее сферы в соответствующие слоты — сферу start в поле start, а сферу end — в поле end. Инспектор будет выглядеть так:


Значение Cost Override учитывается, когда ему задаётся положительное значение. Оно применяет множитель затрат при использовании этой связи в противоположность более эффективного по стоимости маршрута к цели.

Bi Directional при значении true позволяет агенту двигаться в обоих направлениях. Для создания связей с односторонней проходимостью можно отключить это значение. Значение Activated используются в соответствии со своим названием. При значении false агент игнорирует эту связь. Можно включать и отключать его для создания игровых сценариев, в которых, например, игрок для активации связи должен нажать на переключатель.

Для включения этой связи повторное запекание выполнять не нужно. Посмотрите на свой NavMesh и вы увидите, что он выглядит точно так, как на скриншоте:


Как видите, меньший разрыв по-прежнему соединяется автоматически, и теперь у нас есть новая связь, сгенерированная компонентом Off Mesh Link между двумя сферами. Запустите режим Play и нажмите на дальнюю платформу. Как и ожидалось, агент теперь может перейти к отсоединённой платформе:


На уровнях вашей игры вам может понадобиться изменять эти параметры, чтобы добиться нужных результатов, но сочетание этих возможностей предоставляет вам удобный готовый инструмент. Вы можете довольно быстро создать простую игру с помощью функционала NavMesh.

Этот туториал является фрагментом книги Unity 2017 Game AI Programming — Third Edition, написанной Ray Barrera, Aung Sithu Kyaw и Thet Naing Swe.

Комментарии (1)


  1. Suvitruf
    19.06.2018 13:43

    Оно всё ещё работает только для относительно плоских поверхностей? Для сферы так и нельзя прикрутить?