Как создать игровой ИИ: гайд для начинающих

Наткнулся на интересный материал об искусственном интеллекте в играх. С объяснением базовых вещей про ИИ на простых примерах, а еще внутри много полезных инструментов и методов для его удобной разработки и проектирования. Как, где и когда их использовать — тоже есть.
Большинство примеров написаны в псевдокоде, поэтому глубокие знания программирования не потребуются. Под катом 35 листов текста с картинками и гифками, так что приготовьтесь.
UPD. Извиняюсь, но собственный перевод этой статьи на Хабре уже делал PatientZero. Прочитать его вариант можно здесь, но почему-то статья прошла мимо меня (поиском пользовался, но что-то пошло не так). А так как пишу в блог, посвященный геймдеву, решил оставить свой вариант перевода для подписчиков (некоторые моменты у меня оформлены по-другому, некоторые — намеренно пропущены по совету разработчиков).
Что такое ИИ?
Игровой ИИ сосредоточен на том, какие действия должен выполнять объект, исходя из условий, в которых находится. Обычно это называют управлением «интеллектуальными агентами», где агент является игровым персонажем, транспортным средством, ботом, а иногда и чем-то более абстрактным: целой группой сущностей или даже цивилизацией. В каждом случае это вещь, которая должна видеть свое окружение, принимать на его основе решения и действовать в соответствии с ними. Это называется циклом Sense/Think/Act (Чувствовать/Мыслить/Действовать):
- Sense: агент находит или получает информацию о вещах в своей среде, которые могут повлиять на его поведение (угрозы поблизости, предметы для сбора, интересные места для исследования).
- Think: агент решает, как реагировать (рассматривает, достаточно ли безопасно собирать предметы или сначала он должен сражаться/скрываться).
- Act: агент выполняет действия для реализации предыдущего решения (начинает движение к противнику или предмету).
- …теперь ситуация изменилась из-за действий персонажей, поэтому цикл повторяется с новыми данными.
Играм не нужна сложная система для извлечения информации, так как большая часть данных уже является ее неотъемлемой частью. Нет необходимости запускать алгоритмы распознавания изображений, чтобы определить, есть ли враг впереди — игра уже знает и передает сведения прямо в процессе принятия решений. Поэтому часть цикла Sense часто намного проще, чем Think и Act.
Ограничения игрового ИИ
У ИИ есть ряд ограничений, которые необходимо соблюдать:
- ИИ не нужно заранее тренировать, будто это алгоритм машинного обучения. Бессмысленно писать нейросеть во время разработки, чтобы наблюдать за десятками тысяч игроков и изучать лучший способ игры против них. Почему? Потому что игра не выпущена, а игроков нет.
- Игра должна развлекать и бросать вызов, поэтому агенты не должны находить лучший подход против людей.
- Агентам нужно выглядеть реалистичными, чтобы игроки чувствовали будто играют против настоящих людей. Программа AlphaGo превзошла человека, но выбранные шаги были сильно далеки от традиционного понимания игры. Если игра имитирует противника-человека, такого чувства не должно быть. Алгоритм нужно изменить, чтобы он принимал правдоподобные решения, а не идеальные.
- ИИ должен работать в реальном времени. Это значит, что алгоритм не может монополизировать использование процессора в течение длительного времени для принятия решений. Даже 10 миллисекунд на это — слишком долго, потому что большинству игр достаточно от 16 до 33 миллисекунд, чтобы выполнить всю обработку и перейти к следующему кадру графики.
- Идеально, если хотя бы часть системы управляется данными, чтобы «некодеры» могли вносить изменения, и чтобы корректировки происходили быстрее.
Принятие базовых решений
Начнем с простейшей игры — Pong. Цель: переместить платформу (paddle) так, чтобы мяч отскакивал от нее, а не пролетал мимо. Это как теннис, в котором вы проигрываете, если не отбиваете мяч. Здесь у ИИ относительно легкая задача — решить, в каком направлении перемещать платформу.

Условные операторы
Для ИИ в Pong есть самое очевидное решение — всегда стараться расположить платформу под мячом.
Простой алгоритм для этого, написанный в псевдокоде:
every frame/update while the game is running:
if the ball is to the left of the paddle:
move paddle left
else if the ball is to the right of the paddle:
move paddle right
Если платформа двигается со скоростью мяча, то это идеальный алгоритм для ИИ в Pong. Не нужно ничего усложнять, если данных и возможных действий для агента не так уж и много.
Этот подход настолько прост, что весь цикл Sense/Think/Act едва заметен. Но он есть:
- Часть Sense находится в двух операторах if. Игра знает где мяч и где платформа, поэтому ИИ обращается к ней за этой информацией.
- Часть Think тоже входит в два оператора if. Они воплощают в себе два решения, которые в данном случае являются взаимоисключающими. В результате выбирается одно из трех действий — переместить платформу влево, переместить вправо, или ничего не делать, если она уже правильно расположена.
- Часть Act находится в операторах Move Paddle Left и Move Paddle Right. В зависимости от дизайна игры, они могут перемещать платформу мгновенно или с определенной скоростью.
Дерево решений
Пример с игрой Pong фактически равен формальной концепции ИИ, называемой деревом решений. Алгоритм проходит его, чтобы достичь «листа» — решения о том, какое действие предпринять.
Сделаем блок-схему дерева решений для алгоритма нашей платформы:

Каждая часть дерева называется node (узел) — ИИ использует теорию графов для описания подобных структур. Есть два типа узлов:
- Узлы принятия решений: выбор между двумя альтернативами на основе проверки некоторого условия, где каждая альтернатива представлена в виде отдельного узла.
- Конечные узлы: действие для выполнения, представляющее окончательное решение.
В чем же преимущество, если дерево решений, выполняет ту же работу, что и if-операторы в предыдущем разделе? Здесь есть общая система, где каждое решение имеет только одно условие и два возможных результата. Это позволяет разработчику создавать ИИ из данных, представляющих решения в дереве, избежав его хардкодинга. Представим в виде таблицы:

На стороне кода вы получите систему для чтения строк. Создайте узел для каждой из них, подключите логику принятия решений на основе второго столбца и дочерние узлы на основе третьего и четвертого столбцов. Вам все еще нужно запрограммировать условия и действия, но теперь структура игры будет сложнее. В ней вы добавляете дополнительные решения и действия, а затем настраиваете весь ИИ, просто отредактировав текстовый файл с определением дерева. Далее передаете файл геймдизайнеру, который сможет изменить поведение без перекомпилирования игры и изменения кода.
Деревья решений весьма полезны, когда они строятся автоматически на основе большого набора примеров (например, с использованием алгоритма ID3). Это делает их эффективным и высокопроизводительным инструментом для классификации ситуаций на основе получаемых данных. Однако мы выходим за рамки простой системы для выбора действий агентами.
Сценарии
Мы разобрали систему дерева решений, которая использовала заранее созданные условия и действия. Человек, проектирующий ИИ, может организовать дерево так, как хочет, но он все еще должен полагаться на кодера, который это все запрограммировал. Что если мы могли бы дать дизайнеру инструменты для создания собственных условий или действий?
Чтобы программисту не писать код для условий Is Ball Left Of Paddle и Is Ball Right Of Paddle, он может сделать систему, в которой дизайнер будет записывать условия для проверки этих значений. Тогда данные дерева решений будут выглядеть так:

По сути это то же самое, что и в первой таблице, но решения внутри себя имеют свой собственный код, немного похожий на условную часть if-оператора. На стороне кода это считывалось бы во втором столбце для узлов принятия решений, но вместо поиска конкретного условия для выполнения (Is Ball Left Of Paddle), оно оценивает условное выражение и возвращает true или false соответственно. Это делается с помощью скриптового языка Lua или Angelscript. С помощью них разработчик может принимать объекты в своей игре (ball и paddle) и создавать переменные, которые будут доступны в сценарии (ball.position). Кроме того, язык сценариев проще, чем C++. Он не требует полной стадии компиляции, поэтому идеально подходит для быстрой корректировки игровой логики и позволяет «некодерам» самим создавать нужные функции.
В приведенном примере язык сценариев используется только для оценки условного выражения, но его также можно использовать и для действий. Например, данные Move Paddle Right, могут стать оператором сценария (ball.position.x += 10). Так, чтобы действие также определялось в скрипте, без необходимости программирования Move Paddle Right.
Можно пойти еще дальше и полностью написать дерево решений на языке сценариев. Это будет код в виде жестко запрограммированных (hardcoded) условных операторов, но они будут находится во внешних файлах скрипта, то есть могут быть изменены без перекомпиляции всей программы. Зачастую можно изменить файл сценария прямо во время игры, чтобы быстро протестировать разные реакции ИИ.
Реагирование на события
Примеры выше идеально подходят к Pong. Они непрерывно запускают цикл Sense/Think/Act и действуют на основе последнего состояния мира. Но в более сложных играх нужно реагировать на отдельные события, а не оценивать все и сразу. Pong в таком случае уже неудачный пример. Выберем другой.
Представьте шутер, где враги неподвижны пока не обнаружат игрока, после чего действуют в зависимости от своей «специализации»: кто-то побежит «рашить», кто-то будет атаковать издалека. Это все еще базовая реагирующая система — «если игрок замечен, то сделай что-нибудь», — но ее можно логически разделить на событие Player Seen (игрок замечен) и реакцию (выберите ответ и выполните его).
Это возвращает нас к циклу Sense/Think/Act. Мы можем накодить Sense-часть, которая каждый кадр будет проверять — видит ли ИИ игрока. Если нет — ничего не происходит, но если видит, то создается событие Player Seen. У кода будет отдельный раздел, в котором говорится: «когда происходит событие Player Seen, сделай », где — ответ, который вам нужен для обращения к частям Think и Act. Таким образом, вы настроите реакции к событию Player Seen: на «рашущего» персонажа — ChargeAndAttack, а на снайпера — HideAndSnipe. Эти связи можно создать в файле данных для быстрого редактирования без необходимости заново компилировать. И здесь тоже можно использовать язык сценариев.
Принятие сложных решений
Хоть простые системы реакций очень действенны, бывает много ситуаций, когда их недостаточно. Иногда нужно принимать различные решения, основанные на том, что агент делает в настоящий момент, но представлять это за условие тяжело. Иногда существует слишком много условий, чтобы эффективно представить их в дереве решений или скрипте. Иногда нужно заранее оценивать, как изменится ситуация, прежде чем принимать решение о следующем шаге. Для решения этих проблем нужны более сложные подходы.
Finite state machine
Finite state machine или FSM (конечный автомат) — это способ сказать, что наш агент в настоящее время находится в одном из нескольких возможных состояний, и что он может переходить из одного состояния в другое. Таких состояний определённое количество — отсюда и название. Лучший пример из жизни — дорожный светофор. В разных местах разные последовательности огней, но принцип тот же — каждое состояние представляет что-то (стой, иди и т.д.). Светофор находится только в одном состоянии в любой момент времени, и переходит от одного к другому на основе простых правил.
С NPC в играх похожая история. Для примера возьмем стража с такими состояниями:
- Патрулирующий (Patrolling).
- Атакующий (Attacking).
- Убегающий (Fleeing).
- Если страж видит противника, он атакует.
- Если страж атакует, но больше не видит противника, он возвращается к патрулированию.
- Если страж атакует, но сильно ранен, он убегает.
- Бездействие (Idling) — между патрулями.
- Поиск (Searching) — когда замеченный враг скрылся.
- Просить о помощи (Finding Help) — когда враг замечен, но слишком силен, чтобы сражаться с ним в одиночку.
В конце концов огромный список «если , то
», может стать слишком громоздким, поэтому следует формализовать метод, который позволит нам держать в уме состояния и переходы между состояниями. Чтобы это сделать, примем во внимание все состояния, и под каждым состоянием запишем в список все переходы в другие состояния, вместе с необходимыми для них условиями.

Это таблица переходов состояний — комплексный способ представления FSM. Нарисуем диаграмму и получим полный обзор того, как меняется поведение NPC.

Диаграмма отражает суть принятия решений для этого агента на основе текущей ситуации. Причем каждая стрелка показывает переход между состояниями, если условие рядом с ней истинно.
Каждое обновление мы проверяем текущее состояние агента, просматриваем список переходов, и если условия для перехода выполнены, он принимает новое состояние. Например, каждый кадр проверяется истек ли 10-секундный таймер, и если да, то из состояния Idling страж переходит в Patrolling. Таким же образом, состояние Attacking проверяет здоровье агента — если оно низкое, то он переходит в состояние Fleeing.
Это обработка переходов между состояниями, но как насчет поведения, связанного с самими состояниями? Что касается реализации фактического поведения для конкретного состояния, обычно существует два типа «крюка», где мы присваиваем действия к FSM:
- Действия, которые мы периодически выполняем для текущего состояния.
- Действия, которые мы предпринимаем при переходе из одного состояния в другое.
Для второго типа рассмотрим переход «если враг виден и враг слишком силен, то перейти в состояние Finding Help. Агент должен выбрать, куда пойти за помощью, и сохранить эту информацию, чтобы состояние Finding Help знало куда обратиться. Как только помощь найдена, агент переходит обратно в состояние Attacking. В этот момент он захочет рассказать союзнику об угрозе, поэтому может возникнуть действие NotifyFriendOfThreat.
И снова мы можем посмотреть на эту систему через призму цикла Sense/Think/Act. Sense воплощается в данных, используемых логикой перехода. Think — переходами, доступными в каждом состоянии. А Act осуществляется действиями, совершаемыми периодически в пределах состояния или на переходах между состояниями.
Иногда непрерывный опрос условий перехода может быть дорогостоящим. Например, если каждый агент будет выполнять сложные вычисления каждый кадр, чтобы определить видит ли он врагов и понять, можно ли переходить от состояния Patrolling к Attacking — это займет много времени процессора.
Важные изменения в состоянии мира можно рассматривать как события, которые будут обрабатываться по мере их появления. Вместо того, чтобы FSM каждый кадр проверял условие перехода «может ли мой агент видеть игрока?», можно настроить отдельную систему, чтобы выполнять проверки реже (например, 5 раз в секунду). А результатом выдавать Player Seen, когда проверка проходит.
Это передается в FSM, который теперь должен перейти в условие Player Seen event received и соответствующе отреагировать. Итоговое поведение одинаково за исключением почти незаметной задержки перед ответом. Зато производительность стала лучше в результате отделения части Sense в отдельную часть программы.
Hierarchical finite state machine
Однако работать с большими FSM не всегда удобно. Если мы захотим расширить состояние атаки, заменив его отдельными MeleeAttacking (ближний бой) и RangedAttacking (дальний бой), нам придется изменить переходы из всех других состояний, которые ведут в состояние Attacking (текущие и будущие).
Наверняка вы заметили, что в нашем примере много дублированных переходов. Большинство переходов в состоянии Idling идентичны переходам в состоянии Patrolling. Хорошо бы не повторяться, особенно если мы добавим больше похожих состояний. Имеет смысл сгруппировать Idling и Patrolling под общим ярлыком «небоевые», где есть только один общий набор переходов в боевые состояния. Если мы представим этот ярлык как состояние, то Idling и Patrolling станут подсостояниями. Пример использования отдельной таблицы переходов для нового небоевого подсостояния:

Основные состояния:

Состояние вне боя:
И в форме диаграммы:

Это та же самая система, но с новым небоевым состоянием, которое включает в себя Idling и Patrolling. С каждым состоянием, содержащим FSM с подсостояниями (а эти подсостояния, в свою очередь, содержат собственные FSM — и так далее сколько вам нужно), мы получаем Hierarchical Finite State Machine или HFSM (иерархический конечный автомат). Сгруппировав небоевое состояние, мы вырезали кучу избыточных переходов. То же самое мы можем сделать для любых новых состояний с общими переходами. Например, если в будущем мы расширим состояние Attacking до состояний MeleeAttacking and MissileAttacking, они будут подсостояниями, переходящими между друг другом на основе расстояния до врага и наличия боеприпасов. В итоге сложные модели поведения и подмодели поведения можно представить с минимумом дублированных переходов.
Дерево поведений
С HFSM создаются сложные комбинации поведений простым способом. Тем не менее, есть небольшая трудность, что принятие решений в виде правил перехода тесно связано с текущим состоянием. И во многих играх это как раз то, что нужно. А тщательное использование иерархии состояний может уменьшить количество повторов при переходе. Но иногда нужны правила, работающие независимо от того, в каком состоянии вы находитесь или которые применяются почти в любых состояниях. Например, если здоровье агента упало до 25%, вы захотите, чтобы он убегал независимо от того, был ли он в бою, бездельничал или разговаривал — вам придется добавлять это условие в каждое состояние. А если ваш дизайнер позже захочет изменить порог низкого здоровья с 25% до 10%, то этим снова придется заниматься.
В идеале для этой ситуации нужна система, в которой решения «в каком состоянии пребывать», находятся за пределами самих состояний, чтобы вносить изменения только в одном месте и не трогать условия перехода. Здесь появляются деревья поведений.
Существует несколько способов их реализации, но суть для всех примерно одинакова и похожа на дерево решений: алгоритм начинается с «корневого» узла, а в дереве находятся узлы, представляющие либо решения, либо действия. Правда есть несколько ключевых отличий:
- Теперь узлы возвращают одно из трех значений: Succeeded (если работа выполнена), Failed (если нельзя запустить) или Running (если она все еще запущена и нет конечного результата).
- Больше нет узлов решений для выбора между двумя альтернативами. Вместо них узлы Decorator, у которых есть один дочерний узел. Если они Succeed, то выполняют свой единственный дочерний узел.
- Узлы, выполняющие действия, возвращают значение Running для представления выполняемых действий.

