Управляемый данными фреймворк искусственного интеллекта в игровом мире

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

В этой статье мы обсудим управляемую данными архитектуру ИИ, построенную для игры Destroy All Humans 2 студии Pandemic. Архитектура представляет собой фрэймворк, описывающий управляемое данными поведение персонажа, а также то, как создаются, взаимодействуют и применяются к нему различные поведения.

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

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

Основы поведения: Разбиваем задачи на подзадачи.

Основу системы поведений в игре Destroy All Humans 2 составляет иерархический конечный автомат (ИКА), в котором текущее состояние персонажа определяется несколькими уровнями абстракции. На каждом уровне этой лестницы одни состояния могут использовать другие состояния (обычно, уровнем ниже). Тем самым задача разбивается на более мелкие (например, абстрактное состояние «атаковатьврагов» использует более конкретное «стрелятьизоружия» как часть своей функции). Структура ИКА — это стандартный метод ИИ, определяющий поведения персонажа в игре. Система обладает некоторыми очевидными преимуществами перед традиционным конечным автоматом (КА)

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

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

Потомки с приоритетом. Первый и самый частый способ запуска потомка — это список поведений с приоритетом. Поведения, запускаемые с приоритетом, создаются сразу (для них выделяется память и добавляются они в качестве потомков родителя) и устанавливаются в специальный режим ожидания.

РИС. 1. Поведение «бой» запускает потомков с приоритетом, которые, в свою очередь, делают тоже самое. Поведения в режиме ожидания — оранжевые, активные — синие.

В режиме ожидания модель поведения не обновляется. Опираясь на собственные настройки, она сама решает, когда запуститься, и лишь затем запускает одного из своих потомков. Только активное поведение может иметь потомков, поэтому поведение в режиме ожидания не запустит потомка раньше времени.

Как правило, только одно активное поведение может быть запущено своим непосредственным родителем, что создает следующую проблему: как быть в ситуации, когда могут быть запущены несколько поведений одновременно. Для определения важности подзадач расставим приоритеты. Запуская потомка в качестве приоритетного, мы неявно определяем приоритет, основываясь на порядке, в котором поведения были добавлены к родителю. Чем раньше поведение попало в список, тем выше его приоритет.

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

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

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

С таким повышением приоритеты могут быть определены подробнее, нежели простым порядком в списке. Например, несмотря на то, что поведение рукопашная имеет низший приоритет перед уклонением (рис. 1), оно должно иметь возможность завершить свою анимацию, даже если уклонение решило запуститься. Назначив повышение приоритета, мы не позволим уклонению оборвать анимацию рукопашного боя на середине.

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

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

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

Собираем мозаику

В данном фреймворке мы определили основную единицу, из которой будут состоять ИКА: поведение. Сделав так, чтобы поведения управлялись данными, мы получаем доступ не только к их параметрам и настройкам, но также и к самой структуре действий, которые они выполняют.

РИС. 2. Когда оба предусловия данного поведения удовлетворены, оно активируется, передавая побуждающий параметр своим двум потомкам. Потомки разделены по приоритетам, поэтому обладающий высшим приоритетом «бег» запустится при первой возможности, позволив запуститься «приседанию» только в случае собственной неудачи.

Каждое поведение самодостаточно. Всё необходимое уже определено внутри него: когда оно может быть активировано, когда больше не может выполняться, чем может быть прервано, что делает до и после активации и обновления. Но, самое значимое — каких потомков оно запускает. Каждый из перечисленных параметров описан в файле .behavior, по файлу на каждое поведение, который затем парсится и считывается вместе с остальными игровыми данными.

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

Установки, определяющие предусловие, хранятся в отдельном файле и могут быть использованы другим поведением. Это позволяет нам создать библиотеку предусловий и с легкостью подключать их к новому поведению, просто перечисляя их в файле конфигурации поведения.

Когда поведение активировано, оно выполняет свое назначение, которое в большинстве случаев также определено данными. Любое поведение может привести в действие персонаж (проиграть анимацию, звук или включить эффект) или запустить потомков без необходимости вносить изменения в код. Это дает нам гибкость в конкретизации новых поведений.

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

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

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

Однако, объекты, с которыми мы работаем, не определены до начала игры (например, текущая цель персонажа во время боя или последний враг, ранивший игрока), поэтому нам необходим способ принимать решения по объектам внутри поведений. Нам нужна система параметров.

Соединяем поведения с объектами

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

Для решения этой задачи в Destroy All Humans 2 мы разрешили предкам передавать параметры вновь созданным потомкам. Это позволило поведениям манипулировать объектами и делать запросы к ним, которые передавались в качестве параметров потомкам.

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

