Пишем ИИ для игры. Часть 1: Как найти противников в поле зрения
Всем привет. С сегодняшнего дня я решил начать написание цикла статей по работе с ИИ противника. В ходе данного цикла я хочу показать на простых примерах, как можно сделать ИИ в игре, который будет учитывать поле зрения противника, уровень издаваемого шума, умеет патрулировать местность, ставить приоритетные цели и обладать различными типами поведения.
В конце цикла статей наш ИИ будет уметь:
- Получать список целей по FOV;
- Искать путь до ближайшей цели, будь то враг или что-то другое;
- Обладать типом (враг, союзник, болванка);
- Обладать поведением (патруль, поиск цели, бой, следование, убегание);
- Учитывать уровень шума и освещенности;
- Работать с инверсной кинематикой (получать Impact конечностей, смотреть на врагов);
Для чего это нужно?
В сегодняшней статье я расскажу о простой реализации поля зрения противника. Дабы исключить архитектурные особенности - мы сделаем все на обычных монобехах.
Итак, начнем с того, что должно делать наше поле зрения:
- Находить цели, которые попадают в угол обзора;
- Ставить приоритет на ближайшую цель в области обзора;
- При выходе текущей цели из поля обзора - сохранять её до определенной дистанции;
- При полной потере цели - переключиться на другие;
Интерфейс Field of View
Начнем с того, что нам нужен некий интерфейс, который сможет просто обрабатывать наше поле зрения:
Рассмотрим, что содержит наш интерфейс:
- Параметры Radius, Angle - для того, чтобы получить возможность узнать информацию о FOV;
- Параметр CurrentTarget (в моем случае для простоты используется Transform, но лучше сделать интерфейс ITarget и работать с ним);
- Методы для проверки целей - HasTargets, GetAllTargets, GetNearestTarget и ForceRecalculate.
Почему здесь нет метода поиска целей, а сделан только ForceRecalculate? Мы делаем просчет внутри самого компонента FOV, а все его данные получаем через его методы обработки целей. ForceRecalculate нужен нам только тогда, когда к примеру текущая цель умерла и хочет оповестить наш объект об этом.
Базовая реализация FOV
Теперь приступим к самой реализации FOV. По своей сути он работает через оверлап коллайдера, однако вы можете использовать Raycast. Также у компонента FOV есть таймер пересчета целей и чем меньше он будет, тем больше будет нагрузки и тем выше точность поиска. Таймер полезен тогда, когда игрок непосредственно видит ИИ противника и для него выставляется наименьший таймер, а для противников в далеке - наибольший.
Теперь разберем подробнее составляющие кода:
- В методе Start() мы запускаем наш счетчик проверки FieldOfView через интервал. В моем случае используется UniRx, но вы можете сделать реализацию таймера по-другому.
- Метод FieldOfViewCheck() запускает процесс проверки целей внутри поля зрения. Если изначально у нас нет никаких целей - мы проверяем есть ли кто-то по нужному слою в физике, затем смотрим ближайшую цель и добавляем её в список. Если же цель есть - мы смотрим дистанцию до неё вне зависимости от поля зрения и если главная цель слишком далеко - пересчитываем снова список целей.
- Дополнительные методы GetAllTargets, GetNearestTarget, HasTargets и ForceRecalculate служат вспомогательными. Они могут использоваться в нашем контроллере ИИ.
Теперь мы научили нашего противника определять ближайшую цель в поле зрения:
Так же важно, что если цель покинет поле зрения, то герой останется на ней сфокусирован, пока её дистанция не увеличиться больше допустимой, в этом случае он опять будет переключаться на ближайшую цель в поле зрения. Так же нужно учесть, что пока здесь не реализовано поведение, которое учитывает урон от ближайших целей (о чем мы поговорим в следующих частях цикла).
Итог
Проверка поля зрения позволяет нам убедиться, что наш ИИ вообще видит кого-то перед собой, цель не перекрывается и задать приоритет по целям исходя из дистанции. В дальнейшем, мы будем комбинировать FieldOfView с показателями шума, издаваемого целями, а также задавать поведение для нашего ИИ.
Следующая часть статьи будет включать в себя обработку целей в зависимости от паттерна поведения нашего ИИ, а также же учитывать инверсную кинематику для того, чтобы наш герой поворачивал голову и туловище к ближайшей цели.
Буду рад пообщаться на эту тему и послушать о ваших реализациях FOV.
Lead Unity Developer
if (!Physics.Raycast(transform.position, directionToTarget, distanceToTarget,
_obstructionMask))
{
isSeeTarget = true;
}
else
isSeeTarget = false;
Ну серьезно?
isSeeTarget = !Physics.Raycast(transform.position, directionToTarget, distanceToTarget, _obstructionMask) же.
Там еще флаги изначально стояли, я их вырезал и забыл поменять
Интерфейс IFOV выполняет несколько задач. Там пробел в одну строку как раз там где следует его порезать на два разных интерфейса. Тот что является Fov реализацией и тот что отвечает за поиск целей в реализаций.
Есть версия overlap sphere non alloc. Текущаягенерит каждый раз новый лист, даже если он ре нужен. Еще, не уверен до конца, но вроде бы вызов этих методов следует в FixedUpdate засунуть, но это не точно.
IsSeeTarget просто не нужен - добавляй в список и все. Код чище будет.
Не нравится проверка на попадание в угол зрения - normalized - высчитывает корень квадратный, лучше бы этого избежать.
GetNearestTarget делает расчеты при каждом вызове. Следует хранить таргеты в отсортированном по дистанции виде и просто возвращать первый.
Со всем согласен, кроме GetNearestTarget. Позиция ближайшего может измениться на следующий вызов метода, так что его следует пересчитать. В целом это просто на понимание как это работает, код естественно написан за 5 минут без всякой оптимизации
Очень грубое решение задачи в лоб, но как пример для тех кто не делал ИИ никогда - сойдет. Такое можно использовать в первом приближении новичку понимая что это не оптимальный код и нужна оптимизация. Кстати делать какие либо проверки визуального контакта с игроком когда он на большем расстоянии чем может видеть ИИ не имеет смысла даже раз в минуту. В данном случае оверлап используется именно для определения того что игрок не спрятался и находится в поле зрения. Также скрипт который будут вешать на всех врагов должен использовать подсистему ИИ, которая и должна будет уже делать все самые сложные расчеты выбирая для них только те объекты которые могут встретится хотя бы теоретически. Но это уже способы оптимизации и они будут важны для конкретных случаев использования ИИ. Будем ждать продолжения. Спасибо за статью. Тем более тоже было желание подобное написать, но так и не определился для какой аудитории.
Рейкаст на проверку видимости кидается из пивота объекта, в данном случае из точки на земле (видно на скриншоте). Если персонаж будет стоять рядом с ящиком или забором, который ему по пояс, он "не увидит" врага, стоящего за ним, потому что рейкас из ног будет сразу упираться в препятствие.
Как вариант можно добавить чайлд-пустышку на уровне глаз персонажа и кидать рейкаст из него. При таком подходе пивот может быть где угодно и не повлияет на качество проверки рейкаста.
Вот почему все нужно дебажить, да.