С этой структурой не должно быть явного перехода от состояний Idling/Patrolling к состоянию Attacking или любым другим. Если враг виден, а здоровье персонажа низкое, выполнение остановится на узле Fleeing, независимо от того, какой узел он ранее выполнял — Patrolling, Idling, Attacking или любой другой.

Деревья поведений сложны — есть много способов их составить, да и найти правильное сочетание декораторов и составных узлов может быть проблематично. Есть также вопросы о том, как часто проверять дерево — мы хотим проходить его каждую часть или только тогда, когда одно из условий изменилось? Как хранить состояние, относящееся к узлам — как узнать, когда мы были в состоянии Idling в течение 10 секунд или как узнать, какие узлы выполнялись в прошлый раз, чтобы правильно обработать последовательность?
Именно поэтому существует множество реализаций. Например, в некоторых системах узлы декоратора заменили встроенными декораторами. Они повторно оценивают дерево при изменении условий декоратора, помогают присоединиться к узлам и обеспечивают периодические обновления.
Utility-based system
У некоторых игр есть множество различных механик. Желательно, чтобы они получили все преимущества простых и общих правил перехода, но не обязательно в виде полного дерева поведения. Вместо того, чтобы иметь чёткий набор выборов или дерево возможных действий, проще изучить все действия и выбрать самое подходящее в данный момент.
Utility-based system (система, основанная на полезности) как раз в этом поможет. Это система, где у агента есть множество действий, и он сам выбирает какое выполнить, основываясь на относительной полезности каждого. Где полезность — произвольная мера того, насколько важно или желательно выполнение этого действия для агента.
Рассчитанную полезность действия на основе текущего состояния и среды, агент может проверить и выбрать наиболее подходящее другое состояние в любое время. Это похоже на FSM, за исключением того, где переходы определяются оценкой для каждого потенциального состояния, включая текущее. Обратите внимание, что мы выбираем самое полезное действие для перехода (или остаемся, если уже выполнили его). Для большего разнообразия это может быть взвешенный, но случайный выбор из небольшого списка.
Система назначает произвольный диапазон значений полезности — например, от 0 (совершенно нежелательно) до 100 (полностью желательно). У каждого действия есть ряд параметров, влияющих на вычисление этого значения. Возвращаясь к нашему примеру со стражем:

Переходы между действиями неоднозначны — любое состояние может следовать за любым другим. Приоритеты действий находятся в возвращаемых значениях полезности. Если враг виден, и этот враг силен, а здоровье персонажа низкое, то и Fleeing, и FindingHelp вернут высокие ненулевые значения. При этом FindingHelp всегда будет выше. Аналогичным образом, небоевые действия никогда не возвращают больше 50, поэтому они всегда будут ниже боевых. Нужно учитывать это при создании действий и вычислении их полезности.
В нашем примере действия возвращают либо фиксированное постоянное значение, либо одно из двух фиксированных значений. Более реалистичная система предполагает возврат оценки из непрерывного диапазона значений. Например, действие Fleeing возвращает более высокие значения полезности, если здоровье агента низкое, а действие Attacking возвращает более низкие, если враг слишком силен. Из-за этого действие Fleeing имеет приоритет над Attacking в любой ситуации, когда агент чувствует, что у него недостаточно здоровья для победы над противником. Это позволяет изменять приоритеты действий на основе любого числа критериев, что делает такой подход более гибким и вариативным, чем дерево поведения или FSM.
Каждое действие имеет много условий для расчета программы. Их можно написать на языке сценариев или в виде серии математических формул. В The Sims, которая моделирует распорядок дня персонажа, добавляют дополнительный уровень вычислений — агент получает ряд «мотиваций», влияющих на оценки полезности. Если персонаж голоден, то со временем он будет голодать еще сильнее, и результат полезности действия EatFood будет расти до тех пор, пока персонаж не выполнит его, снизив уровень голода, и вернув значение EatFood равным нулю.
Идея выбора действий на основе системы оценок довольно проста, поэтому Utility-based system можно использовать как часть процессов принятия решений ИИ, а не как их полную замену. Дерево решений может запросить оценку полезности двух дочерних узлов и выбрать более высокую. Аналогичным образом, дерево поведения может иметь составной узел Utility для оценки полезности действий, чтобы решить, какой дочерний элемент выполнить.
Движение и навигация
В предыдущих примерах у нас была платформа, которую мы перемещали влево или вправо, и страж, который патрулировал или атаковал. Но как именно мы обрабатываем перемещение агента в течение определенного периода времени? Как мы устанавливаем скорость, как мы избегаем препятствий, и как мы планируем маршрут, если добраться до места назначения сложнее, чем просто двигаться по прямой? Давайте это рассмотрим.
Управление
На начальном этапе будем считать, что каждый агент имеет значение скорости, которое включает в себя, как быстро он двигается и в каком направлении. Она может быть измерена в метрах в секунду, километрах в час, пикселей в секунду и т. д. Вспоминая цикл Sense/Think/Act, мы можем представить, что часть Think выбирает скорость, а часть Act применяет эту скорость к агенту. Обычно в играх есть физическая система, которая выполняет эту задачу за вас, изучая значение скорости каждого объекта и регулируя ее. Поэтому можно оставить ИИ с одной задачей — решить, какую скорость должен иметь агент. Если известно, где агент должен быть, то нужно переместить его в правильном направлении с установленной скоростью. Очень тривиальное уравнение:
desired_travel = destination_position – agent_position
Представьте 2D-мир. Агент находится в точке (-2,-2), пункт назначения где-то на северо-востоке в точке (30, 20), а необходимый путь для агента, чтобы оказаться там — (32, 22). Допустим, эти позиции измеряются в метрах — если принять скорость агента за 5 метров в секунду, то мы будем масштабировать наш вектор перемещения и получим скорость примерно (4.12, 2.83). С этими параметрами агент прибыл бы к месту назначения почти через 8 секунд.
Пересчитать значения можно в любое время. Если агент был на полпути к цели, перемещение было бы половиной длины, но так как максимальная скорость агента 5 м/с (мы это решили выше), скорость будет одинаковой. Это также работает для движущихся целей, позволяя агенту вносить небольшие изменения по мере их перемещения.
Но мы хотим больше вариативности — например, медленно нарастить скорость, чтобы симулировать персонажа, движущегося из стоячего состояния и переходящего к бегу. Тоже самое можно сделать в конце перед остановкой. Эти фичи известны как steering behaviours, каждое из которых имеет конкретные имена: Seek (поиск), Flee (бегство), Arrival (прибытие) и т. д. Идея заключается в том, что силы ускорения могут быть применены к скорости агента, на основе сравнения положения агента и текущей скорости с пунктом назначения, чтобы использовать различные способы перемещения к цели.
Каждое поведение имеет немного другую цель. Seek и Arrival — это способы перемещения агента к точке назначения. Obstacle Avoidance (избегание препятствий) и Separation (разделение) корректируют движение агента, чтобы обходить препятствия на пути к цели. Alignment (согласование) и Cohesion (связь) держат агентов при перемещении вместе. Любое число различных steering behaviours может быть суммировано для получения одного вектора пути с учётом всех факторов. Агент, использующий поведения Arrival, Separation и Obstacle Avoidance, чтобы держаться подальше от стен и других агентов. Этот подход хорошо работает в открытых локациях без лишних деталей.
В более трудных условиях, сложение разных поведений работает хуже — к примеру, агент может застрять в стене из-за конфликта Arrival и Obstacle Avoidance. Поэтому нужно рассматривать варианты, которые сложнее, чем просто сложение всех значений. Способ такой: вместо сложения результатов каждого поведения, можно рассмотреть движение в разных направлениях и выбрать лучший вариант.
Однако в сложной среде с тупиками и выбором, в какую сторону идти, нам понадобится что-то ещё более продвинутое.
Поиск пути
Steering behaviours отлично подходит для простого движения на открытой местности (футбольное поле или арена), где добраться от А до Б — это прямой путь с небольшими отклонениями мимо препятствий. Для сложных маршрутов нам нужен pathfinding (поиск пути), который является способом изучения мира и принятия решения о маршруте через него.
Самый простой — наложить сетку на каждый квадрат рядом с агентом и оценить в каких из них разрешено двигаться. Если какой-то из них является пунктом назначения, то следуйте из него по маршруту от каждого квадрата к предыдущему, пока не дойдете до начала. Это и есть маршрут. В противном случае повторяйте процесс с ближайшими другими квадратами, пока не найдете место назначения или не закончатся квадраты (это означает, что нет никакого возможного маршрута). Это то, что формально известно как Breadth-First Search или BFS (алгоритм поиска в ширину). На каждом шаге он смотрит во всех направлениях (поэтому breadth, «ширина»). Пространство поиска похоже на волновой фронт, который перемещается, пока не достигнет искомое место — область поиска расширяется на каждом шаге до тех пор, пока в нее не попадет конечная точка, после чего можно отследить путь к началу.
В результате вы получите список квадратов, по которым составляется нужный маршрут. Это и есть путь (отсюда, pathfinding) — список мест, которые агент посетит, следуя к пункту назначения.
Учитывая, что мы знаем положение каждого квадрата в мире, можно использовать steering behaviours, чтобы двигаться по пути — от узла 1 к узлу 2, затем от узла 2 к узлу 3 и так далее. Простейший вариант — направиться к центру следующего квадрата, но еще лучше — остановиться на середине грани между текущим квадратом и следующим. Из-за этого агент сможет срезать углы на крутых поворотах.
У алгоритма BFS есть и минусы — он исследует столько же квадратов в «неправильном» направлении, сколько в «правильном». Здесь появляется более сложный алгоритм под названием A* (A star). Он работает также, но вместо слепого изучения квадратов-соседей (затем соседей соседей, затем соседей соседей соседей и так далее), он собирает узлы в список и сортирует их так, что следующий исследуемый узел всегда тот, который приведет к кратчайшему маршруту. Узлы сортируются на основе эвристики, которая учитывает две вещи — «стоимость» гипотетического маршрута к нужному квадрату (включая любые затраты на перемещение) и оценку того, насколько далеко этот квадрат от места назначения (смещая поиск в правильном направлении).
В этом примере показано, что агент исследует по одному квадрату за раз, каждый раз выбирая соседний, который является самым перспективным. Полученный путь такой же, как и при BFS, но в процессе было рассмотрено меньше квадратов — а это имеет большое значение для производительности игры.
Движение без сетки
Но большинство игр не выложены на сетке, и зачастую ее невозможно сделать без ущерба реалистичности. Нужны компромиссы. Каких размеров должны быть квадраты? Слишком большими — и они не смогут правильно представить небольшие коридоры или повороты, слишком маленькими — будет чрезмерно много квадратов для поиска, который в итоге займет кучу времени.
Первое, что нужно понять — сетка дает нам граф связанных узлов. Алгоритмы A* и BFS фактически работают на графиках и вообще не заботятся о нашей сетке. Мы могли бы поставить узлы в любых местах игрового мира: при наличии связи между любыми двумя соединенными узлами, а также между начальной и конечной точкой и по крайней мере одним из узлов — алгоритм будет работать также хорошо, как и раньше. Часто это называют системой путевых точек (waypoint), так как каждый узел представляет собой значимую позицию в мире, которая может быть частью любого количества гипотетических путей.

Пример 1: узел в каждом квадрате. Поиск начинается из узла, в котором находится агент, и заканчивается в узле нужного квадрата.

Пример 2: меньший набор узлов (путевых точек). Поиск начинается в квадрате с агентом, проходит через необходимое количество узлов, и затем продолжается до пункта назначения.
Это вполне гибкая и мощная система. Но нужна некоторая осторожность в решениях, где и как поместить waypoint, иначе агенты могут просто не увидеть ближайшую точку и не смогут начать путь. Было бы проще, если мы могли автоматически расставить путевые точки на основе геометрии мира.
Тут появляется navigation mesh или navmesh (навигационная сетка). Это обычно 2D-сетка треугольников, которая накладывается на геометрию мира — везде, где агенту разрешено ходить. Каждый из треугольников в сетке становится узлом в графе и имеет до трех смежных треугольников, которые становятся соседними узлами в графе.
Эта картина является примером из движка Unity — он проанализировал геометрию в мире и создал navmesh (на скриншоте светло-голубым цветом). Каждый полигон в navmesh — это область, на которой агент может стоять или перемещаться из одного полигона в другой полигон. В данном примере полигоны меньше этажей, на которых они расположены — сделано для того, чтобы учесть размеры агента, которые будут выходить за пределы его номинального положения.

