December 3

Игровой ИИ. State Tree

В прошлый раз мы закончили обсуждать Goal Oriented Action Planning (GOAP), сегодня же мы обсудим комбинированные решения.Также не забывайте подписываться на мой канал, чтобы не пропустить следующие части.

FSM + Behaviour Tree

Один из самых простых подходов в сочетании разных решениях - это прямой гибрид Конечного Автомата и Дерева Поведения. Автомат принимает высокоуровневые решения: запускается боевое поведение или мирное, стоит NPC  убегать от игрока или разговаривать с ним. Каждое состояние Автомата реализовано как самостоятельное дерево поведения. Деревья поведения подробно на низком уровне реализуют каждые состояния. Такой подход позволяет не перегружать FSM переходами низкоуровневых поведений (такими как неудачная навигация или выбор способности для текущего состояния).

Насколько нам известно, Дерево Поведения чаще всего использует BlackBoard для хранения информации. Поскольку наши деревья в случае сочетания с Автоматом не должны покрывать все поведение нашего агента, то, следовательно, наши Blackboard должны хранить информацию только для текущего состояния Автомата, что упрощает его структуру  и облегчает работу с ним. Кроме того мы получаем возможность использовать шаблонные деревья. Например дерево ближнего боя может выглядеть как навигация и использование способности, в котором настройки навигации и способность храниться в Blackboard. Blackboard настраивается уже автоматом, в зависимости от того, какое поведение он описывает текущим состоянием.

Одна из проблем такого подхода заключается в том, что передача данных между Деревьями и Автоматами может быть нетривиальной. Например если дизайн вашего агента требует, чтобы агенту нужно было убегать от игрока, если у него нет подходящих предметов для активации. Но такой запрос про предметы может быть частью низкоуровневой логики, за которую отвечает Дерево поведений. Получается Дерево должно сообщить более высокоуровневой логике о своей неудаче. Такое направление потока данных в ИИ выглядит тяжелым для поддержки: в Автомате будут условия, которые скрыты где-то в дебрях Деревьев поведений, причем не обязательно,что всех.

Главным преимуществом совмещения FSM и Дерева Поведения является прирост производительности. Количество переходов в Автомате ограничено высокоуровневыми состояниями, и их частоту обновления можно понизить, так как такая задержка не будет заметна игроку. Дерево Поведений отвечает за одно состояние, соответственно количество декораторов и условий тоже ограничено, и Дерево работает быстрее.

Behaviour Tree + Utility AI

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

Дерево поведений может описывать абстрактное поведение с модулями и узлами, которые выбираются и настраиваются с помощью Utility AI. Например Дерево Поведения дальнего боя будет включать перемещение в точку для атаки и использование оружия дальнего боя, а  выбор точки и выбор оружия осуществляется с помощью полезности.

Такой ИИ отлично работает с дизайном агентов, у которых дизайн отличается способностями и предметами, но общее поведение, позиционирование и схожая логика. Например РПГ, где разнообразие ИИ кроется именно в сборке или инвентаре противников, а не в их поведении.

GOAP + FSM/BehaviorTree

Основным подходом к работе с GOAP является реализация конкретных действий в плане с помощью других моделей поведений. Такой подход использовался при создании GOAP в F.E.A.R.: каждое действие реализовывалось через Конечный Автомат, состоящий из 3 действий: передвижение, использование smart object и  проигрывание анимации. А куда перемещаться, что за объект использовать, и какую анимацию играть, определялось типом действием и задавалось в настройках действий.

Такой подход позволяет убрать из планов тривиальные действия и сочетания и сфокусироваться на высокоуровневых поведениях. Также  это сокращает размер планов, а следовательно и размер пространства, в котором мы ищем план.

StateTree

Один из новых и реализованных гибридов двух моделей - это StateTree в Unreal Engine 5. Подход StateTree - это попытка получить бонусы и удобства контролируемых переходов в разные состояния с понятной системой последовательностей и селекторов.

Schema и Context

State Tree является абстрактной моделью, и, хотя естественное его использование - это ИИ, сама структура состояний и переходов может быть использована для чего угодно: от низкоуровневой системы комбо до высокоуровневой системы миссий и заданий. Поэтому в основе state tree, в отличие от дерева поведения, лежит идея, что state tree может запускаться на любом объекте и с любым контекстом. Для того чтобы код понимал, с чем он работает и какие структуры обязательные для данного дерева, существует schema

UStateTreeAIComponentSchema::UStateTreeAIComponentSchema(const FObjectInitializer& ObjectInitializer /*= FObjectInitializer::Get()*/)
	: AIControllerClass(AAIController::StaticClass())
{
	check(ContextDataDescs.Num() == 1 && ContextDataDescs[0].Struct == AActor::StaticClass());
	// Make the Actor a pawn by default so it binds to the controlled pawn instead of the AIController.
	ContextActorClass = APawn::StaticClass();
	ContextDataDescs[0].Struct = ContextActorClass.Get();
	ContextDataDescs.Emplace(UE::GameplayStateTree::Private::Name_AIController, AIControllerClass.Get(), FGuid(0xEDB3CD97, 0x95F94E0A, 0xBD15207B, 0x98645CDC));
}

Схема содержит набор полей и правил, с помощью которых остальные части state tree могут получить необходимую информации и доступ к нужным объектам.

Для запуска дерева поведения и его обновления необходимо использовать контекст и внешней контролер (которым может быть компонент, актор или подсистема).