Такое дополнение позволяет нашему ИКА передавать объекты из одной точки в другую (в нашем случае это предок -> потомок). При этом объекты подключаются к другим поведениям, которые реализуют свой функционал на данных объектах. Таким способом мы подмешиваем немного скриптовой функциональности в цельную систему ИКА, не жертвуя структурой.

Помимо конкретных и абстрактных, существуют также частично реализованные поведения. Возможность передавать параметры открывает значительное количество способов использования поведений. И в нашей реализации использование параметров стало обычным делом. Чтобы облегчить нам задачи, в которых могут быть использованы параметры, мы создали несколько частично реализованных поведений. Они решали некоторые стандартные задачи с помощью кода. Следует сказать, что сами они не принадлежат к древу ИКА. Эти вспомогательные поведения остаются абстрактными до тех пор, пока не получают данные для конфигурации своих действий.

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

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

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

Поведение защиты

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

Далее защищающий персонаж должен вступить в бой с ближайшими врагами, которые в нашей реализации обнаруживаются с помощью запросов к модулю поиска целей, запущенного на персонаже. Подобные модули работают отдельно от системы поведений. Для выполнения задачи запускается бой (ещё одно поведение-дальномер), и если враг слишком близко, поведение активируется и запускает потомков. Если ни сближение, ни бой не могут быть запущены (в случае если их пред-условия не соблюдены), по умолчанию будет запущено блуждание, как обладающее низшим приоритетом и не имеющее пред-условий.

Далее персонаж будет хаотично патрулировать вокруг защищаемого объекта.

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

Совместное и повторное использование поведений

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

Каждое поведение в своей системе (за исключением простейших) имеет установки, определенные в собственном файле .behavior. Однако создавая своих потомков, поведение не обращается к их файлам напрямую по имени. Вместо этого оно использует псевдоним. Любой персонаж, запуская поведение, создает список псевдонимов и привязывает их к файлам .behavior. Абстрагируя ссылку на поведение, мы получаем возможность настраивать и использовать повторно компоненты поведения, внося изменения в определение данной привязки.

Одним из недостатков использования простого КА вместо ИКА для управления персонажем является отсутствие возможности быстро вносить изменения в существующее поведение, изменив в нём какой-либо параметр. Решить эту проблему в КА вы можете, только воссоздав весь конечный автомат заново ради единственного отличия.

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

Стандартная привязка поведения «бой»

MapAlias(«Combat», «pedestrian_combat.behavior»)

MapAlias(«Melee», «pedestrian_melee.behavior»)

MapAlias(«PathFollow», «pedestrian_pathfollow.behavior»)

MapAlias(«Patrol», «pedestrian_patrol.behavior»)

MapAlias(«Protect», «pedestrian_protect.behavior»)

MapAlias(«Approach», «pedestrian_approach.behavior»)…

Привязка поведения ниндзя

INCLUDE(«Common Combat Behaviors Map»)

OverrideAlias(«Melee», «ninja_melee_claw»)

OverrideAlias(«FireWeapon», «ninja_throw_shuriken»)

OverrideAlias(«Flee», NONE)

РИС. 4. Стандартная привязка поведений и специализированная — для ниндзя, обладающего другими методами рукопашного и огнестрельного боя. Также в списке нет поведения «сбежать» (flee)

Обычно псевдонимы определяются в файле, связанным напрямую с действующим персонажем. Однако, мы вновь сделать систему более гибкой, позволив псевдонимам переопределяться где угодно в древе поведений. Рассмотрим в качестве примера защитное поведение, описанное выше. На самом деле в нём два варианта дочерних поведений сближения внутри ИКА: один — для того, чтобы следовать за персонажем, требующим защиты; другой — для приближения к врагам, с которыми нужно вступить в бой.

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

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

Ещё один простой, но весьма успешный метод индивидуализации поведений — это добавление возможности не только настраивать, но и убирать у предка поведения. Задача выполнена простой привязкой поведения к специальному ключевому слову «none», указывающему на то, что поведение запускать не надо. Такая возможность пришлась очень кстати при создании вариаций врагов, которые, например, не кидают гранаты или не уклоняются.

ИИ в массы

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

В Destroy All Humans 2 решения, принятые нами в отношении ИИ в независимости от архитектуры, были направлены на достижение этих черт. Это адаптируемая, модульная система, с функциями, раскрытыми в обобщенном виде. Система, которая проводит черту между вручную написанными и скриптовыми поведениями, оставляя последние техническим дизайнерам.

Разработанная в Pandemic Australia’s система представлена в виде отдельных фрагментов, комбинируемых на различных уровнях абстракции. В результате, расширяемые и используемые повторно фрагменты приумножают свою пользу и делают нас на шаг ближе к населению виртуального мира виртуальной жизнью.

Заполнен: Туториалы
Присвоен тэг:

avatar

Об Авторе ()

Комментирование закрыто.

Наверх