Мы можем искать маршрут через эту сетку, снова используя алгоритм A*. Это даст нам практически идеальный маршрут в мире, который учитывает всю геометрию и при этом не требует лишних узлов и создания путевых точек.
Pathfinding — слишком обширная тема, о которой мало одного раздела статьи. Если хотите изучить ее более подробно, то в этом поможет сайт Амита Пателя.
Планирование
Мы убедились с pathfinding, что иногда недостаточно просто выбрать направление и двигаться — мы должны выбрать маршрут и сделать несколько поворотов, чтобы добраться до нужного места назначения. Мы можем обобщить эту идею: достижение цели это не просто следующий шаг, а целая последовательность, где иной раз требуется заглянуть вперед на несколько шагов, чтобы узнать, каким должен быть первый. Это называется планированием. Pathfinding можно рассматривать как одно из нескольких дополнений планирования. С точки зрения нашего цикла Sense/Think/Act, это то, где часть Think планирует несколько частей Act на будущее.
Разберем на примере настольной игры Magic: The Gathering. Мы ходим первыми с таким набором карт на руках:
- Swamp — дает 1 черную ману (карта земли).
- Forest — дает 1 зеленую ману (карта земли).
- Fugitive Wizard — требует 1 синию ману для призыва.
- Elvish Mystic — требует 1 зеленую ману для призыва.
Простое планирование
Тривиальный подход — пробовать каждое действие по очереди, пока не останется подходящих. Глядя на карты, ИИ видит, что может сыграть Swamp. И играет его. Остались ли другие действия на этом ходу? Он не может вызвать ни Elvish Mystic, ни Fugitive Wizard, поскольку для их призыва требуется соответственно зеленая и синяя мана, а Swamp дает только черную ману. И он уже не сможет играть Forest, потому что уже сыграл Swamp. Таким образом, игровой ИИ сходил по правилам, но сделал это плохо. Можно улучшить.
Планирование может найти список действий, которые приводят игру в желаемое состояние. Также, как каждая квадрат на пути имел соседей (в pathfinding), каждое действие в плане тоже имеет соседей или преемников. Мы можем искать эти действия и последующие действия, пока не достигнем желаемого состояния.
В нашем примере, желаемый результат «вызвать существо, если это возможно». В начале хода мы видим только два возможных действия, разрешенных правилами игры:
1. Сыграть Swamp (результат: Swamp в игре)
2. Сыграть Forest (результат: Forest в игре)
Каждое принятое действие может привести к дальнейшим действиям и закрыть другие, опять же в зависимости от правил игры. Представьте, что мы сыграли Swamp — это удалит Swamp в качестве следующего шага (мы его уже сыграли), также это удалит и Forest (потому что по правилам можно сыграть одну карту земли за ход). После этого ИИ добавляет в качестве следующего шага — получение 1 черной маны, потому что других вариантов нет. Если он пойдет дальше и выберет Tap the Swamp, то получит 1 единицу черной маны и ничего с ней не сможет сделать.
1. Сыграть Swamp (результат: Swamp в игре)
1.1 «Тапнуть» Swamp (результат: Swamp «тапнута», +1 единица черной маны)
Нет доступных действий – КОНЕЦ
2. Сыграть Forest (результат: Forest в игре)
Список действий вышел коротким, мы зашли в тупик. Повторяем процесс для следующего действия. Мы играем Forest, открываем действие «получить 1 зеленую ману», которая в свою очередь откроет третье действие — призыв Elvish Mystic.
1. Сыграть Swamp (результат: Swamp в игре)
1.1 «Тапнуть» Swamp (результат: Swamp «тапнута», +1 единица черной маны)
Нет доступных действий – КОНЕЦ
2. Сыграть Forest (результат: Forest в игре)
2.1 «Тапнуть» Forest (результат: Forest «тапнута», +1 единица зеленой маны)
2.1.1 Призвать Elvish Mystic (результат: Elvish Mystic в игре, -1 единица зеленой маны)
Нет доступных действий – КОНЕЦ
Наконец, мы изучили все возможные действия и нашли план, призывающий существо.
Это очень упрощенный пример. Желательно выбирать лучший возможный план, а не любой, который соответствует каким-то критериям. Как правило, можно оценить потенциальные планы на основе конечного результата или совокупной выгоды от их выполнения. Можно начислить себе 1 очко за игру карты земли и 3 очка за вызов существа. Играть Swamp было бы планом, дающим 1 очко. А сыграть Forest → Tap the Forest → призвать Elvish Mystic — сразу даст 4 очка.
Вот так работает планирование в Magic: The Gathering, но по той же логике это применяется и в других ситуациях. Например, переместить пешку, чтобы освободить место для хода слона в шахматах. Или укрыться за стеной, чтобы безопасно стрелять в XCOM так. В общем, вы поняли суть.
Улучшенное планирование
Иногда бывает слишком много потенциальных действий, чтобы рассматривать каждый возможный вариант. Возвращаясь к примеру с Magic: The Gathering: допустим, что в игре и на у вас на руках по несколько карт земли и существ — количество возможных комбинаций ходов может исчисляться десятками. Есть несколько решений проблемы.
Первый способ — backwards chaining (обратное формирование цепи). Вместо перебора всех комбинаций, лучше начать с итогового результата и попробовать найти прямой маршрут. Вместо пути от корня дерева к определенному листу, мы двигаемся в обратном направлении — от листа к корню. Этот способ проще и быстрее.
Если у противника 1 единица здоровья, можно найти план «нанести 1 или более единиц урона». Чтобы добиться этого нужно выполнить ряд условий:
1. Урон может нанести заклинание — оно должно быть в руке.
2. Чтобы разыграть заклинание — нужна мана.
3. Чтобы получить ману — нужно разыграть карту земли.
4. Чтобы разыграть карту земли — нужно иметь ее в руке.
Другой способ — best-first search (наилучший первый поиск). Вместо перебора всех путей, мы выбираем наиболее подходящий. Чаще всего этот способ даёт оптимальный план без лишних затрат на поиски. A* — это форма наилучшего первого поиска — исследуя наиболее перспективные маршруты с самого начала, он уже может найти наилучший путь без необходимости проверять остальные варианты.
Интересным и все более популярным вариантом best-first search является Monte Carlo Tree Search. Вместо угадывания, какие планы лучше других при выборе каждого последующего действия, алгоритм выбирает случайных преемников на каждом шаге, пока не достигнет конца (когда план привел к победе или поражению). Затем итоговый результат используется для повышения или понижения оценки «веса» предыдущих вариантов. Повторяя этот процесс несколько раз подряд, алгоритм дает хорошую оценку того, какой следующий шаг лучше, даже если ситуация изменится (если противник примет меры, чтобы помешать игроку).
В рассказе о планировании в играх не обойдется без Goal-Oriented Action Planning или GOAP (целенаправленное планирование действий). Это широко используемый и обсуждаемый метод, но помимо нескольких отличительных деталей это, по сути, метод backwards chaining, о котором мы говорили ранее. Если задача была «уничтожить игрока», и игрок находится за укрытием, план может быть таким: уничтожь гранатой → достань ее → брось.
Обычно существует несколько целей, каждая со своим приоритетом. Если цель с наивысшим приоритетом не может быть выполнена (ни одна комбинация действий не создает план «уничтожить игрока», потому что игрок не виден), ИИ вернется к целям с более низким приоритетом.
Обучение и адаптация
Мы уже говорили, что игровой ИИ обычно не использует машинное обучение, потому что это не подходит для управления агентами в реальном времени. Но это не значит, что нельзя что-нибудь позаимствовать из этой области. Мы хотим такого противника в шутере, у которого можно чему-нибудь научиться. Например, узнать о лучших позициях на карте. Или противника в файтинге, который блокировал бы часто используемые игроком комбо-приемы, мотивируя использовать другие. Так что машинное обучение в таких ситуациях может быть весьма полезно.
Статистика и вероятности
Прежде чем мы перейдем к сложным примерам, прикинем, как далеко мы можем зайти, взяв несколько простых измерений и используя их для принятия решений. К примеру, стратегия в реальном времени — как нам определить сможет ли игрок начать атаку в первые несколько минут игры и какую оборону против этого приготовить? Мы можем изучить прошлый опыт игрока, чтобы понять, какой может быть будущая реакция. Начнем с того, что у нас нет таких исходных данных, но мы их можем собрать — каждый раз, когда ИИ играет против человека, он может записывать время первой атаки. Спустя несколько сессий мы получим среднее значение времени, через которое игрок будет атаковать в будущем.
У средних значений есть и проблема: если игрок 20 раз «рашил», а 20 раз играл медленно, то нужные значения будут где-то в середине, а это ничего полезного нам не даст. Одним из решений является ограничение входных данных — можно учитывать последние 20 штук.
Аналогичный подход используется при оценке вероятности определенных действий, предполагая, что прошлые предпочтения игрока будут такими же в будущем. Если игрок атакует нас пять раз фаерболом, два раза молнией и один раз врукопашную, очевидно, что он предпочитает фаербол. Экстраполируем и увидим вероятность использования различного оружия: фаербол=62,5%, молния=25% и рукопашная=12,5%. Нашему игровому ИИ нужно подготовиться к защите от огня.
Еще один интересный метод — использовать Naive Bayes Classifier (наивный байесовский классификатор) для изучения больших объемов входных данных и классифицировать ситуацию, чтобы ИИ реагировал нужным образом. Байесовские классификаторы наиболее известны за использование в фильтрах спама электронной почты. Там они исследуют слова, сравнивают их с тем, где появлялись эти слова ранее (в спаме или нет), и делают выводы о входящих письмах. Мы можем сделать то же самое даже с меньшим количеством входных данных. На основе всей полезной информации, которую видит ИИ (например, какие вражеские юниты созданы, или какие заклинания они используют, или какие технологии они исследовали), и итогового результата (война или мир, «рашить» или обороняться и т. д.) — мы выберем нужное поведение ИИ.
Всех этих способов обучения достаточно, но желательно использовать их на основе данных из тестирования. ИИ научится адаптироваться к различным стратегиям, которые использовали ваши плэйтестеры. ИИ, который адаптируется к игроку после релиза, может стать слишком предсказуемым или наоборот слишком сложным для победы.
Адаптация на основе значений
Учитывая наполнение нашего игрового мира и правил, мы можем изменить набор значений, которые влияют на принятие решений, а не просто использовать входные данные. Делаем так:
- Пусть ИИ собирает данные о состоянии мира и ключевых событиях во время игры (как указано выше).
- Изменим несколько важных значений (value) на основе этих данных.
- Реализуем свои решения, основанные на обработке или оценке этих значений.
Марковская модель
Что если мы используем собранные данные для прогнозирования? Если запомнить каждую комнату, в которой видим игрока в течение определенного периода времени, мы будем предугадывать в какую комнату игрок может перейти. Отследив и записав перемещения игрока по комнатам (values), мы можем прогнозировать их.
Возьмем три комнаты: красную, зеленую и синюю. А также наблюдения, которые мы записали при просмотре игровой сессии:

Количество наблюдений за каждой комнатой почти равное — где сделать хорошее место для засады мы до сих пор не знаем. Сбор статистики также осложняется респауном игроков, которые появляются равномерно по всей карте. Но данные о следующей комнате, в которую они входят после появления на карте — уже полезны.
Видно, что зеленая комната устраивает игроков — большинство людей из красной переходят в нее, 50% которых остается там и дальше. Синяя комната наоборот не пользуется популярность, в нее почти не ходят, а если ходят, то не задерживаются.
Но данные говорят нам кое-что более важное — когда игрок находится в синей комнате, то следующая комната, в которой мы его скорее всего увидим будет красной, а не зеленой. Несмотря на то, что зеленая комната популярнее красной, ситуация меняется, если игрок находится в синей. Следующее состояние (то есть комната, в которую игрок перейдет) зависит от предыдущего состояния (то есть комнаты, в которой игрок находится сейчас). Из-за исследования зависимостей мы будем делать прогнозы точнее, чем если бы мы просто подсчитывали наблюдения независимо друг от друга.
Предугадывание будущего состояния на основе данных прошлого состояния называется марковской моделью (Markov model), а такие примеры (с комнатами) называют марковскими цепями. Поскольку модели представляют собой вероятность изменений между последовательными состояниями, визуально они отображаются в виде FSM с вероятностью около каждого перехода. Ранее мы использовали FSM для представления поведенческого состояния, в котором находился агент, но эта концепция распространяется на любое состояние, независимо от того, связано это с агентом или нет. В этом случае состояния представляют комнату, которую занимает агент:

Это простой вариант представления относительной вероятности изменений состояний, дающий ИИ некую возможность предсказывать следующее состояние. Можно предугадывать несколько шагов вперед.
Если игрок в зеленой комнате, то есть 50% шанс, что он там и останется при следующем наблюдении. Но какова вероятность, что он все еще будет там даже после? Есть не только шанс, что игрок остался в зеленой комнате после двух наблюдений, но и шанс, что он ушел и вернулся. Вот новая таблица с учетом новых данных:

Из нее видно, что шанс увидеть игрока в зеленой комнате после двух наблюдений будет равен 51% — 21%, что он придется из красной комнаты, 5% из них, что игрок посетит синюю комнату между ними, и 25%, что игрок вообще не уйдет из зеленой комнаты.
Таблица — просто наглядный инструмент — процедура требует только умножения вероятностей на каждом шаге. Это означает, что вы можете заглянуть далеко в будущее с одной поправкой: мы предполагаем, что шанс войти в комнату полностью зависит от текущей комнаты. Это называется марковским свойством (Markov Property) — будущее состояние зависит только от настоящего. Но это не стопроцентно точно. Игроки могут менять решения в зависимости от других факторов: уровень здоровья или количество боеприпасов. Так как мы не фиксируем эти значения, наши прогнозы будут менее точными.
N-Grams
А что насчет примера с файтингом и предсказанием комбо-приемов игрока? То же самое! Но вместо одного состояния или события, мы будем исследовать целые последовательности, из которых состоит комбо-удар.
Один из способов сделать это — сохранить каждый ввод (например, Kick, Punch или Block) в буфере и записать весь буфер в виде события. Итак, игрок неоднократно нажимает Kick, Kick, Punch, чтобы использовать атаку SuperDeathFist, система ИИ хранит все вводы в буфере и запоминает последние три, используемые на каждом шаге.