FStateTreeExecutionContext Context(*GetOwner(), *StateTreeRef.GetStateTree(), InstanceData);
if (SetContextRequirements(Context))
{
    const EStateTreeRunStatus PreviousRunStatus = Context.GetStateTreeRunStatus();
    const EStateTreeRunStatus CurrentRunStatus =  Context.Tick(DeltaTime);

    if (CurrentRunStatus != PreviousRunStatus)
    {
        OnStateTreeRunStatusChanged.Broadcast(CurrentRunStatus);
    }
}

Именно контролер и схема заземляют абстрактное дерево до конкретной реализации. В дальнейшем мы будем рассматривать State Tree в контексте ИИ и brain component Unreal Engine. Но зона его применения намного шире.

State (Состояние)

Можно догадаться из названия, что основой StateTree является состояние (state). Состояние совмещает в себе сразу несколько моделей. Первое - это аналог состояния из конечного автомата: структура, у которой есть понятие входа/старта и выхода/завершения. Она описывает некоторое поведение и выполняет некоторую логику, также может иметь условия и правила выполнения или входа.

Вторая модель - это управляющий узел из дерева поведения: структура обладает набором потомков, которые она запускает и обрабатывает в соответствии с конкретным набором правил.

Например мы можем указать состоянию правила TrySelectChildrenInOrder

, для того чтобы наше состояние вело себя как селектор, либо как селектор с выбором по весам(кусочек utility based ai).

Transition (Переход)

В отличие от конечного автомата, состояния в State Tree чаще обладают циклом “выполняю  -> завершил (успешно или нет)” , что делает их схожими с конечными узлами деревьев поведения. После завершения же состояние обладает свойствами уже конечного автомата: вы может свободно определить, куда переходит дерево по завершению конкретного действия. Можно совершить переход в соседнее состояние, либо прыгнуть в другое часть дерева, либо завершить дерево, либо перезапустить его. Также такие переходы можно совершать в любой момент времени, как и в конечном автомате: по событию или во время тика по условию. Кроме того сохраняется иерархическая структура дерева: если переход совершает родительское состояние, то все его потомки завершаются автоматически.

Такой подход кажется сложным, но на практике позволяет легко реализовывать сложные и банальные вещи. Например у вас может быть сложное дерево мирного поведения с кучей состояний правил, но вам надо будет создать одно условия для перехода из него в боевое. Аналогично сложное дерево комбо может по команде от конкретного состояния совершить переход в другую ветку, и такую проверку не надо настраивать вне этого состояния. Например у вас может быть сложное дерево боя и ветка комбо-ударов: в момент апперкота вы можете совершить переход в другую ветку, и за этот переход отвечает только состояние апперкота, в другие моменты никаких проверок или переходов совершать не надо. Переходы настраиваются в состояниях и могут обладать набором условий.

Condition (Условия)

Условия в State Tree встречаются в переходах из состояний и в самих состояниях при старте состояния. Условия при входе позволяют реализовать модель селектор из дерева последней: потомки конкретного состояния обладают набором условий, и он будет перебирать их при входе пока её найдет то, у которого условие выполнено.

Условия обычно наследуются от FStateTreeConditionBase. Однако, если в вашем проекте несколько StateTree, которые используются с разными схемами, может быть удобно создать промежуточные базовые классы и указать в схеме, какие классы условий можно использовать в какой схеме.

bool UStateTreeAIComponentSchema::IsStructAllowed(const UScriptStruct* InScriptStruct) const
{
	return Super::IsStructAllowed(InScriptStruct)
		|| InScriptStruct->IsChildOf(FStateTreeAITaskBase::StaticStruct())
		|| InScriptStruct->IsChildOf(FStateTreeAIConditionBase::StaticStruct());
}

Условия можно комбинировать по правилам булевых операций. Для этого в unreal Engine 5.6 есть интерфейс создания условий. Конечно же можно создавать blueprint условия.

Пример комбинированого условия

Для удобства рекомендую всегда реализовывать функцию FText FStateTreeNodeBase::GetDescription, чтобы ваше дерево было читаемым и удобным.

Удобное описание условия

Task (задачи)

Задачи являются той частью состояния, которая позволяет им выполнять логику. Задача - это кирпичики, из которых собирается поведение в State Tree. Задачи могут простыми (например навигация в пространстве), и сложными действиями (например проиграть последовательность атак). В принципе, как и в дереве поведения, задачи в StateTree лучше делать атомарными и простыми. В каждом состоянии может быть сколько угодно задач.Состояние определяет само, когда оно должно завершиться, с помощью настройки (по окончанию любой задачи или всех).

Задачи наследуются от класса FStateTreeTaskBase, но, по аналогии с условиями, удобно будет завести промежуточный базовый класс, который вы разрешите использовать вашей схеме.

Пример состояние с нексколькими задачами

Бесконечные задачи могут выполнять роль сервисов и декораторов из дерева поведения. Такую задачу можно добавить в любое состояние, либо добавить в список глобальных задач всего дерева. Глобальные задачи чаще всего собирают и анализируют состояние игрового мира и агента и сводят его к простым флагам или полям, которые в свою очередь могут использоваться остальными состояниями.

Data Flow

При работе и проектировании StateTree важно сразу понять как работает видимость данных в дереве. Данные доступны иерархически, т.е. любая задача или условие может получить данные от всех задач текущего состояния и его предка в иерархии. Такой подход позволяет организовать удобную передачу данных: задача родительского состояния выполняет запрос, например, к систему EQ, получает цель для перемещения, а потомки состояния запускают задачи навигации используя результаты задачи родительского состояния.

В следующий раз мы разберемся какие переменные и функции доступны для State Tree и попробуем собрать простой боевой ИИ.