Игровой ИИ. Behavior Tree.
Продолжаю мой цикл про игровой ИИ. В предыдущей части мы разобрались с вложенными деревьями и начали собирать простой ИИ. В этой части передадим внешние данные в нашу ИИ и соберем простое боевое поведение. Также не забывайте подписываться на мой канал, чтобы не пропустить следующие части.
Передача данных.
Напомню, что в нашей модели ИИ присутствует четкое архитектурное разделение на Пешку, которая взаимодействует с миром игры и видима игроком, и Контроллер, который отвечает за поведение. Сам контроллер в свою очередь разделен на несколько модулей, одним из которых является модуль принятия решений и выбора поведения. В данном случае таким модулем выступает дерево поведения. Более того, только этот модуль отвечает за выбор поведения, в то время как остальные модули выступают в роли сервисов, обеспечивающих корректную работу ИИ.
Существуют два направления передачи данных или событий: из модуля решений и в модуль решений.
Исходящие данные
Случай, когда модулю поведения нужно передать данные в другой модуль ИИ или в Пешку, является самым простым и понятным. Так как модуль поведения находится наверху иерархии нашего ИИ, он может отправлять запросы непосредственно в необходимые модули. Это не отменяет правил инкапсуляции и абстракции. Скорее всего, ИИ не должно знать как конкретно реализована пешка или модуль поиска пути, но модуль поведения должен иметь представление о структуре Пешки и других модулей контроллера. Такой подход реализуется обычно через интерфейсы или базовые классы. Реализация через события и подписчиков (publisher-subscriber pattern), на мой взгляд, не оптимальна, ибо ограничивает модуль поведения в понимании выполняется ли его запрос и существует ли система, на которую он рассчитывает.
Существует ситуация, когда модуль не может ответить сразу же: например при навигации, построении пути или при совершении продолжительного действия. Как поступать модулю поведения зависит от конкретной модели ИИ и от конкретной имплементации. Дерево поведения, например, останавливает обход дерева и возвращает статус Выполнятся (Running). Когда действие будет закончено, статус узла меняется на статус, соответствующий результату действия. Например, для узла навигации достижение пункта назначения соответствует статусу Успех (Complete), если путь не найден или возникли проблемы при навигации, то даётся статус Провал (Failed). От реализации и типа систем зависит то, как ИИ узнает о завершении запрашиваемого действия. Это может быть callback либо событие, в зависимости от системы, с которой взаимодействует ИИ
В дереве поведения реализация запроса к внешним системам выполняется через сервис или узел действия (если необходимо совершить некоторое действие).
Примером такого узла действия (или task в Unreal Engine 5) служит UBTTask_MoveTo
const FPathFollowingRequestResult RequestResult = MyController->MoveTo(MoveReq);
if (RequestResult.Code == EPathFollowingRequestResult::RequestSuccessful)
{
MyMemory->MoveRequestID = RequestResult.MoveId;
WaitForMessage(OwnerComp, UBrainComponent::AIMessage_MoveFinished, RequestResult.MoveId);
WaitForMessage(OwnerComp, UBrainComponent::AIMessage_RepathFailed);
NodeResult = EBTNodeResult::InProgress;
}
else if (RequestResult.Code == EPathFollowingRequestResult::AlreadyAtGoal)
{
NodeResult = EBTNodeResult::Succeeded;
}
Как видно из приведенного кода, мы обращаемся к контролеру за запросом на движение, после чего мы анализируем результат запроса и либо сразу завершаем выполнение узла, либо подписываемся на событие от ИИ контролера о завершении навигации. В данном случае обращение к другим системам выполняется через прямой вызов функции на базовом классе AAIController, а сообщение о завершении использует абстрактную систему сообщений UBrainComponent.
Входящие данные
Любому дереву, кроме совсем тривиальных, необходимо анализировать состояние как минимум своей пешки, а чаще всего и состояние игрового мира.
Основным подходом, конечно, является опрос модулей, компонентов и пешки, описанный в предыдущем подходе. По своей структуре он очень схож с запросом действия. Модуль поведения запрашивает данные из другого модуля или системы и ожидает (или сразу получает) необходимые данные.
Простым примером может служить декоратор BTDecorator_CheckGameplayTagsOnActor, который позволяет проверить, есть ли на определенном акторе игровой тэг.
IGameplayTagAssetInterface* GameplayTagAssetInterface = Cast<IGameplayTagAssetInterface>(BlackboardComp->GetValue<UBlackboardKeyType_Object>(ActorToCheck.GetSelectedKeyID()));
if (GameplayTagAssetInterface == NULL)
{
return false;
}
switch (TagsToMatch)
{
case EGameplayContainerMatchType::All:
return GameplayTagAssetInterface->HasAllMatchingGameplayTags(GameplayTags);
case EGameplayContainerMatchType::Any:
return GameplayTagAssetInterface->HasAnyMatchingGameplayTags(GameplayTags);
default:
{
UE_LOG(LogBehaviorTree, Warning, TEXT("Invalid value for TagsToMatch (EGameplayContainerMatchType) %d. Should only be Any or All."), static_cast<int32>(TagsToMatch));
return false;
}
}
В данном случае мы используем Интерфейс, чтобы получить и проверить теги и вернуть результат.
Альтернативным подходом может служить взаимодействия с Blackboard. Напомню, что Blackboard абстрактная память в формате ключ-значение.Внешняя система может записать что-то в Blackboard, затем модуль поведения может проанализировать эти данные и принять решение о выборе поведения. Такой подход надо использовать осторожно, чтобы не создавать странные архитектурные зависимости. Плюс, скорее всего, он должен происходить в рамках ИИ контроллера, который например в Unreal Engine 5 является владельцем Blackboard, что позволяет взаимодействовать с памятью, не нарушая инкапсуляцию модулей поведения.
Простой Тестовый ИИ
В качестве примера попробуем реализовать тестового противника, описанного ранее (https://teletype.in/@jazzyjohn/azNSiLQyzHX#hnTQ).
Поиск целей
Для начала нам нужно реализовать детектирование противника. Для этого мы будем использовать Perception систему Unreal Engine 5. В рамках нашего архитектурного подхода давай создадим сервис, который анализирует список всех замеченных Акторов и находит оптимальную цель для боя. Поскольку поиск цели осуществляется в сервисе, контроль за тем, когда и как ищется цель, будет выполняться в дереве поведения.
N.B. В этой статье нас не сильно интересует сам сервис и логика его выполнения, нас интересует взаимодействие с разными системами и игровыми объектами. Поэтому я позволяю писать простые и не оптимизированные блюпринты, которые легко поместить на скриншот и в которых легко разобраться. В реальном проекте логика выбора целей очевидно должна быть вынесена в data driven компоненту, и сервис должен обращаться уже непосредственно к этому компоненту.
Наш сервис выбирает ближайшую цель и записывает её в блюпринт. Давайте дополним дерево из прошлой статьи этим сервисом, также заведем дополнительное поле в нашем Blackboard. Кроме того, нам необходимо добавить Perception System Component нашему контролеру.
Следующий этап - добавить ветку для боевого поведения. Наш простой ИИ не будет переключаться между целями, поэтому сервис поиска целей будет выполняться только в части мирного поведения. Для переключения в другую ветку будем использовать декоратор, который проверяет на валидность поле в Blackboard. Поскольку мы хотим, чтобы ИИ прерывал свое мирное поведение, мы выставляем в настройках декоратора Observer Aborts = Both. Эта настройка отвечает за то, что при смене состояния декоратора, он прервет либо ветку на которой находится (если условие перестало выполняться), либо все низкоприоритетные ветки, если условие стало верным.
Атака дальнего боя
Теперь начнем реализовывать боевое поведение. Для начала начнем с атаки дальнего боя. Как говорилось раньше, для реализации атак в Пешке мы будем использовать AbilitySystem. Мы не будем останавливаться на настройках системы, но что нам важно - что у пешки есть способность и тег, который висит на пешке, пока способность активна. С учетом этого мы можем реализовать простой узел действия, который будет запускать выбранную способность.
Полученный узел достаточно абстрактен, все что у него есть - несколько тегов для запуска и ожидания завершения способности, и ключ blackboard, который является целью способностьи. Его логике известно о системах, которые присутствуют у пешки, но практически нет конкретики, поэтому его можно переиспользовать для разных целей, что мы и сделаем в дальнейшем.
Добавим к новому узлу уже известную нам логику перемещения, поменяв в ней параметры, плюс добавим базовый сервис Unreal Engine 5, который выставляет фокус на нашу цель. Это позволяет ИИ поворачиваться к игроку во время стрельбы.
Ближний бой.
Теперь наш ИИ бегает и стреляет в игрока, но у нас было еще одно боевое поведение, которое мы не реализовали - это атака ближнего боя. Давайте добавим атаку, которая будет проигрывать пешка в ситуации, когда игрок оказался очень близко. Для этого нам потребуется два декоратора. Первый, который будет проверять расстояние между акторами.
N.B. Дерево поведений собирается в блюпринтах, но относиться к нему надо как к коду, а не контенту. Константы и числа лучше выносить в Blackboard или в таблицы параметров. В моих примерах для некоторых узлов сделаны исключения (например, для узлов ожидания, так как в них нет возможности вынести параметры их узла, и они существуют в движке по умолчанию). Для остальных узлов константы и цифры обладают теми же негативными факторами что и цифры в коде. Поэтому для нашей ноды расстояния я создал переменную в Blackboard.
Второй декоратор, чисто утилитарный и служит заменой базового BTDecorator_CheckGameplayTagsOnActor. К сожалению, базовому декоратору требуется, чтобы передаваемый Актор имплементировал интерфейс IGameplayTagAssetInterface. Поскольку мой тестовый проект целиком на блюпринтах, имплементировать этот интрефейс нашей пешке не получится, поэтому я завел простой аналог. Этот декоратор понадобится нам, чтобы убедиться, что атака ближнего боя не прерывает атаку дальнего боя.
Собрав уже существующие узлы и декораторы, получим финальное дерево.Стоит обратить внимание, что у декоратора расстояния выставлено Observer Aborts = Lower Priority, что позволяет ему прерывать менее приоритетные ветки, но не прерывать способность ближнего боя. Декоратор тегов не прерывает никого.
Весь тестовый проект который мы собирали доступен на моем gitverse https://gitverse.ru/JazzyJohn/AILessons
На этом наше изучение дерева поведения подошло к концу. Дерево поведения является очень сильной и удобной моделью реализации модуля поведения, оно способно при правильном использовании реализовать ИИ любой сложности и, при грамотном подходе, показывать хорошую производительность и отзывчивость ИИ.
В следующий раз мы рассмотрим ИИ, основанное на полезности (utility ai), модель, которую мы уже использовали в EQS для наших деревьев поведений. Рассмотрим как её можно использовать для выбора поведений, и в качестве поддерживающего инструмента остальных моделей поведения.