(Жирным выделены строки, когда игрок запускает атаку SuperDeathFist.)
ИИ увидит все варианты, когда игрок выбрал Kick, следом за другим Kick, а после заметить, что следующий ввод всегда Punch. Это позволит агенту спрогнозировать комбо-прием SuperDeathFist и заблокировать его, если это возможно.
Эти последовательности событий называются N-граммами (N-grams), где N — количество хранимых элементов. В предыдущем примере это была 3-грамма (триграмма), что означает: первые две записи используются для прогнозирования третьей. Соответственно в 5-грамме первые четыре записи предсказывают пятую и так далее.
Разработчику нужно тщательно выбирать размер N-грамм. Меньшее число N требует меньше памяти, но и хранит меньшую историю. Например, 2-грамма (биграмма) будет записывать Kick, Kick или Kick, Punch, но не сможет хранить Kick, Kick, Punch, поэтому ИИ не отреагирует на комбо SuperDeathFist.
С другой стороны, большие числа требуют больше памяти и ИИ будет сложнее обучиться, так как появится гораздо больше возможных вариантов. Если у вас было три возможных ввода Kick, Punch или Block, а мы использовали 10-грамму, то получится около 60 тысяч различных вариантов.
Модель биграммы это простая марковская цепь — каждая пара «прошлое состояние/текущее состояние» является биграммой, и вы можете предсказать второе состояние на основе первого. 3-грамма и более крупные N-граммы также можно рассматривать как марковские цепи, где все элементы (кроме последнего в N-грамме) вместе образуют первое состояние, а последний элемент — второе. Пример с файтингом показывает шанс перехода от состояния Kick и Kick к состоянию Kick и Punch. Рассматривая несколько записей входной истории как одну единицу, мы, по сути, преобразуем входную последовательность в часть целого состояния. Это дает нам марковское свойство, позволяющее использовать марковские цепи для прогнозирования следующего ввода и угадать, какой комбо-ход будет следующим.
Заключение
Мы поговорили о наиболее распространенных инструментах и подходах в разработке искусственного интеллекта. А также разобрали ситуации, в которых их нужно применять и где они особенно полезны.
Этого должно быть достаточно для понимания базовых вещей в игровом ИИ. Но, конечно же, это далеко не все методы. К менее популярным, но не менее эффективным относятся:
- алгоритмы по оптимизации, включая восхождение по холмам, градиентный спуск и генетические алгоритмы
- состязательные алгоритмы поиска/планирования (minimax и alpha-beta pruning)
- методы классификации (перцептроны, нейронные сети и машины опорных векторов)
- системы для обработки восприятия и памяти агентов
- архитектурные подходы к ИИ (гибридные системы, подмножество архитектур и другие способы наложения систем ИИ)
- инструменты анимации (планирование и согласование движения)
- факторы производительности (уровень детализации, алгоритмы anytime, и timeslicing)
1. На GameDev.net есть раздел со статьями и туториалами по ИИ, а также форум.
2. AiGameDev.com содержит множество презентаций и статей по широкому спектру связанных с разработкой игрового ИИ.
3. The GDC Vault включает в себя топики с саммита GDC AI, многие из которых доступны бесплатно.
4. Полезные материалы также можно найти на сайте AI Game Programmers Guild.
5. Томми Томпсон, исследователь ИИ и разработчик игр, делает ролики на YouTube-канале AI and Games с объяснением и изучением ИИ в коммерческих играх.
1. Серия книг Game AI Pro представляет собой сборники коротких статей, объясняющих, как реализовать конкретные функции или как решать конкретные проблемы.
2. Серия AI Game Programming Wisdom — предшественник серии Game AI Pro. В ней более старые методы, но почти все актуальные даже сегодня.
3. Artificial Intelligence: A Modern Approach — это один из базовых текстов для всех желающих разобраться в общей области искусственного интеллекта. Это книга не о игровой разработке — она учит базовым основам ИИ.
- AI
- ИИ
- scripting language
- искусственный интеллект
- боты
- разработка игр
- Finite State machine
- Hierarchical finite state machine
- decision tree
- behaviour tree
- Utility-based system
- Markov model
Игры, бизнес, интерфейсы
Эта статья собрана на основе моей лекции «Проектирование опыта в живых играх», которую я читал на мероприятии для дизайнеров Сбербанка, а потом и своим сотрудникам в JetStyle. Она про то, что такое проектирование опыта в понимании шире, чем в интерфейсах.
Спойлер. Главная мысль автора такая: всё, чем я занимаюсь (управлением бизнеса, проектированием интерфейсов и созданием игр) — это на самом деле всё одно и то же.
Я занимаюсь тем, что проектирую опыт людей.
Сейчас я расскажу о том, как устроены живые игры и какие инструменты я в них нашел и использую при проектировании взаимодействия в бизнесе.
Что такое игра
Несколько классических определений
Игра — это серия интересных выборов.
Создатель «Цивилизации»
В бизнесе мы не хотим попадать в ситуацию, когда выбор интересный. Мы хотим, чтобы выбор был однозначный. Интересный выбор — сложный, в нем легко ошибиться, а это не то, чего нам хочется в бизнесе обычно.
Казалось бы, это и не та ситуация, в которую мы должны ставить пользователя. Например, если задумать интерфейс как серию интересных выборов, то в большинстве случаев это окажется неэффективным решением. Если только это не Tinder.
Игра — добровольное усилие ради преодоления ненужных препятствий.
Бернард Сьютс
Люди в игру приходят специально, чтобы почувствовать удовольствие от того, что у них получается преодолевать препятствия. Сами препятствия — искусственные, а переживание опыта преодоления — настоящее.
Это определение на бизнес похоже уже больше, чем пример со сложными выборами. Особенно если бизнес как-то по-дурацки организован. Но все-таки ни в интерфейсе, ни в бизнесе мы вроде бы ненужных препятствий стараемся не делать. Хотя, если разобраться, мы найдем гору исключений, но разбираться мы будем чуть позже.
Свободная деятельность, которая осознается как «невзаправду» и вне повседневной жизни выполняемое занятие, однако может целиком овладевать играющим, не преследует при этом никакого прямого материального интереса, не ищет пользы, совершается внутри намеренно ограниченного пространства и времени, протекает упорядоченно, по определенным правилам.
Йохан Хейзинга
Это длинное и самое классическое определение. Никто не способен его запомнить, и я тоже. Поэтому я в нем выделил те вещи, которые мне кажутся важными.
Признаки игры
Давайте из этих определений выделим применимые для задач бизнеса признаки игры.
Игра проходит в границах «магического круга» — она вне реальной, обычной жизни
Терминально важный признак, без которого игра не игра: люди должны понимать, где она начинается, а где заканчивается.
В сленге разработчиков это называется «границы магического круга»: когда кто-то вступает в игру, он понимает, что попадает в пространство, которое построено по определенным правилам, принимает эти правила и знает, что может выйти из этой истории.
В игре участвуют добровольно , ради удовольствия
Этот и первый пункты обязательные, всё остальное может меняться. Позже расскажу почему.
Сама игра — проявление свободы игрока
Если не дать участнику как действующему лицу принимать свободные решения, если он не сможет проявлять свою личность в действии — у него исчезнет зона игры. Это превратится в театр или кино, где от него ничего не зависит.
Для того, чтобы игра оставалась игрой, нужно, чтобы от игрока что-то зависело. Это что-то может быть очень обширным, как это бывает в компьютерных играх с открытым миром. Или очень узким, как в игре Flappy Bird, где всё, что можно делать, — поднимать и опускать птичку. Какая-то свобода у игрока должна оставаться.
И еще три признака, о которых тоже дальше расскажу.
В игру играют ради интереса
В игре есть внутренний порядок, заданный правилами
(или другими соглашениями)
В игре есть ненужные препятствия и риск , на который идут ради выигрыша
Почему я столько этому уделяю внимания? Что такое игра все понимают очень по-разному , трактуют шире или уже. И прежде чем подходить к чему-то (бизнесу, интерфейсу, институту) как к игре, стоит убедиться, насколько это может быть игрой. У меня подход примерно такой: чем вот этих признаков больше, тем больше аргументов за то, чтобы подходить к этому как к игре. И наоборот.
Виды игр
Игры бывают всякие, например настольные или компьютерные. Я занимаюсь играми живого действия. Это когда люди моделируют ситуацию и в этой ситуации ведут себя так, как бы вели себя в ней их персонажи. При этом они свои действия определяют самостоятельно: сами ищут мотив и сами выбирают способ из доступного арсенала.
Живые игры проводятся ради удовольствия и делятся на художественные и прикладные.
Художественные игры
Мы с моими коллегами-организаторами игр в последнее время на художественные игры смотрим скорее как на искусство, чем как на развлечение, хотя начиналось все наоборот. Как и любое другое искусство, художественные игры делаются для того, чтобы организаторы, они же мастера игры, вместе с игроками реализовывали свои замыслы и получали от этого радость.
Чем это похоже на то, чем мы занимаемся в обычной жизни и в бизнесе? Когда мы делаем бизнес или продукт, или какой-то некоммерческий проект, у нас как у организаторов есть замысел. Этот замысел будет воплощаться в поведении других людей. Это поведение свободное, они будут им управлять сами, в рамках, которые мы им предложим. И то, что у нас получается, всегда создается совместными усилиями владельцев бизнеса, системы и ее участников.
Прикладные игры
Прикладные игры делаются, чтобы в результате игроки за пределами игры получили пользу. Например, выучили в игре какой-то навык, спроектировали или примерили на себя какое-то взаимодействие, попробовали себя в командных ролях и поняли, кто с чем лучше справляется.
Еще такая игра — это способ добровольно понять отношение к происходящему, занять собственную позицию. Это самая полезная штука, которую с помощью игры как прикладного инструмента можно сделать, потому что другим способом формирования позиции фиг добьешься. Ну, т. е. не считая настоящего кризиса в реальной деятельности, но это очень дорогой способ обучения.
Общие черты
И в художественных, и в прикладных играх мы присваиваем позицию — отношение к ситуации в результате пережитого опыта. Когда человек чувствует, что опыт, который с ним случился, может быть использован им дальше. И имеет к этому опыту отношение, формирует намерение как себя вести в подобных ситуациях.
Я лично хожу в игры за новыми хорошими вопросами, на которые мне интересно отвечать. Хороший вопрос — это вопрос, на который ответить хочется, но не получается.
Игра — это форма коллективного мышления и действия. Это опыт, который игроки переживают совместно.
Время в игре гораздо более плотное, чем в жизни. В обычной реальности сравнимая плотность событий бывает только в экстремальных ситуациях, когда у вас все горит или когда произошла по-настоящему большая неприятность и вам приходится совместно предпринимать очень много срочных действий и испытывать много эмоций. Вы наверняка чувствовали, как, выходя из таких испытаний, вы становитесь более близкими людьми. Может быть, друзьями. Может быть, командой. Почему это так? Потому что у вас возник разделенный опыт — то, через что вы прошли вместе, к чему у вас есть общее отношение.
Игра в этом плане — способ получить такой же эффект, не платя за это такую же цену. Не обязательно рисковать бизнесом, жизнью и здоровьем. Можно получить настолько же интенсивные переживания с помощью совместного опыта игры.
Призы и награды, к которым относятся хорошие вопросы, позиция, опыт, совместные решения, — не существовали до игры. Они были найдены игроками вместе, внутри игры, в результате личного или коллективного опыта. А в готовом виде их не было.
Это значит, что хотя у организаторов игры есть замысел, он все равно трансформируется и переосмысляется игроками. Игроки достают из игры не совсем то же самое, что мастера туда вложили.
Нельзя взять и до игры сказать: «Ну, смотрите. Вы в игре обнаружите вот такие выводы, просто возьмите их и всe». Так не работает. Точнее, так можно, но тогда игра не полезней менее интерактивных инструментов. Потому что выводы, которые были получены не в результате собственного опыта, гораздо менее ценные. Они принадлежат не тому, кому их дали, а тому, кто сам их из своего опыта извлек.
Инструменты проектирования опыта
Это не все инструменты, которыми я пользуюсь, а три основных. На их примере я покажу параллели с UX-дизайном и бизнесом.
Структура игры
Структура роли
Виды мотивации
Инструмент 1
Структура игры
Это структура вообще любой интерактивной деятельности. Интерактивной, это когда:
в деятельности есть интенсивное взаимодействие сторон,
она представляет собой цельный замысел
и направлена на то, чтобы с людьми что-нибуд ь произошло для них ценное, полезное или значимое.
Структура держится на вот этой триаде: открывающее событие, непосредственно действие и собирающее событие. На нашем сленге это называется проблематизацией, движухой и рефлексией.
Картинку со схемой-конфетой я взял из книжки «Геймшторминг. Игры, в которые играет бизнес». Кстати, очень толковая книжка. Если вам хочется найти какие-нибудь простые квазиигровые формы для бизнеса, то это лучшее, что до сих пор на русском языке написано, хоть и переведено. Позволит вам понять, как пользоваться маленькими играми для рабочих ситуаций.
У нас есть много похожих, как в этой книге, выводов, которые мы сделали сами, немножечко по-другому. Но мы в книгу это не удосужились собрать, а авторы собрали коллекцию неплохих прикладных игр. Так вот, сама схема оттуда, а триада наша собственная.
Проблематизация
Погрузить в проблему
Каждая игра посвящена какой-то заморочке. Она не обязательно дается в терминах проблемы. Просто есть то, что мы в этой игре считаем важным, то, во что и с каким настроением мы предлагаем поиграть, зачем мы это делаем и т. д.
Это может быть событие, уже происходящее в игре или произошедшее до игры. Мы можем сказать это игрокам прямо или прогнать их через анкету. А можем просто первую треть игры этому посвятить.
Проблематизация нужна, чтобы человек понял: вот в чем фокус внимания, про который мне предлагают играть. Вот какие цели мне предлагают выбрать. Вот какие правила я должен на себя принять и с которыми должен согласиться. Вот в каких формах эта игра вообще проистекает, чтобы ее можно было попробовать.
Помочь понять формы игры
Чтобы попасть непосредственно в мир игры и почувствовать погружение в него, игроку важно дать себя захватить. И стать частью этого мира, которая получает какую-то пользу от происходящего и другим полезна.
Человек должен заранее понимать, готов ли он к этому. Поэтому желательно дать ему это попробовать. В современных играх мы это делаем в основном за счет воркшопов. В смысле не за счет того, что мы объясняем долго правила, а за счет того, что мы даем их попробовать.
Убедиться, что приняты цели и правила
В бизнесе и в продукте, как и в игре, есть искусственная реальность, которая создается совместными представлениями и согласованными действиями всех участников. Норма о принятии на себя правил существует потому, что если человек начинает правила игнорировать, он разрушает реальность вокруг себя. Ведь эта реальность работает только постольку, поскольку эти правила и конвенции всеми разделяются и выполняются.
Принятие правил ради того, чтобы все получили от игры некоторую ценность, — это контракт между организаторами игры и игроком или между владельцами сервиса и пользователем, или между руководителем и сотрудником. Это всё одно и то же. Владелец игрового пространства — мастер игры или владелец сервиса/бизнеса — говорит примерно так:
« Дорогой друг, если ты будешь нарушать контракт, пойдешь на хрен. Знай это».
Обычно в ролевом сообществе это достаточно мягкая норма. Человеку делают несколько предупредительных выстрелов в воздух. Основной инструмент здесь не нормативный, а культурный, ну и, кстати, в остальных случаях тоже.
Например, в Rideró автор приходит для того, чтобы издать свою книгу. У него есть с нами какие-то соглашения. Если он начинает вместо книги издавать что-нибудь другое, например Википедию, или пытается абьюзить наш сервис — это не укладывается в то поведение, которое мы ему предлагаем. Наше пространство для этого не предназначено. Оферта, которую подписывает автор, как раз и задает границы того, что он может делать в сервисе, не делая плохо другим «игрокам».
Что важнее: цели или правила?
У вас могут быть более или менее жесткие способы создания реальности игры. Важно, чтобы они понимались участниками одинаково. Чтобы когда один делал предположение, что мы себя ведем вот таким образом, соблюдаем вот такие конвенции или работаем по вот таким гласным правилам, он мог быть уверен, что все остальные участники имеют в виду то же самое и будут вести себя соответственно.
Почему это важно в бизнесе? Потому что таким образом мы очень сильно экономим усилия на коммуникацию, ну и просто прямо поднимаем коэффициент полезного действия — совместно производим больше полезной работы в единицу времени.
Если у нас расходятся представления о том, по каким правилам мы играем и какие умолчания у нас действуют, это приводит к прямо обратному эффекту, который я называю «налогом на мышление». Это, во-первых , все делает гораздо-гораздо более медленным. И качество результата снижается. А еще это делает пространство небезопасным и убивает доверие. А скорость мышления и доверие — это основа того, что делает отношения в ходе создания чего-нибудь полезного продуктивными.
Не очень важно, действуете ли вы по гласным правилам или по умолчаниям, — важно, насколько у вас общие об этом представления.
У меня есть околоигровой пример, он не про игры, но про опыт в близком коммьюнити — фехтовальном. Заодно эта история дает мне возможность посмотреть на еще одну трактовку слова игры — спорт. Так вот, есть такой спорт — историческое фехтование (HEMA). Русские в нем чемпионы мира почти во всех дисциплинах.
При этом в HEMA есть правила, а есть дух исторических источников. И бывает, они входят в конфликт. Например, некоторые из чемпионов России фехтуют просто эффективно — их заботит только победа в рамках правил. Им время от времени предъявляют претензию, что стиль ударов, который они используют, не описан в исторических кодексах, хоть и не нарушает правила. Кто прав — не очень понятно, потому что авторам пространства нужно заявить, ради чего они это делают и что первично.
Первичен стиль, цель или правила — это позиция владельца пространства.
Что здесь важно для бизнеса, так это то, что правила — более жесткий и надежный инструмент. Их можно формально анализировать и явно определять, кто прав в их рамках. Конвенции и стиль — инструменты мягкие и не отделимые от субъективной трактовки. Правила хороши при управлении конкурентными отношениями или при любых других играх вокруг ресурсов. Конвенции гораздо лучше справляются с задачей создания и трансляции культурной нормы, они более гибкие и меньше ограничивают свободу участников.
В бизнесе та же самая история. В JetStyle у нас не так уж и много правил, и если руководствоваться только ими, то можно много всякого веселого наделать. Мы стараемся, чтобы всем было понятно, зачем мы это делаем. Для JetStyle цели важнее правил, но бывают и другие организации.
Движуха
Большинство людей, которые делают бизнесы, продукты или игры, концентрируются на деятельности. В игре это геймплей, в продукте — сервис, а в бизнесе — процессы (то, что мы делаем каждый день). Это не самая важная, но самая специфическая (для предметной области) часть.
Разрешить быть свободным
В игре это основная задача — добиться того, чтобы игрок понял, в каких границах ему интересно проявлять свободу, и начал ей пользоваться. Для этого игрок должен понимать, в какой зоне игры он может проявлять свою свободу, а где эта свобода ограничена правилами или другими средствами. Он должен понимать, какие формы взаимодействия эта среда вообще поддерживает: что и как здесь можно делать, какие есть конвенции, на какие формы деятельности мир готов давать ответ, а какие лежат за фокусом игры. А еще он должен понимать, чего ждут от него организаторы и другие участники игры. Про что это все? Что в этой игре значит «быть хорошим игроком»?
Дать форму действия
Если мы даем игрокам тотальную свободу и говорим: «Ну, делайте, что хотите, что вам фантазия в голову приносит», — они придумывают что-то совсем свободное. И сразу возникает проблема. Никто другой не имеет представления, как на это отвечать, как на это должен реагировать игровой мир, как это согласуется со всем остальным замыслом. Каждое действие повисает в воздухе. Это проблема для большого количества UGC-сервисов , которые дают пользователям слишком свободную форму.
Когда мы просто говорим: «Ну, ребята, это форум или это чат. Делайте, что хотите», — и мы никак не сетапим тематику разговора и форматы высказываний, не указываем, что здесь хорошо, а что плохо, — в большинстве случаев эта история сдыхает. Потому что только технической возможности взаимодействия недостаточно. Нужен еще мотив, стиль, повод, пример и еще куча всего «мягкого».
Игра подразумевает, что любое осмысленное действие игроков должно быть в фокусе замысла. Конвенция с игровым миром, в который они входят, в том, что значимые действия получат значимый ответ. Если игрок значимого ответа от игры не получает, то ему бессмысленно напрягаться ради действий. И он перестает прикладывать значимые усилия. И маховик не раскручивается.
Другими словами, дав игрокам свободу, нужно показать им рамки формата взаимодействия — ограничения их свободы, которые приводят к тому, что на их действия игра будет возвращать заслуживающий интереса ответ.
Погрузить в метафору
У игры, продуктов и бизнесов есть структура деятельности, а есть ракурс, с которого мы на эту структуру смотрим. Метафора. Иногда она явная: например, мы играем в игру по Толкину или в игру по «Королевской битве», или в игру в события вокруг сквера и храма, происходившие весной 2019-го в Екатеринбурге, — неважно. Есть сеттинг и значение того, что в игре происходит. Значение взаимодействий, которыми игроки в ней занимаются. Мы предлагаем на них смотреть через сюжетную рамку и наделяем действия символами. Иногда метафора менее явная. Например, в шашках, казалось бы, нет никакого сеттинга.
При проектировании любого бизнеса или продукта уровень метафоры присутствует, хотим мы того или нет. Мы ее не каждый раз осознаем, поэтому бывает, не проектируем. Но она все равно будет найдена участниками спинным мозгом и будет достаточно серьезно влиять на происходящее.
На организацию можно смотреть по-разному : как на джунгли, в которых каждый сам за себя, как на машину, где все винтики смазаны, как на армию, где все подчинены защите от врага, как на семью, где все любят друг друга, как на храм, где есть что-то святое. Все эти фреймы имеют свою логику и стиль взаимодействия, свои сильные и темные стороны, при том что регламенты под этим могут лежать одни и те же.
А вот пример из проектирования сервиса. У одного из наших любимых клиентов есть проект — школа развития для детей и взрослых. Совершенно очевидно, что мы там работаем с метафорой класса. Почему? Потому что она привычна участникам: педагогам, ученикам, родителям.
Когда мы придумывали для них видеомессенджер, мы могли на него посмотреть с каких-нибудь других ракурсов. Мы могли сказать: «Это театр, здесь есть актер, и все остальные на него смотрят и аплодируют». Примерно в такой метафоре обычно реализуют платформы вебинаров. Или мы могли сказать: «Это спорт, это место, где мы состязаемся, есть победители и проигравшие». Или: «Это конкурс красоты, здесь есть респект и подиум и нужно оказаться в лучах софитов». Но мы говорим: «Это небольшой класс частной школы, здесь есть учитель, и этот учитель дает обратную связь ученикам». И это решение прямо влияет на сценарии поведения, продуктовую стратегию и через нее на бизнес и программную архитектуру.
А вот как метафора работала на разных этапах развития Rideró. Мы поначалу думали, что Rideró — текстовый редактор. Место, где человек работает над книгой. Потом мы решили, что нет. Сменили ракурс и взяли новую метафору — преодоления плотины: есть большое количество авторов, которые хотят и не могут начать продавать книги, им что-то мешает, а мы уничтожаем препятствия между ними и читателем. Делаем так, чтобы движение книги стало свободным. Наш продукт долго строился вокруг этой идеи.
Вовлечь в действие
Это непростая история, потому что люди могут не иметь опыта игры вообще или именно такой игры, или опыта использования сервиса. Или это новичок, который пришел в компанию и не понимает еще, как здесь принято. Вполне вероятно, он будет достаточно инертен — будет сидеть на заднице и ждать, что к нему кто-нибудь обратится, и он на это среагирует.
В начале игры все игроки в таком состоянии. И есть риск попасть в ситуацию, когда не происходит просто ничего, потому что каждый ждет, что действовать начнет кто-то еще, «а мне надо будет просто среагировать». Нужно как-то сделать так, чтобы жизнь началась, экосистема начала кипеть, бурлить: пчелки жужжать, олени бегать. И да, это значит, что первую подачу должна делать игра, компания, продукт – первое свободное действие игрок делает как реакцию на внешнее событие. Потому что реагировать проще, чем действовать самому.
Дать занять позицию
Вот мы добились вовлечения, у нас есть люди, которые освоили формы взаимодействия, свободно действуют и погружены в метафору. Теперь, чтобы игра для них состоялась, у них должна возникнуть позиция относительно происходящего. Игрок должен по-новому посмотреть на себя. В игре это значит, что с ним произошла рефлексия после важного события. В продукте это значит, что он воспользовался ценностью продукта и осознает это.
Например, я купил кроссовки на AliExpress и чувствую, что я очень рачительный чувак и реально сэкономил. Или я купил в Rideró репринт издания, которое давным-давно закончилось, и чувствую радость коллекционера. Или я издал книгу и теперь чувствую, что я настоящий автор, мое творчество приобрело законченную форму. Каждый раз это связка из действия и осознания себя в новом качестве. Квант изменения жизни.
Вывести на кульминацию
Кульминация — это когда игрок проходит через значительную перипетию, через ряд рискованных или неприятных ситуаций для того, чтобы выйти на контрастный этому пик преодоления себя или момент максимального напряжения. И для его личного сюжета это переломный момент, который все меняет.
Кульминация случается не с каждым игроком. Если у вас игра на 300 человек, вы каждому такое не обеспечите. Кроме всего прочего, человек сам для этого должен много сделать. В компьютерных играх, в которых свобода игрока сильно меньше и со сторителлингом всё окей, кульминация получается в более предсказуемых местах. Но не всегда.
Есть достаточно тупой момент, когда где-нибудь , например в «Зельде», выходит финальный босс. Он очень сильный, но к этому времени вы его уже почти без проблем убиваете, просто делаете это долго. И часто опыт с финальным боссом не такой яркий, как с боссом примерно третьим. Вы не переживаете той эмоции, которая в этот момент должна с вами происходить, потому что знаете, что где-то раньше в игре вам было гораздо более сложно, и вы потратили гораздо больше эмоций, и для вас кульминация была там. То есть сюжетная история не совпадает с опытом, который вы переживаете. Это проблема.
С точки зрения продукта я точно знаю, когда кульминация происходит в Rideró. Это когда первая книжка издана и автор держит тираж в руках, особенно если раньше такого не было. Это именно кульминация, потому что человек впервые проходит через очень важный для него личностный опыт, и он очень сильно вовлечен в эту ситуацию. Он много себя вложил, и вот у него есть этот оргазмический или катарсический момент. Он теперь человек в новом качестве. Книга многое в нем изменила, и вот она состоялась.
К сожалению, кульминацию себе может позволить не любой бизнес. Добиться катарсического момента, когда покупаешь что-нибудь в обычном интернет-магазине , сложно. Ты рад, когда открываешь посылку, но чаще всего ты не обалдеть как рад. Просто потому, что это не первая твоя покупка. Не произошло ничего «из ряда вон». Не было никаких сюрпризов — все предсказуемо неплохо. Это если магазин хороший.
Но можно и по-другому. Есть такая книжка «Доставляя счастье» — о предпринимателе по имени Тони Шей, который делал обувной магазин. Она про то, как такую эмоцию создать у людей в ритейле. Или, например, книжка Сета Година «Подарок в придачу» — про это же самое.
В e-commerce кульминация обычно возникает тогда, когда мы превзошли ожидания игрока. То есть мы его вовлекли в действие, он себя инвестировал и потом получил нечто в хорошем смысле удивительное для себя. Он обалдевает: «Неужели так классно/экстремально/небанально/искренне/whatever может быть?»
Когда я слышу слова «создаваемая ценность», «работа, которую делает продукт» и так далее — для меня это значит «вывести игрока на кульминацию».
Рефлексия
Всё, высшая точка пройдена. Мастера сказали: «Конец игры». Или закончился проект. Или клиент получил услугу, и нам надо сделать так, чтобы этот опыт стал его персональной ценностью. Игрок должен понять, что же произошло. Потому что в большинстве случаев, если этого не сделать, человек просто пройдет мимо своего опыта и забудет его.
Рефлексия — это увеличительное стекло, которое сделает все, что с нами произошло до этого, большим и значимым, но мы очень часто им просто не пользуемся.
Устроено оно так: игроки разбираются, что с ними произошло, получают фидбэк на свои действия, дают фидбэк другим. Цель — разобрать происходящее и сделать так, чтобы то полезное и значимое, что в игре произошло, человеком было присвоено.
Наверняка все вы как пользователи сталкивались с опросом net promoter score (индекс потребительской лояльности, NPS). Это когда вас спрашивают, с какой вероятностью вы посоветуете продукт другим людям (от 1 до 10 — никогда не посоветую/точно всем буду советовать). Обычно его делают по-дурацки .
Считается, что NPS собирается, чтобы понимать, насколько люди удовлетворены сервисом. Это достаточно спорная и изрядно дискредитированная метрика, потому что ее часто применяют как попало. Но если им заниматься по уму и вставлять его в нужное место коммуникации, то его основной смысл будет в том, чтобы человек сам себе сказал, насколько был рад этому опыту.
Например, я пользуюсь одной доставкой готовой еды. Не то чтобы я бегаю и об этом всем рассказываю, но если в момент, когда мне понравилось, меня об этом спросить и уточнить, с какой вероятностью я об этом всем расскажу, я отвечу, что с высокой. И после того, как я это скажу, вероятность того, что я это сделаю, сильно увеличится. Потому что я это сказал себе.
Задать вопрос «Как тебе?» — это лучше, чем ничего. Но для того, чтобы на него ответить, человек должен понимать, что потом с этим ответом будет. Я отвечу на вопрос, только если я понимаю, кому я отвечаю и почему мне на него не все равно. Другое дело, если меня спросить НЕ в тот момент, когда я уже благодарен, – скорее всего, на этой самой шкале сам факт вопроса эту вероятность снизит. Потому что «и так все не супер, а они еще и пристают!»
Отдельный важный кусок – коллективная рефлексия общего для нас опыта. Для прикладной игры или для осознания эволюции коллектива особенно важно иметь общую мифологию: представление о том, как построен мир, где в этом мире место человеку, к чему стремиться, что такое хорошо, а что такое плохо, что есть добро, а что зло. После того, как мир состоялся, игра прошла, нам нужно на эти воспоминания посмотреть и в какой-то форме их друг другу рассказать. Я это называю синхронизацией галлюцинаций. Именно после значимого события у нас есть шанс стать коллективом – совместно присвоить опыт. Тогда он становится общим и делает из отдельных людей «нас».
В большинстве компьютерных игр нет рефлексии
Если не говорить о редких исключениях, которые к игре относятся скорее как к искусству, чем как к бизнесу. В России это Коля Дыбовский, а в мире — Хидэо Кодзима. Вот они хотят, чтобы когда в игре что-то происходит, у игроков случался инсайт. А в других случаях компьютерная игра чаще всего пытается сказать: «Сиди, пожалуйста, друг, и не задумывайся. Вот тебе следующий виток дофаминовой петли».
Еще рефлексия есть, например, в компьютерном спорте. Там человек играет для того, чтобы выиграть, и ему рефлексию, условно говоря, делает тренер. Устраивает ему разбор полетов, насколько он удачно играл в StarCraft или какой-нибудь Counter-Strike , чтобы в следующий раз в такое играть успешнее. Но это делается не средствами платформы, а социальным институтом. Рефлексия возникает, когда в качестве ценности выступает опыт человека за пределами системы.
В компьютерных играх похожая рефлексия тоже есть, например, в виде обсуждений в интернете. Но там она просто вырвалась на свободу и не факт, что создатели этого вообще хотели. А если бы рефлексия была встроена в художественное произведение, то это было бы в пространстве, созданном авторами, и это было бы подчинено их цели. Забавно, что во многом задачу рефлексии закрывает летс-плей – просто тем, что является вторичным по поводу игры творческим продуктом.
Конечно, из правила должны быть исключения. Чтобы вы не думали, что я отказываю компьютерным играм во встроенной рефлекссии, вот вам пример. «Цивилизация» Сида Мейера. Старая ее версия (да, мне уже 45, такие у меня, у бумера, примеры). У меня там была любимая часть: когда ты доигрываешь, тебе показывают статистику всего, как ты играл. Дают хронологию всех событий, которые с тобой произошли, ты можешь ее пересматривать. Мне больше всего нравилось, что это заставляет еще раз отнестись к тому, что сейчас произошло. Длится минуты три: ты пыришь на этот график и ничего другого не делаешь, но понимаешь, что сделал не так, и где был поворотный момент.
Рефлексия случается в момент, когда у человека есть время подумать о том, что произошло. Ему дают время и повод. Поэтому у меня за всю жизнь было много идей про перенос метафор «Цивилизации» на нашу реальную жизнь.
Что может пойти не так?
Концентрация только на наборе функций и контенте
Нужно работать с тем, какую позицию занимает игрок насчет контента, а не пытаться руководить сервисом как набором функций.
Если у сервиса есть несколько аудиторий, нужно понимать, с которой из них вы играете в игру. Стоит понимать общую конвенцию: зачем вы этим занимаетесь с игроками, что они будут готовы считать выигрышем, какие формы игры вы им предлагаете, освоили ли они их, как вообще в продукте происходит освоение его формы.
Принуждение
Если игра не добровольна это не игра. Это ад. Если сервисом пользуются против воли, это конечно все на хрен портит и выводит продукт из состояния игры. Моя философия игр касается процессов, в которые люди заходят по своей воле, и конвенция в том, что люди от этого получают ценность и радость. Это не то чтобы истина. Просто я для себя так решил, потому что мне нравится, когда людям нравится. Нравится делать жизнь людей лучше.
Когда мы начинаем абьюзить определение игры: в самом начале говорим, что игра недобровольная, или закрываем выход из нее, — мы начинаем вместо игры делать концлагерь. Из концлагеря ценность узники не извлекают. И поэтому они его любить не будут. Как бы все остальные принципы игры ни были соблюдены – принуждение ко входу в игру и запрет на выход превращают происходящее в кафкианский ад.
Кого-то где-то удержать можно только ценностью продукта: вот здесь цветочки, вы пчелы, летите к цветочкам, потому что вам нужно сырье для мёда. А когда мы пытаемся просто загнать пчел в банку и закрыть крышку, то они не будут рады. Игры не будет, всё это не будет работать.
Чтобы продукт состоялся, игрокам нужно понимать, в чем их свобода, выигрыш, от чего можно отказаться и ради чего действовать. А главное, если пользователи действуют успешно, то как сервис им эту успешность показывает.
Главный тезис
Вообще-то, проблематизация и рефлексия важнее, чем движуха. Если мы нормально человека погружаем в деятельность, а потом выводим, то от самой деятельности по сути нужно просто, чтобы она была достаточно яркая и азартная. И, в общем-то , всё.
Если деятельность людей захватывает, они погружаются в проблему и безопасно из этой всей истории выходят, мы помогаем им разобраться с опытом, то они умудряются извлекать массу всякого прикольного из совершенно ерундовых переживаний.
Например, несколько лет назад была мода на веревочные тренинги, на которых организаторы просто заставляли вас выполнять коллективные упражнения с помощью веревок. Всякий раз это требовало синхронного взаимодействия и работало с эмоциональной базой людей. Это, вообще-то, очень нехитрое мероприятие, которое обалдеть как работает, если оно глубоко проблематизировано и внимательно отрефлексировано. И «хорошие» веревочные тренинги отличались от «так себе» не тем, какие наборы трюков с веревками имели организаторы. Они отличались тем, как устроен вход и выход. Как людей готовили это воспринимать до и как помогали им присвоить опыт после.
Еще пример. Если ехать на совместную велопокатушку, при этом проведя погружение, а потом рефлексию, уверяю, у вас будет масса всяких выводов и тонна пользы. Хотя вы будете просто кататься на великах.
Я на этом делаю такой акцент, потому что, вообще-то, в большинстве случаев делают строго наоборот. В создании продукта концентрируются непосредственно на интерфейсе, а не на сюжете, который происходит с человеком до и после его использования. В бизнесе бьются за идеальные регламенты. В играх занимаются непосредственно геймплеем.
Введение


Эта статья познакомит вас с широким диапазоном концепций искусственного интеллекта в играх («игрового ИИ»), чтобы вы понимали, какие инструменты можно использовать для решения задач ИИ, как они работают совместно и с чего можно начать их реализацию в выбранном движке.
Я буду предполагать, что вы знакомы с видеоиграми, немного разбираетесь в таких математических концепциях, как геометрия, тригонометрия и т.д. Большинство примеров кода будет записано псевдокодом, поэтому вам не потребуется знание какого-то конкретного языка.
Что же такое «игровой ИИ»?
Игровой ИИ в основном занимается выбором действий сущности в зависимости от текущих условий. В традиционной литературе по ИИ называет это управлением «интеллектуальными агентами». Агентом обычно является персонаж игры, но это может быть и машина, робот или даже нечто более абстрактное — целая группа сущностей, страна или цивилизация. В любом случае это объект, следящий за своим окружением, принимающий на основании него решения и действующий в соответствии с этими решениями. Иногда это называют циклом «восприятие-мышление-действие» (Sense/Think/Act):
- Восприятие: агент распознаёт — или ему сообщают — информацию об окружении, которая может влиять на его поведение (например, находящиеся поблизости опасности, собираемые предметы, важные точки и так далее)
- Мышление: агент принимает решение о том, как поступить в ответ (например, решает, достаточно ли безопасно собрать предметы, стоит ли ему сражаться или лучше сначала спрятаться)
- Действие: агент выполняет действия для реализации своих решений (например, начинает двигаться по маршруту к врагу или к предмету, и так далее)
- … затем из-за действий персонажей ситуация изменяется, поэтому цикл должен повториться с новыми данными.
Игры необычны тем, что для извлечения этой информации им не нужна сложная система, поскольку она является неотъемлемой частью симуляции. Нет необходимости выполнять алгоритмы распознавания изображений, чтобы обнаружить врага перед собой; игра знает, что там есть враг и может передать эту информацию непосредственно процессу принятия решений. Поэтому «восприятие» в этом цикле обычно сильно упрощено, а вся сложность возникает в реализации «мышления» и «действия».
Ограничения разработки игрового ИИ
Игровой ИИ обычно принимает во внимание следующие ограничения:
- В отличие от алгоритма машинного обучения, он обычно не тренируется заранее; при разработке игры непрактично писать нейронную сеть для наблюдения за десятками тысяч игроков, чтобы найти наилучший способ играть против них, потому что игра ещё не выпущена и игроков у неё нет!
- Обычно предполагается, что игра должна развлекать и бросать игроку вызов, а не быть «оптимальной» — поэтому даже если и можно обучить агентов противостоять игрокам наилучшим образом, то чаще всего дизайнерам нужно от них совершенного другое.
- Часто к агентам предъявляют требование «реалистичного» поведения, чтобы игроки ощущали, что соревнуются с человекоподобными противниками. Программа AlphaGo оказалась намного лучше, чем люди, но выбираемые ею ходы настолько далеки от традиционного понимания игры, что опытные противники говорили о ней как об игре против инопланетянина. Если игра симулирует противника-человека, то обычно это нежелательно, поэтому алгоритм нужно настроить таким образом, чтобы он принимал правдоподобные решения, а не идеальные.
- ИИ должен выполняться «в реальном времени». В этом контексте это означает, что алгоритм не может для принятия решения монополизировать ресурсы процессора на длительное время. Даже 10 миллисекунд на принятие решения — это слишком много, потому что большинство игр имеют всего 16-33 миллисекунды на выполнение всех операций для следующего кадра графики.
- В идеале хотя бы часть системы должна зависеть от данных, а не быть жёстко заданной, чтобы не владеющие программированием разработчики могли быстрее вносить изменения.
Простое принятие решений
Давайте начнём с очень простой игры, например с Pong. Задача игрока заключается в перемещении «ракетки», чтобы мячик отскакивал от неё, а не пролетал мимо. Правила похожи на теннисные — ты проигрываешь, если пропустил мяч. У ИИ есть относительно простая задача принятия решений о выборе направления движения ракетки.
Жёстко заданные условные конструкции
Если бы мы хотели написать ИИ для управления ракеткой, то существует интуитивно понятное и простое решение — просто постоянно двигать ракетку так, чтобы она находилась под мячом. Когда мяч достигает ракетки, она уже находится в идеальном положении и может отбить его.
Простой алгоритм для этого, выраженный в псевдокоде, может быть таким:
в каждом кадре/обновлении, пока игра выполняется: если мяч слева от ракетки: двигать ракетку влево иначе если мяч справа от ракетки: двигать ракетку вправо
Если считать, что ракетка может двигаться с не меньшей скоростью, чем мяч, то это будет идеальный алгоритм для ИИ-игрока в Pong. В случаях, когда для обработки есть не так много данных «восприятия» и мало действий, которые может выполнить агент, нам не требуется ничего более сложного.
Этот подход так прост, что в нём едва просматривается весь цикл «восприятие-мышление-действие». Но он есть.
- «Восприятие» — это два оператора «если». Игра знает, где находятся мяч и ракетка. Поэтому ИИ запрашивает у игры их позиции, таким образом «чувствуя», находится ли мяч слева или справа.
- «Мышление» тоже встроено в два оператора «если». В них находится два решения, которые в данном случае взаимно исключают друг друга, приводящие к выбору одного из трёх действий — двигать ракетку влево, двигать её вправо или ничего не делать, если ракетка уже расположена верно.
- «Действие» заключается в конструкциях «двигать ракетку влево» или «двигать ракетку вправо». В зависимости от способа реализации игры это может принимать вид мгновенного перемещения положения ракетки или задания скорости и направления ракетки, чтобы её можно было сдвинуть должным образом в другом коде игры.
Деревья решений
Этот пример с Pong на самом деле аналогичен формальной концепции ИИ под названием «дерево решений». Это система, в которой решения выстроены в форме дерева и алгоритм должен обходить его, чтобы достичь «листа», содержащего окончательное решение о выбираемом действии. Давайте нарисуем графическое представление дерева решений для алгоритма ракетки Pong с помощью блок-схемы:

Видно, что она напоминает дерево, только перевёрнутое!
Каждую часть дерева решений обычно называют «узлом», потому что в ИИ для описания подобных структур используется теория графов. Каждый узел может быть одного из двух типов:
- Узлы решений: выбор из двух альтернатив на основании проверки какого-то условия. Каждая альтернатива представлена в виде собственного узла;
- Конечные узлы: выполняемое действие, представляющее собой окончательное решение, принимаемое деревом.
На первый взгляд преимущество дерева решений неочевидно, потому что оно выполняет абсолютно ту же работу, что и операторы «если» из предыдущего раздела. Но тут есть очень общая система, в которой каждое решение имеет ровно 1 условие и 2 возможных результата, что позволяет разработчику строить ИИ из данных, представляющих решения в дереве, и избегая жёсткого прописывания его в коде. Легко представить простой формат данных для описания подобного дерева:
| Номер узла | Решение (или «конец») | Действие | Действие |
| 1 | Мяч слева от ракетки? | Да? Проверить узел 2 | Нет? Проверить узел 3 |
| 2 | Конец | Сдвинуть ракетку влево | |
| 3 | Мяч справа от ракетки? | Да? Перейти к узлу 4 | Нет? Перейти к узлу 5 |
| 4 | Конец | Сдвинуть ракетку вправо | |
| 5 | Конец | Ничего не делать | |
С точки зрения кода, нам нужно заставить систему считать каждую из этих строк, создать для каждой узел, прицепить логику решений на основании второго столбца и прицепить дочерние узлы на основании третьего и четвёртого столбцов. Нам по-прежнему нужно вручную жёстко прописывать условия и действия, но теперь мы можем вообразить более сложную игру, в которой можно добавлять новые решения и действия, а также настраивать весь ИИ изменением единственного текстового файла, в котором содержится определение дерева. Мы можем передать файл геймдизайнеру, который получит возможность настраивать поведение без необходимости перекомпиляции игры и изменения кода — при условии, что в коде уже есть полезные условия и действия.
Деревья решений могут быть очень мощными, когда их конструируют автоматически на основе большого множества примеров (например, с помощю алгоритма ID3). Он делает их эффективным и высокопроизводительным инструментом для классификации ситуации на основании входящих данных, но эта тема находится за пределами области создания дизайнерами простых систем выбора действий для агентов.
Скриптинг
Выше мы рассмотрели систему дерева решений, в которой используются заранее созданные условия и действия. Разработчик ИИ может перестраивать дерево любым нужным ему образом, но он должен рассчитывать на то, что программист уже создал все необходимые ему условия и действия. А что, если мы дадим дизайнеру более мощные инструменты, позволяющие ему создавать собственные условия, а может и свои действия?
Например, вместо того, чтобы заставлять кодера писать условия «Мяч слева от ракетки?» и «Мяч справа от ракетки?», он может просто создать систему, в которой дизайнер самостоятельно пишет условия проверки этих значений. В результате данные дерева решений могут выглядеть вот так:
| Номер узла | Решение (или «конец») | Решение | Действие |
| 1 | ball.position.x < paddle.position.x | Да? Проверить узел 2 | Нет? Проверить узел 3 |
| 2 | Конец | Двигать ракетку влево | |
| 3 | ball.position.x > paddle.position.x | Да? Проверить узел 4 | Нет? Проверить узел 5 |
| 4 | Конец | Двигать ракетку вправо | |
| 5 | Конец | Ничего не делать | |
То же самое, что и раньше, но теперь в решениях есть собственный код, похожий на условную часть оператора «если». Код будет считывать из второго столбца узлы решений и вместо поиска конкретного условия (например, «мяч слева от ракетки?»), вычислять условное выражение и возвращать true или false. Это можно реализовать встраиванием скриптового языка, наподобие Lua или Angelscript, который позволяет разработчику брать объекты из игры (например мяч и ракетку) и создавать переменные, доступные из скрипта (например ball.position). На скриптовом языке обычно проще писать, чем на C++, и он не требует полного этапа компиляции, поэтому хорошо подходит для внесения быстрых изменений в логику игры и позволяет менее технически подкованным участникам команды создавать игровые функции без вмешательства кодера.
В показанном выше примере скриптовый язык используется только для вычисления условного выражения, но можно также описывать в скрипте и конечные действия. Например, данные действия вида «двигать ракетку вправо» могут стать конструкцией скрипта наподобие ball.position.x += 10 , то есть действие тоже задаётся в скрипте без написания программистом кода функции MovePaddleRight.
Если сделать ещё один шаг вперёд, то можно (и это часто делается) дойти до логического завершения и написать всё дерево решений на скриптовом языке, а не как список строк данных. Это будет код, который похож на показанные выше условные конструкции, только они не «жёстко заданы» — они находятся во внешних файлах скриптов, то есть их можно изменять без рекомпиляции всей программы. Часто даже возможно изменять файл скрипта во время выполнения игры, что позволяет разработчикам быстро тестировать различные подходы к реализации ИИ.
Реакция на события
Показанные выше примеры предназначены для покадрового выполнения в простых играх наподобие Pong. Идея заключается в том, что они непрерывно выполняют цикл «восприятие-мышление-действие» и продолжают действовать на основании последнего состояния мира. Но в более сложных играх вместо вычисления всего чаще разумнее реагировать на «события», то есть на важные изменения в окружении игры.
Это не особо применимо к Pong, поэтому давайте выберем другой пример. Представьте игру-шутер, в которой враги неподвижны, пока не обнаружат игрока, после чего начинают выполнять действия в зависимости от своего класса — рукопашные бойцы могут броситься к игроку, а снайперы остаются на расстоянии и пытаются прицелиться. По сути это является простой реактивной системой — «если видим игрока, то что-то делаем» — но её можно логически разделить на событие («видим игрока») и реакцию (выбираем отклик и выполняем его).
Это возвращает нас к циклу «восприятие-мышление-действие». У нас может быть фрагмент кода, являющийся кодом «восприятия», который проверяет в каждом кадре, видит ли враг игрока. Если нет, то ничего не происходит. Но если он видит, то это создаёт событие «видим игрока». В коде будет отдельная часть, в которой говорится: «когда происходит событие „видим игрока“, то делаем „xyz“», а «xyz» — это любой отклик, которым мы хотим обрабатывать мышление и действие. Для персонажа-бойца можно подключить к событию «видим игрока» отклик «бежать и атаковать». Для снайпера мы подключим к этому событию функцию отклика «спрятаться и прицелиться». Как и в предыдущих примерах, мы можем создавать такие ассоциации в файле данных, чтобы их можно было быстро изменять без пересборки движка. Кроме того, возможно (и это часто используется) писать такие функции откликов на скриптовом языке, чтобы могли создавать сложные решения при возникновении событий.
Улучшенное принятие решений
Хотя простые реактивные системы очень мощны, существует множество ситуаций, когда их недостаточно. Иногда нам нужно принимать разные решения на основании того, чем занимается агент в текущий момент, и представить это в виде условия бывает неудобно. Иногда просто существует слишком много условий, чтобы эффективным образом представить их в виде дерева решений или скрипта. Иногда нам нужно думать заранее и оценивать, как изменится ситуация, прежде чем принимать решение о следующем ходе. Для таких задач нужны более сложные решения.
Конечные автоматы
Конечный автомат (КА, finite state machine, FSM) — это способ сказать иными словами, что какой-то объект — допустим, один из наших ИИ-агентов — в данный момент находится в одном из нескольких возможных состояний, и что он может переходить из одного состояния в другое. Существует конечное количество таких состояний, отсюда и название. Примером из реального мира может служить множество огней светофора, переключающееся с красного на жёлтый, потом на зелёный, и снова обратно. В разных местах есть разные последовательности огней, но принцип одинаков — каждое состояние что-то обозначает («стой», «едь», «стой, если возможно» и т.д.), в любой момент времени существует только одно состояние, а переходы между ними основаны на простых правилах.
Это хорошо применимо к NPC в играх. У охранника могут быть следующие чётко разделённые состояния:
- Патрулирование
- Нападение
- Бегство
- Если охранник видит противника, он нападает
- Если охранник нападает, но больше не видит противника, то возвращается к патрулированию
- Если охранник нападает, но его серьёзно ранили, то он сбегает
- Ожидание (между патрулированием)
- Поиск (когда ранее замеченный враг спрятался)
- Бегство за помощью (когда враг замечен, но он слишком силён, чтобы сражаться с ним в одиночку)
Рано или поздно длинный список «if then
» становится слишком неуклюжим, и помочь здесь может формализованный подход к реализации состояний и к переходов между ними. Для этого мы рассматриваем все состояния и под каждым состоянием пперечисляем все переходы к другим состояниям вместе с необходимыми для них условиями. Также нам нужно указать исходное состояние, чтобы мы знали, с чего начать, прежде чем применять другие условия.
| Состояние | Условие перехода | Новое состояние |
| Ожидание | ожидал в течение 10 секунд | Патрулирование |
| враг видим и враг слишком силён | Поиск помощи | |
| враг видим и здоровья много | Нападение | |
| враг видим и здоровья мало | Бегство | |
| Патрулирование | завершён маршрут патрулирования | Ожидание |
| враг видим и враг слишком силён | Поиск помощи | |
| враг видим и здоровья много | Нападение | |
| враг видим и здоровья мало | Бегство | |
| Нападение | врага не видно | Ожидание |
| здоровья мало | Бегство | |
| Бегство | врага не видно | Ожидание |
| Поиск | искал в течение 10 секунд | Ожидание |
| враг видим и враг слишком силён | Поиск помощи | |
| враг видим и здоровья много | Нападение | |
| враг видим и здоровья мало | Бегство | |
| Поиск помощи | друг видим | Нападение |
| Начальное состояние: ожидание | ||
Такая схема называется таблицей перехода между состояниями. Она является сложным (и непривлекательным) способом представления КА. По этим данным также можно нарисовать диаграмму и получить сложное графическое представление того, как может выглядеть поведение NPC.

Она ухватывает саму суть принятия решений для агента на основании ситуации, в которой он находится. Каждая стрелка обозначает переход между состояниями, если условие рядом со стрелкой истинно.
При каждом обновлении (или «цикле») мы проверяем текущее состояние агента, просматриваем список переходов и если условие перехода соблюдено, то переходим к новому состоянию. Состояние «Ожидание» проверяет в каждом кадре или цикле, истекло ли время 10-секундного таймера. Если истекло, то оно запускает переход к состоянию «Патрулирование». Аналогичным образом состояние «Нападение» проверяет, не мало ли здоровья у агента, и если это так, то выполняет переход в состояние «Бегство».
Так обрабатываются переходы между состояниями — но как насчёт поведений, связанных с самими состояниями? С точки зрения выполнения самих действий для состояния обычно существует два вида прикрепления действий к КА:
- Действия для текущего состояния выполняются периодически, например, в каждом кадре или «цикле».
- Действия выполняются при переходе от одного состояния к другому.
Пример первого вида: состояние «Патрулирование» в каждом кадре или цикле продолжает перемещать агента по маршруту патрулирования. Состояние «Нападение» в каждом кадре или цикле пытается начать атаку или переместить в позицию, откуда она возможна. И так далее.
Пример второго вида: рассмотрим переход «если враг видим и враг слишком силён → Поиск помощи». Агент должен выбрать, куда двигаться для поиска помощи, и хранить эту информацию так, чтобы состояние «Поиск помощи» знало, куда двигаться. Аналогично, в состоянии «Поиск помощи», когда помощь найдена, агент снова возвращается в состояние «Нападение», но в этот момент он хочет сообщить дружественному персонажу об угрозе, поэтому может существовать действие «сообщить другу об опасности», выполняемое при этом переходе.
И здесь мы снова можем рассмотреть эту систему с точки зрения «восприятия-мышления-действия». Восприятие встроено в данные, используемые логикой переходов. Мышление встроено в переходы, доступные для каждого состояния. А действие выполняется действиями, совершаемыми периодически в состоянии или при переходе между состояниями.
Эта простая система хорошо работает, хотя иногда постоянный опрос условий перехода может быть затратным процессом. Например, если каждому агенту нужно выполнять в каждом кадре сложные вычисления для определения видимости врагов и принятия решения о переходе от патрулирования к нападению, то на это может уходить много процессорного времени. Как мы видели раньше, можно воспринимать важные изменения состояния мира как «события», которые обрабатываются после того, как они произошли. Поэтому вместо того, чтобы в каждом кадре явным образом проверять условие перехода «может ли мой агент увидеть игрока?», мы можем создать отдельную систему видимости, выполняющую эти проверки чуть менее часто (например, 5 раз в секунду), и создающую событие «игрок видим» при срабатывании проверки. Оно передаётся конечному автомату, у которого теперь есть условие перехода «Получено событие „игрок видим“», и которое реагирует на него соответствующим образом. Получившееся поведение будет аналогичным, за исключением едва заметной (и даже увеличивающей реалистичность) задержки реакции, но производительность увеличится благодаря переносу «восприятия» в отдельную часть программы.
Иерархические конечные автоматы
Всё это хорошо, но с большими конечными автоматами становится очень неудобно работать. Если мы хотим расширить состояние «Нападение», заменив его на отдельные состояния «Рукопашная атака» и «Атака издалека», то нам придётся изменять входящие переходы от каждого состояния, настоящего и будущего, которому нужна возможность перехода в состояние «Нападение».
Вероятно, вы также заметили, что в нашем примере есть много дублируемых переходов. Большинство переходов в состоянии «Ожидание» идентичны переходам в состоянии «Патрулирование», и было бы неплохо избежать дублирования этой работы, особенно если мы захотим добавить ещё больше похожих состояний. Будет логично соединить «Ожидание» и «Патрулирование» в какую-нибудь группу «Небоевые состояния», имеющую только один общий набор переходов в боевые состояния. Если мы представим эту группу как состояние, то сможем рассматривать «Ожидание» и «Патрулирование» «подсостояниями» этого состояния, что позволит нам более эффективно описывать всю систему. Пример использования отдельной таблицы переходов для нового небоевого подсостояния:
Основные состояния:
| Состояние | Условие перехода | Новое состояние |
| Небоевые | враг видим и враг слишком силён | Поиск помощи |
| враг видим и здоровья много | Нападение | |
| враг видим и здоровья мало | Бегство | |
| Нападение | врага не видно | Небоевое |
| мало здоровья | Бегство | |
| Бегство | врага не видно | Небоевое |
| Поиск | искал в течение 10 секунд | Небоевое |
| враг видим и враг слишком силён | Поиск помощи | |
| враг видим и здоровья много | Нападение | |
| враг видим и здоровья мало | Бегство | |
| Поиск помощи | друг видим | Нападение |
| Начальное состояние: небоевое | ||
Небоевое состояние:

По сути это та же система, только теперь здесь есть небоевое состояние, заменяющее «Патрулирование» и «Ожидание», которое само по себе является конечным автоматом с двумя подсостояниями патрулирования и ожидания. Если каждое состояние потенциально может содержать в себе конечный автомат подсостояний (а эти подсостояния тоже могут содержать в себе собственный конечный автомат, и так далее), то у нас получился иерархический конечный автомат (Hierarchical Finite State Machine, HFSM). Группируя небоевые поведения, мы отсекаем кучу излишних переходов, и можем сделать то же самое для любых новых состояний, которые могут иметь общие переходы. Например, если в будущем мы расширим состояние «Нападение» до состояний «Рукопашная атака» и «Атака снарядом», они могут быть подсостояниями, переход между которыми выполняется на основании расстояния до врага и наличия боеприпасов, имеющими общие выходные переходы на основании уровней здоровья и прочего. Таким образом, минимумом дублированных переходов можно представить сложные поведения и подповедения.
Деревья поведений
С помощью HFSM мы получили способность создавать достаточно сложные множества поведений довольно интуитивно понятным способом. Однако сразу заметно, что принятие решений в виде правил переходов тесно связано с текущим состоянием. Во многих играх требуется именно это. А аккуратное использование иерархии состояний позволяет снизить количество дублируемых переходов. Но иногда нам нужны правила, применяемые вне зависимости от текущего состояния, или применяемые почти во всех состояниях. Например, если здоровье агента снизилось до 25%, он может захотеть убежать, вне зависимости от того, находится ли он в бою, или ожидает, или говорит, или находится в любом другом состоянии. Мы не хотим запоминать, что нужно дописывать это условие к каждому состоянию, которое мы, возможно, добавим персонажу в будущем. Чтобы когда дизайнер позже скажет, что хочет изменить пороговое значение с 25% до 10%, нам не пришлось бы перебирать и изменять каждый соответствующий переход.
Идеальной в такой ситуации была система, в которой решения о том, в каком состоянии находиться, существуют отдельно от самих состояний, чтобы мы могли изменить всего лишь один элемент, а переходы всё равно обрабатывались правильно. Именно здесь нам пригодятся деревья поведений.
Существует несколько способов реализации деревьев поведений, но суть для большинства одинакова и очень похожа на упомянутое выше дерево решений: алгоритм начинает работу с «корневого узла», и в дереве есть узлы, обозначающие решения или действия. Однако здесь существуют ключевые отличия:
- Узлы теперь возвращают одно из трёх значений: «успешно» (если работа выполнена), «безуспешно» (если выполнить её не удалось), или «выполняется» (если работа всё ещё выполняется и полностью не закончилась успехом или неудачей).
- Теперь у нас нет узлов решений, в которых мы выбираем из двух альтернатив, а есть узлы-«декораторы», имеющие единственный дочерний узел. Если они «успешны», то выполняют свой единственный дочерний узел. Узлы-декораторы часто содержат условия, определяющие, окончилось ли выполнение успехом (значит, нужно выполнить их поддерево) или неудачей (тогда делать ничего не нужно). Также они могут возвращать «выполняется».
- Выполняющие действия узлы возвращают значение «выполняется», чтобы обозначить происходящие действия.

При использовании этой структуры нет необходимости в явном переходе из состояний «Ожидание» или «Патрулирование» в состояния «Нападение» или любые другие — если дерево обходится сверху вниз и слева направо, то правильное решение принимается на основании текущей ситуации. Если враг видим и у персонажа мало здоровья, то дерево завершит выполнение на узле «Бегство», вне зависимости от предыдущего выполненного узла («Патрулирование», «Ожидание», «Нападение» и т.д.).
Можно заметить, что у нас пока нет перехода для возврата в состояние «Ожидание» из «Патрулирования» — и тут нам пригодятся безусловные декораторы. Стандартным узлом-декоратором является «Повтор» (Repeat) — у него нет условий, он просто перехватывает дочерний узел, возвращающий «успешно» и снова выполняет дочерний узел, возвращая «выполняется». Новое дерево выглядит так:

Деревья поведения довольно сложны, потому что часто существует множество различных способов составления дерева, а поиск правильной комбинации декоратора и составляющих узлов может быть хитрой задачей. Существуют также проблемы с тем, как часто нужно проверять дерево (хотим ли мы обходить его каждый кадр или когда происходит что-то, способное повлиять на условия?) и со способом хранения состояния относительно узлов (как мы узнаем, что ждали 10 секунд? Как мы узнаем, сколько узлов было выполнено в последний раз, чтобы правильно завершить последовательность?) Поэтому существует множество разных реализаций. Например, в некоторых системах наподобие системы деревьев поведений Unreal Engine 4 узлы-декораторы заменены на строковые декораторы, которые проверяют дерево только при изменении условий декоратора и предоставляют «сервисы», которые можно подключить к узлам и обеспечивать периодические обновления даже когда дерево не проверяется заново. Деревья поведений — это мощные инструменты, но изучение их правильного использования, особенно с учётом множества разных реализаций, может быть пугающей задачей.
Системы на основе полезности (Utility)
В некоторых играх требуется существование множества различных действий, поэтому им требуются более простые централизованные правила переходов, но при этом не требуется мощь полной реализации дерева поведений. Вместо создания явного набора выборов или дерева потенциальных действий с подразумеваемыми резервными позициями, задаваемыми структурой дерева, возможно, лучше просто исследовать все действия и выбирать то, которое наиболее применимо прямо сейчас?
Именно этим и занимается системы на основе полезности — это такие системы, в которых в распоряжении агента есть множество действий, и он выбирает выполнение одного на основании относительной полезности каждого действия. Полезность здесь — это произвольная мера важности или желательности для агента выполнения этого действия. Благодаря написанию функций полезности для вычисления полезности действия на основании текущего состояния агента и его окружения, агент может проверять значения полезности и выбирать наиболее подходящее в данный момент состояние.
Это тоже очень сильно напоминает конечный автомат, за исключением того, что переходы определяются оценкой каждого потенциального состояния, в том числе и текущего. Стоит заметить, что в общем случае мы выбираем переход к наиболее ценному действию (или нахождение в нём, если мы уже выполняем это действие), но для большей вариативности это может быть взвешенный случайный выбор (отдающий приоритет самому ценному действию, но допускающий выбор других), выбор случайного действия из пяти лучших (или любого другого количества), и т.д.
Стандартная система на основе полезности назначает некий произвольный интервал значений полезности — допустим от 0 (совершенно нежелательно) до 100 (абсолютно желательно), а у каждого действия может быть набор факторов, влияющих способ вычисления значения. Возвращаясь к нашему примеру с охранником, можно представить нечто подобное:
В нашем примере действия возвращают или постоянное значение полезности, или одно из двух постоянных значений полезности. В более реалистичной системе используется возврат значения из непрерывного интервала значений. Например действие Бегство может возвращать более высокие значения полезности, если здоровье агента ниже, а действие Нападение может возвращать более низкие значения полезности, если враг слишком силён. Это позволит Бегству иметь приоритет перед Нападением в любой ситуации, когда агент чувствует, что ему недостаточно здоровья, чтобы сражаться с врагом. Это позволяет изменять относительные приоритеты действий на основании любого количества критериев, что может сделать такой подход более гибким, чем дерево поведений или КА.
У каждого действия обычно есть несколько условий, влияющих на вычисление полезности. Чтобы не задавать всё жёстко в коде, можно записывать их на скриптовом языке или как серию математических формул, собранных вместе понятным способом. Гораздо больше информации об этом есть в лекциях и презентациях Дэйва Марка (@IADaveMark).
В некоторых играх, которые пытаются моделировать повседневную жизнь персонажа, например, в The Sims, добавляется ещё один слой вычислений, в котором у агента есть «стремления» или «мотивации», влияющие на значения полезности. Например, если у персонажа есть мотивация «Голод», то она со временем может увеличиваться, и вычисление полезности для действия «Поесть» будет возвращать всё более и более высокие значения, пока персонаж не сможет выполнить это действие, снизив уровень голода, а действие «Поесть» снижается до нулевого или околонулевого значения полезности.
Идея выбора действий на основе системы очков достаточно прямолинейна, поэтому очевидно, что можно использовать принятие решений на основе полезности в других процессах принятия решений ИИ, а не заменять их ею полностью. Дерево решений может запрашивать значение полезности своих двух дочерних узлов и выбирать узел с наибольшим значением. Аналогичным образом дерево поведений может иметь композитный узел полезности, который подсчитывает полезность, чтобы выбрать выполняемый дочерний узел.
Движение и навигация
В наших предыдущих примерах были или простая ракетка, которой мы приказывали двигаться влево-вправо, или персонаж-охранник, которому всегда приказывали патрулировать или нападать. Но как именно нам управлять движением агента в течение промежутка времени? Как нам задавать скорость, избегать препятствий, планировать маршрут, когда до конечной точки нельзя добраться напрямую? Сейчас мы рассмотрим эту задачу.
Steering
На самом простом уровне часто разумно работать с каждым агентом так, как будто у него есть значение скорости, определяющее скорость и направление его движения. Эта скорость может измеряться в метрах в секунду, в милях в час, в пикселях в секунду и так далее. Если вспомнить наш цикл «восприятие-мышление-действие», то можно представить, что «мышление» может выбирать скорость, после чего «действие» прикладывает эту скорость к агенту, перемещая его по миру. Обычно в играх есть система физики, выполняющая эту задачу самостоятельно, изучающая значение скорости каждой сущности и соответствующим образом меняющая позицию. Поэтому часто можно возложить такую работу на эту систему, оставив ИИ только задачу выбора скорости агента.
Если мы знаем, где хочет находиться агент, то нам нужно использовать нашу скорость для перемещения агента в этом направлении. В тривиальном виде у нас получится такое уравнение:
desired_travel = destination_position – agent_position
Представим 2D-мир, в котором агент находится в координатах (-2,-2), а целевая точка — примерно на северо-востоке, в координатах (30, 20), то есть для попадания туда нужно перемещение (32, 22). Давайте будем считать, что эти позиции указаны в метрах. Если мы решим, что агент может двигаться со скоростью 5 м/с, то уменьшим масштаб вектора перемещения до этой величины и увидим, что нам нужно задать скорость примерно (4.12, 2.83). Двигаясь на основании этого значения, агент прибудет в конечную точку чуть менее чем за 8 секунд, как и ожидалось.
Вычисления можно выполнить заново в любой момент времени. Например, если агент находится на полпути к цели, то желательное перемещение будет в два раза меньше, но после масштабирования до максимальной скорости агента в 5 м/с скорость остаётся той же. Это работает и для подвижных целей (в пределах разумного), что позволяет агенту делать небольшие корректировки по ходу движения.
Однако часто нам нужно больше контроля. Например, нам может потребоваться медленно увеличивать скорость, как будто персонаж сначала стоял неподвижно, потом перешёл на шаг, а позже побежал. С другой стороны нам может понадобиться замедлить его, когда он приближается к цели. Часто такие задачи решаются с помощью так называемых «steering behaviours», имеющих собственные названия наподобие Seek, Flee, Arrival и так далее. (На Хабре есть про них серия статей: https://habr.com/post/358366/.) Их идея заключается в том, что к скорости агента можно прикладывать силы ускорения на основании сравнения позиции агента и текущей скорости движения к цели, создавая различные способы движения к цели.
У каждого поведения собственное, слегка отличающееся предназначение. Seek и Arrive служат для движения агента к точке назначения. Obstacle Avoidance и Separation помогают агенту совершать небольшие корректирующие движения для обхода небольших препятствий между агентом и его точкой назначения. Alignment и Cohesion заставляют агентов двигаться вместе, имитируя стадных животных. Любые вариации разных steering behaviours можно сочетать вместе, часто в виде взвешенной суммы, для создания суммарного значения, учитывающего все эти разные факторы и создающего единый результирующий вектор. Например, агент может использовать поведение Arrival вместе с поведениями Separation и Obstacle Avoidance, чтобы держаться подальше от стен и других агентов. Этот подход хорошо работает в открытых окружениях, не слишком сложных и переполненных.
Однако в более сложных окружениях простое сложение выходных значений поведений работает не очень хорошо — иногда движение рядом с объектом происходит слишком медленно, или агент застревает, когда поведение Arrival хочет пройти сквозь препятствие, а поведение Obstacle Avoidance behaviour отталкивает агента в сторону, с которой он пришёл. Поэтому иногда имеет смысл рассмотреть вариации steering behaviours, которые более сложны, чем простое сложение всех значений. Одно из семейств таких подходов заключается в иной реализации — мы рассматриваем не каждое из поведений, дающих нам направление, с последующим их комбинированием для получения консенсуса (который сам по себе может быть неадекватным). Вместо этого мы рассматриваем движение в нескольких различных направлениях — например, в восьми направлениях компаса, или в 5-6 точках перед агентом, после чего выбираем наилучшую.
Тем не менее, в сложных средах с наличием тупиков и вариантов выбора поворотов нам нужно будет что-то более совершенное, и мы вскоре к этому перейдём.
Поиск путей
Steering behaviours отлично подходят для простого движения по достаточно открытой территории, например, футбольному полю или арене, на которой добраться из А в Б можно по прямой линии с небольшими корректировками для избегания препятствий. Но что, если маршрут к конечной точке более сложен? Тогда нам понадобится «поиск путей» (pathfinding) — исследование мира и прокладывание пути по нему для того, чтобы агент попал в конечную точку.
Простейший способ — наложить на мир сетку, и для каждой клетки рядом с агентом смотреть на соседние клетки, в которые мы можем переместиться. Если одна из них является нашей конечной точкой, то пройти маршрут обратно, от каждой клетки к предыдущей, пока не доберёмся до начала, получив таким образом маршрут. В противном случае повторять процесс с достижимыми соседями предыдущих соседей, пока не найдём конечную точку или у нас не кончатся клетки (это будет означать, что маршрута нет). Формально такой подход называется алгоритмом поиска в ширину (Breadth-First Search, BFS), потому что на каждом шаге он смотрит во всех направлениях (то есть «в ширину»), прежде чем переместить поиски наружу. Пространство поиска похоже на волновой фронт, который движется, пока не наткнётся на место, которое мы искали.
Это простой пример поиска в действии. Область поиска расширяется на каждом этапе, пока в неё не будет включена конечная точка, после чего можно отследить путь к началу.
В результате мы получаем список клеток сетки, составляющий маршрут, по которому нужно идти. Обычно он называется «путём», path (отсюда и «поиск путей», pathfinding), но можно его также представить как план, потому что он представляет собой список мест, в которых надо побывать, чтобы достичь своей цели, то есть конечной точки.
Теперь, когда мы знаем позицию каждой клетки в мире, для перемещения по маршруту можно использовать описанные выше steering behaviours — сначала из начального узла к узлу 2, потом от узла 2 к узлу 3, и так далее. Простейший подход заключается в движении к центру следующей клетки, но есть и популярная альтернатива — движение к середине ребра между текущей клеткой и следующей. Это позволяет агенту срезать углы резких поворотов для создания более реалистичного перемещения.
Как можно заметить, этот алгоритм может тратить ресурсы впустую, потому что исследует столько же клеток в «неправильном» направлении, сколько и в «правильном». Также он не позволяет учитывать затраты на перемещение, при которых некоторые клетки могут быть «дороже» других. Здесь нам на помощь приходит более сложный алгоритм под названием A*. Он работает почти так же, как поиск в ширину, только вместо слепого исследования соседей, потом соседей соседей, потом соседей соседей соседей и так далее он помещает все эти узлы в список и сортирует их таким образом, чтобы следующий исследуемый узел всегда был тем, который с наибольшей вероятностью ведёт к кратчайшему маршруту. Узлы сортируются на основании эвристики (то есть, по сути, обоснованного предположения), которая учитывает два аспекта — стоимость гипотетического маршрута до клетки (учитывая таким образом все необходимые затраты на перемещение) и оценку того, насколько далеко эта клетка от конечной точки (таким образом смещая поиск в правильном направлении).
В этом примере мы показали, что он исследует по одной клетке за раз, каждый раз выбирая соседнюю клетку, имеющую наилучшие (или одни из лучших) перспективы. Получающийся путь аналогичен пути поиска в ширину, но в процессе исследовано меньшее количество клеток, и это очень важно для производительности игры на сложных уровнях.
Движение без сетки
В предыдущих примерах использовалась сетка, наложенная на мир, и мы прокладывали маршрут по миру через клетки этой сетки. Но большинство игр не накладывается на сетку, а потому наложение сетки может привести к нереалистичным паттернам движения. Также такой подход может потребовать компромиссов относительно размеров каждой клетки — если она слишком велика, то не сможет адекватно описывать небольшие коридоры и повороты, если слишком маленькая, то поиск по тысячам клеток может оказаться слишком долгим. Какие же есть альтернативы?
Первое, что нам нужно понять — с точки зрения математики сетка даёт нам «граф» соединённых узлов. Алгоритмы A* (и BFS) работают с графами, и им не важна сетка. Поэтому мы можем разместить узлы в произвольных позициях мира, и если между любыми двумя соединёнными узлами есть проходимая прямая линия, а между началом и концом есть хотя бы один узел, то наш алгоритм будет работать как и раньше, и на самом деле даже лучше, потому что узлов станет меньше. Часто это называется системой «точек пути» (waypoints system), поскольку каждый узел обозначает важную позицию мира, которая может создавать часть из любого количества гипотетических путей.

Пример 1: узел в каждой клетке сетки. Поиск начинается с узла, в котором находится агент, и заканчивается конечной клеткой.

Пример 2: гораздо меньшее количество узлов, или точек пути. Поиск начинается с агента, проходит через необходимое количество точек пути и продвигается к конечной точке. Заметьте, что перемещение к первой точке пути к юго-западу от игрока — это неэффективный маршрут, поэтому обычно необходима определённая постобработка сгенерированного пути (например, для того, чтобы заметить, что путь может идти напрямую к точке пути на северо-востоке).
Это довольно гибкая и мощная система, но она требует внимательного расположения точек пути, в противном случае агенты могут и не увидеть ближайшую точку пути, чтобы начать маршрут. Было бы замечательно, если бы могли каким-то образом генерировать точки пути автоматически на основе геометрии мира.
И тут на помощь приходит navmesh. Это сокращение от navigation mesh (навигационный меш). По сути это (обычно) двухмерный меш из треугольников, приблизительно накладывающийся на геометрию мира в тех местах, где игра разрешает перемещаться агенту. Каждый из треугольников в меше становится узлом графа и имеет до трёх смежных треугольников, которые становятся соседними узлами графа.
Ниже представлен пример из движка Unity. Движок проанализировал геометрию мира и создал navmesh (голубого цвета), являющийся аппроксимацией геометрии. Каждый полигон навмеша — это область, в которой может стоять агент, а агент может перемещаться из одного полигона в любой смежный с ним. (В этом примере полигоны сделаны ýже, чем пол, на котором они лежат, чтобы учесть радиус агента, выдающийся за пределы номинальной позиции агента.)

Мы можем выполнить поиск маршрута по мешу, снова воспользовавшись A*, и это даст нам близкий к идеальному маршрут по миру, учитывающий всю геометрию и при этом не требующий избыточного количества лишних узлов (как это было бы с сеткой) и человеческого участия в генерировании точек пути.
Поиск путей — это обширная тема, к которой существует множество подходов, особенно если необходимо программировать низкоуровневые детали самостоятельно. Один из лучших источников дополнительной информации — сайт Амита Патела (перевод статьи на Хабре: https://habr.com/post/331192/).
Планирование
На примере поиска путей мы увидели, что иногда недостаточно просто выбрать направление и начать двигаться в нём — нам необходимо выбирать маршрут и делать несколько ходов, прежде чем достичь нужной конечной точки. Мы можем обобщить эту идею до широкого набора концепций, в которых цель находится не просто на следующем шаге. Для её достижения необходимо совершить серию шагов, и чтобы знать, каким должен быть первый шаг, может потребоваться заглянуть на несколько шагов вперёд. Такой подход называется планированием. Поиск путей можно считать одним из конкретных применений планирования, но у этой концепции существует гораздо больше областей применения. Если вернуться к циклу «восприятие-мышление-действие», это планирование — это фаза мышления, которая пытается спланировать несколько фаз действия на будущее.
Давайте рассмотрим игру Magic: The Gathering. У вас первый ход, на руках несколько карт, среди которых «Болото», дающее 1 очко чёрной маны, и «Лес», дающий 1 очко зелёной маны, «Чародей-изгнанник», для вызова которого нужно 1 очко синей маны, и «Эльфийский мистик», для вызова которого нужно 1 очко зелёной маны. (Для упрощения мы опустим остальные три карты.) Правила гласят, что игрок может сыграть одну карту земель за ход, может «коснуться» своих карт земель, чтобы получить из них ману, и может произнести столько заклинаний (в том числе и вызова существ), сколько маны у него имеется. В этой ситуации игрок скорее всего сыграет «Лес», коснётся его, чтобы получить 1 очко зелёной маны, а затем вызовет «Эльфийского мистика». Но как игровой ИИ узнает, что нужно принять такое решение?
Простой «планировщик»
Наивный подход может заключаться в простом переборе каждого действия по порядку, пока не останется подходящих. Посмотрев на руку, ИИ видит, что может сыграть «Болото», поэтому так и делает. Остались ли после этого в этом ходу ещё действия? Он не может вызывать ни «Эльфийского мистика», ни «Чародея-изгнанника», потому что для этого нужна зелёная или синяя мана, а сыгранное «Болото» даёт только чёрную ману. И мы не можем сыграть «Лес», потому что уже сыграли «Болото». То есть ИИ-игрок совершит ход по правилам, но он будет не очень оптимальным. К счастью, есть решение получше.
Почти так же, как поиск путей находит список позиций для перемещения по миру с целью попадания в нужную точку, наш планировщик может находить список действий, которые переводят игру в нужное состояние. Также, как каждая позиция на пути имеет набор соседей, являющихся потенциальными вариантами выбора следующего шага по пути, каждое действие в плане имеет соседей, или «наследников», являющихся кандидатами на следующий шаг плана. Мы можем выполнять поиск по этим действиям и наследующим действиям, пока не достигнем желаемого состояния.
Предположим, что для нашего примере желательным результатом будет «вызывать существо, если это возможно». В начале хода у нас есть только два потенциальных действия, разрешённых правилами игры:
1. Сыграть "Болото" (результат: "Болото" уходит из руки и входит в игру) 2. Сыграть "Лес" (результат: "Лес" уходит из руки и входит в игру)
Каждое предпринятое действие может открывать дальнейшие действия или закрывать их, тоже в соответствии с правилами игры. Представьте, что мы выбрали сыграть «Болото» — это закрывает возможность сыграть эту карту как потенциальное наследующее действие (потому что «Болото» уже сыграно), закрывает возможность сыграть «Лес» (потому что правила игры разрешают сыграть только одну карту земель за ход) и добавляет возможность коснуться «Болота» для получения 1 очка чёрной маны — и это, по сути, единственное наследуемое действие. Если мы сделаем ещё один шаг и выберем «коснуться „Болота“», то получим 1 очко чёрной маны, с которым не можем ничего сделать, поэтому это бессмысленно.
1. Сыграть "Болото" (результат: "Болото" уходит из руки и входит в игру) 1.1 Коснуться "Болота" (результат: мы коснулись "Болота", доступно +1 чёрной маны) Действий не осталось - КОНЕЦ 2. Сыграть "Лес" (результат: "Лес" уходит из руки и входит в игру)
Этот короткий список действий не дал нам многого и привёл к «тупику», если воспользоваться аналогией с поиском путей. Поэтому повторим процесс для следующего действия. Мы выбираем «сыграть „Лес“». Это тоже устраняет возможность «сыграть „Лес“» и «сыграть „Болото“», и открывает в качестве потенциального (и единственного) следующего шага «коснуться „Леса“». Это даёт нам 1 очко зелёной маны, что в свою очередь открывает третий шаг — «вызвать „Эльфийского мистика“».
1. Сыграть "Болото" (результат: "Болото" уходит из руки и входит в игру) 1.1 Коснуться "Болота" (результат: мы коснулись "Болота", доступно +1 чёрной маны) Действий не осталось - КОНЕЦ 2. Сыграть "Лес" (результат: "Лес" уходит из руки и входит в игру) 2.1 Коснуться "Леса" (результат: мы коснулись "Болота", доступно +1 зелёной маны) 2.1.1 Вызвать "Эльфийского мистика" (результат: "Эльфийский мистик" в игре, доступно -1 зелёной маны) Действий не осталось - КОНЕЦ
Теперь мы исследовали все возможные действия и действия, следующие из этих действий, найдя план, позволяющий нам вызвать существо: «сыграть „Лес“», «коснуться „Леса“», «вызвать „Эльфийского мистика“».
Очевидно, это очень упрощённый пример, и обычно нужно выбирать наилучший план, а не просто план, удовлетворяющий каким-то критериям (например «вызвать существо»). Обычно можно оценивать потенциальные планы на основании конечного результата или накапливающейся выгоды от использования плана. Например, можно давать себе 1 очко за выложенную карту земель и 3 очка за вызов существа. «Сыграть „Болото“» будет коротким планом, дающим 1 очко, а план «сыграть „Лес“ → коснуться „Леса“ → вызвать „Эльфийского мистика“» даёт 4 очка, 1 за землю и 3 за существо. Это будет самый выгодный план из имеющихся, поэтому следует выбрать его, если мы назначали такие очки.
Выше мы показали, как работает планирование в пределах одного хода Magic: The Gathering, но его можно применить и к действиям в серии ходов (например, «переместить пешку, чтобы дать пространство для развития слону» в шахматах или «побежать в укрытие, чтобы юнит мог в следующем ходу стрелять, находясь в безопасности» в XCOM) или к общей стратегии всей игры (например, «строить пилоны до всех остальных зданий протоссов» в Starcraft, или «выпить зелье Fortify Health перед нападением на врага» в Skyrim).
Улучшенное планирование
Иногда на каждом шагу есть слишком много возможных действий, и оценка каждого варианта оказывается неразумным действием. Вернёмся к примеру Magic: The Gathering — представьте, что у нас на руке есть несколько существ, уже сыграно много земель, поэтому мы можем вызвать любое существо, сыграно несколько существ со своими способностями, а на руке есть ещё пара карт земель — количество перестановок выкладывания земель, использования земель, вызова существ и использования способностей существ может равняться тысячам или даже десяткам тысяч. К счастью, есть пара способов решения этой проблемы.
Первый называется «backwards chaining» («обратный обход»). Вместо проверки всех действий и их результатов мы можем начать с каждого из желательных конечных результатов и посмотреть, сможем ли мы найти к ним прямой путь. Можно сравнить это с попыткой дотянуться до определённого листа на дереве — гораздо логичнее начинать с этого листа и возвращаться назад, прокладывая маршрут по стволу (и этот маршрут мы затем сможем пройти в обратном порядке), чем начинать со ствола и пытаться угадать, какую ветвь выбирать на каждом шагу. Если начинать с конца и идти в обратную сторону, то создание плана окажется намного быстрее и проще.
Например, если у противника осталось 1 очко здоровья, то может быть полезным попытаться найти план «нанести 1 или больше очков прямого урона противнику». Наша система знает, что для достижения этой цели ей нужно скастовать заклинание прямого урона, что в свою очередь означает, что оно должно быть у нас на руке и нам нужно достаточно маны для его произнесения. Это, в свою очередь, означает, что нам нужно коснуться достаточного количества земель для получения этой маны, для чего может потребоваться сыграть дополнительную карту земель.
Ещё один способ — поиск по первому наилучшему совпадению. Вместо длительного обхода всех перестановок мы измеряем, насколько «хорош» каждый частичный план (аналогично тому, как мы выбирали из вариантов планов выше) и вычисляем каждый раз наиболее хорошо выглядящий. Часто это позволяет создать оптимальный, или хотя бы достаточно хороший план без необходимости рассмотрения каждой возможной перестановки планов. A* является разновидностью поиска по первому наилучшему совпадению — сначала он исследует самые многообещающие маршруты, поэтому обычно может найти путь к цели без необходимости забираться слишком далеко в других направлениях.
Интересный и всё более популярный вариант поиска по первому наилучшему совпадению — это поиск по дереву Монте-Карло. Вместо того, чтобы гадать, какие планы лучше других при выборе каждого последующего действия, этот способ выбирает на каждом шаге случайные последующие действия, пока не достигает конца, в котором больше невозможны никакие действия — вероятно, потому что гипотетический план привёл к состоянию победы или проигрышв — и использует этот результат для придания большего или меньшего веса предыдущим выбранным вариантам. При многократном повторении процесса способ может создать хорошую оценку наилучшего следующего шага, даже если ситуация поменяется (например, если противник попытается сорвать наши планы).
Наконец, ни одно обсуждение планирования в играх не будет полным без упоминания планирования действий на основе целей (Goal-Oriented Action Planning, GOAP). Это широко используемая и активно обсуждаемая техника, но если отвлечься от нескольких конкретных деталей реализации, то она по сути является планировщиком обратного обхода, который начинает с цели и пытается подобрать действие, приводящее к этой цели, или, что более вероятно, список действий, приводящий к цели. Например, если целью было «убить игрока» и игрок находится в укрытии, то план может быть таким: «Выкурить игрока гранатой» → «Вытащить оружие» → «Атаковать».
Обычно существует несколько целей, и у каждой есть свой приоритет. Если цели с наивысшим приоритетом достичь не удаётся, например, никакой набор действий не может сформировать план «Убить игрока», потому что игрока не видно, то система возвращается к целям с меньшим приоритетам, например, «Патрулировать» или «Охранять на месте».
Обучение и адаптация
В начале статьи мы упомянули, что игровой ИИ в общем случае не использует «машинное обучение», потому что оно обычно не приспособлено для управления в реальном времени интеллектуальными агентами игрового мира. Однако это не значит, что мы не можем позаимствовать что-то из этой области там, где это имеет смысл. Нам может понадобиться, чтобы компьютерный противник в шутере узнал наилучшие места, в которые нужно переместиться, чтобы набрать наибольшее количество убийств. Или мы можем захотеть, чтобы противник в файтинге. например, в Tekken или Street Fighter научился распознавать использование игроком одних и тех же комбо, чтобы начать их блокировать, заставляя игрока применить другую тактику. То есть бывают случаи, когда определённая доля машинного обучения полезна.
Статистика и вероятности
Прежде чем мы перейдём к более сложным примерам, стоит разобраться, насколько далеко мы можем зайти, просто выполняя измерения и используя эти данные для принятия решений. Например, допустим, у нас есть игра в жанре стратегии реального времени, и нам нужно понять, будет ли игрок начинать раш в течение нескольких первых минут, чтобы решить, стоит ли строить больше защиты. Мы можем экстраполировать предыдущее поведение игрока, чтобы понять, каким может быть поведение в будущем. Сначала у нас нет данных, которые можно экстраполировать, но каждый раз, когда ИИ играет против живого противника, он может записывать время первой атаки. Через несколько матчей это время можно усреднить, и мы получим достаточно хорошее приближение времени атаки игрока в будущем.
Проблема с простым усреднением заключается в том, что оно обычно со временем сходится в центре. Поэтому если игрок использовал стратегию раша первые 20 раз, а в следующие 20 раз переключился на гораздо более медленную стратегию, то среднее значение будет где-то посередине, что не даст нам никакой полезной информации. Один из способов улучшения данных — использование простого окна усреднения, учитывающего только последние 20 точек данных.
Похожий подход можно использовать при оценке вероятности совершения определённых действий, если предположить, что предыдущие предпочтения игрока сохранятся и в будущем. Например, если игрок пять раз атаковал фаерболом, два раза молнией и врукопашную всего один раз, то вероятнее всего, что он предпочтёт фаербол в 5 из 8 раз. Экстраполируя из этих данных, мы можем увидеть, что вероятность использования оружия такая: Фаербол=62,5%, Молния=25% Рукопашная=12,5%. Наши ИИ-персонажи поймут, что им лучше найти огнестойкую броню!
Ещё один интересный метод заключается в использовании наивного байесовского классификатора (Naive Bayes Classifier) для исследования больших объёмов входных данных с целью классификации текущей ситуации, чтобы ИИ-агент мог реагировать соответствующим образом. Байесовы классификаторы наверно лучше всего известны благодаря их использованию в спам-фильтрах электронной почты, где они оценивают слова в письме, сравнивают их с теми словами, которые наиболее часто встречались в спаме и нормальных сообщениях в прошлом. На основании этих вычислений они принимают решение о вероятности того, что последнее полученное письмо является спамом. Мы можем сделать что-то подобное, только с меньшим объёмом входных данных. Записывая всю наблюдаемую полезную информацию (например, создаваемые вражеские юниты, используемые заклинания или исследуемые технологии) и отслеживая получившуюся ситуацию (война/мир, стратегия раша/стратегия защиты и т.д.), мы можем подбирать на основании этого подходящее поведение.
При использовании всех этих техник обучения может быть достаточно, а часто и предпочтительно применять их к данным, собранным во время плейтестинга до выпуска игры. Это позволяет ИИ адаптироваться к различным стратегиям, используемым плейтестерами, и не меняться после выпуска игры. ИИ, который адаптируется к игроку после выпуска игры, может в результате стать слишком предсказуемым или даже слишком сложным для победы над ним.
Простая адаптация на основе весов
Давайте сделаем ещё один шаг вперёд. Вместо того, чтобы просто использовать входящие данные для выбора между дискретными заранее прописанными стратегиями, можно изменять набор значений, влияющих на принятие решений. Если мы хорошо понимаем игрового мира и правил игры, то можем сделать следующее:
- Заставить ИИ собирать данные о состоянии мира и ключевых событиях во время игры (как в примере выше);
- Изменять значения или «веса» на основании данных в процессе их сбора;
- Реализовать решения на основании обработки или вычисления этих весов.
Марковские модели
Что, если бы мы захотели использовать собираемые данные для создания прогнозов? Например, если мы записываем каждую комнату, в которой видим игрока в течение какого-то промежутка времени, то можем резонно прогнозировать, в какую комнату он может двинуться дальше. Отслеживая и текущую комнату, в которой находится игрок, и предыдущую, и записывая эти пары значений, мы можем вычислить, как часто каждая из предыдущих ситуаций ведёт к последующей ситуации, и использовать это знание для прогнозов.
Представим, что есть три комнаты — красная, зелёная и синяя, и что во время сеанса игры мы получили такие наблюдения:
О трансформационных играх
Это категория групповых настольных Игр, ориентированных на работу с личными запросами участников, и направленная на нахождение оптимальных решений и преодоление личностных кризисов и сложных ситуаций, и также на достижение персональных целей участников.
Результат в Игре достигается через более глубокую проработку и осознание участниками причин своей текущей ситуации, мотивов и сценариев своего поведения, личных психологических установок и убеждений и их корректировок.
ЭТО ИНСТРУМЕНТ САМОПОЗНАНИЯ
В процессе самой Игры вы начинаете знакомиться с тем собой, которого вы чаще всего не замечали, с теми своими гранями и уникальными способностями, которые вам дарованы от рождения, но вы так и не начали ими пользоваться в полной мере. В процессе Игры происходит это погружение в себя настоящего и как бы «распаковка» спящих ваших внутренних ресурсов.
Вы можете задать резонный вопрос: «а как же литература, кинематограф, театр и всякие там кружки самодеятельности в том числе? Это же все инструменты самопознания! Да и, вообще, вся жизнь — инструмент самопознания!»
Трансформационная Игра — зеркало вашей жизни. В Игре все, как в жизни.
В этом то и есть особенность трансформационных Игр — они показывают вам, как в зеркале, именно ВАШУ (а не персонажа из литературы или кинематографа) УНИКАЛЬНУЮ ЖИЗНЬ.
Игра дает возможность честно увидеть свои привычные маршруты с высоты птичьего полета. Понять, какие привычки мешают вам или помогают в жизни, принять решение и тут же в Игре его опробовать, понять: эти изменения Вам помогают или не помогают, и какие могут быть еще варианты — это, во-первых.
Большим плюсом я считаю возможность увидеть свои стратегии, потому что как бы вы ни хотели, вы будете использовать в Игре только те стратегии, которые используете в жизни.
Во-вторых, трансформационная Игра дает массу информации на тот ваш личный вопрос, решение которого вам не дает покоя. Информация приходит совершенно удивительным образом, как говорится, прямо «в яблочко». Приходят ответы на вопрос: «Какие внутренние препятствия, ограничения или «слепые зоны», мешают вам быть эффективным, структурированным, мотивированным, целенаправленным и так далее.
Ответы внутри, а не снаружи. Никто, кроме вас, не даст вам правильного ответа, просто потому, что его не существует.
Отдельно хотелось бы отметить феноменологический подход к Игре. А именно то, что в ходе Игры мы можем наблюдать явления, которые сложно объяснить научной точкой зрения, но которые, тем не менее, практически работают. Это относится, например, к слепому выбору участником символических карточек, которые впоследствии очень точно отражают его реальную ситуацию, перемещение в определенные области игрового поля в соответствии с запросом участников, к работе группового поля и многое другое.
В-третьих, что важно, в процессе прохождения Трансформационной Игры вам оказывают профессиональную психологическую поддержку. Обстановка уюта и доверия. Здесь вы можете себе позволить быть самим собой.
Особенно из-за нашей привычки погружаться в каждодневную суету, Трансформационная Игра — это уникальная возможность, никуда не спеша и не отвлекаясь на «срочные дела» осознать глубину важных вопросов и ответов про себя, про свою жизнь и свои цели. Осознание глубины тех уникальных особенностей себя и своей жизни, что сразу вам распахивает двери в жизни.
В-четвертых, так как это Игра — вы можете в легком игровом формате разобраться со своими внутренними ограничениями, встретиться со своими сомнениями и тревогами. Легче их встретить и проработать в игровой обстановке, чем в виде жестких граблей жизни. Это возможность в мягкой игровой форме увидеть границы своей системы, найти ограничивающие убеждения, то, что реально вас тормозит. Один из самых ценных опытов — это право на ошибку.
В Игре вы меняете фокус с проблемы на решение, с препятствия на цель. Любая Игра моделирует различные жизненные ситуации, ставит вопросы, с которыми вы сталкиваетесь в реальной жизни.
Игра, словно, создает лабораторные условия, где можно «протестить» изменения, прежде чем реализовывать их в жизни, этакий испытательный полигон, креативная платформа, где происходит масса откровений и озарений.
Если в реальной жизни ценой вашего решения могут быть годы, потерянные впустую, то в Игре вы можете смоделировать эту ситуацию и «прожить» сразу 5–10 лет. И уже тогда принять решение, а нравится ли вам такой путь.
Про Игры я могу говорить бесконечно, методы игротехники, с моей точки зрения, одни из самых простых и эффективных.
Максимум результата — минимум времени. Жестко. Сильно. Не для всех.
По поводу результатов. Каждый получит ровно то, зачем он пришел на Игру. Унесет ровно столько, сколько сможет взять. Это не тренинг, не тренировка навыков… Мастер обычно только показывает, куда можно идти… идти или нет — решать только вам.
Применять полученный опыт в своей привычной жизни, реализовывать трансформацию или продолжать не верить себе обновленному — ваш выбор и ваше право. Но мой вам совет — изменения происходят с теми, кто познает себя и действует.
Ваши решения. Ваша ответственность. Ваша Игра. Ваша жизнь.
ВИДЫ ТРАНСФОРМАЦИОННЫХ ИГР
Любая Трансформационная Игра — это, в первую очередь, путешествие. Путешествие к себе, за ответами на те вопросы, которые и привели вас в Игру.
Под ваш запрос, который вы хотите решить (как найти любимую работу, как выстроить доверительные отношения с сыном-подростком и т.д.) вы выбираете подходящую по тематике Трансформационную Игру.
Игры бывают настольного формата и не только, групповые и индивидуальные.
Каждая из этих Игр самопознания — инструмент для более глубокого понимания себя, своих целей, ресурсов и возможностей.
Средняя длительность одной Игры — 3–6 часов и зависит от участника.
Краткий обзор Трансформационных Игр, о которых я когда-либо читала, слышала, которые я проходила сама (конечно, нет гарантий, что перечислю все ныне существующие):