Как я делал рандомный нарратив в игре для гейм-джема
Почти как в Hades, если не учитывать, что они свою систему улучшали годами, а я свою сделал за пару вечеров.
Как это сделано у них
В ноябре вышло видео, где рассказывается, как устроен нарратив в Hades, а несколько дней назад вышел его перевод на DTF.
В видео нет технических деталей, а просто объясняется дизайн и основные идеи. Если кратко, нарратив у них опирается на 4 принципа:
- Огромное количество текста. Каждому персонажу всегда есть что сказать по любому поводу.
- Случайность. Из кучи возможных реплик и тем для обсуждения нужно выбрать одну.
- Учет текущего состояния игры. Какие-то диалоговые ветки появляются только после каких-то событий или действий игрока. Реплики, которые уже были показаны, убираются из списка доступных и появляются опять, только если больше нет никаких других вариантов.
- Приоритет. Даже если у персонажа есть несколько тем на обсуждение, иногда какую-то конкретную тему нужно обсудить раньше.
Как это сделано у меня
Моя реализация попроще и покрывает только пункты 2 и 3: случайность и учет состояния игры. Сделано это с помощью концепции предусловий и постусловий. Эти термины я подцепил из теории разработки программного обеспечения, а туда они попали, скорее всего, из какой-нибудь математической области, типа мат.логики, теории групп или чего-то такого.
Предусловие — это то, что должно быть выполнено, чтобы можно было запустить программу или вызвать функцию. Постусловие — это то, как изменится состояние системы после запуска программы или вызова функции. В моем случае функцией является код, который дает нам какой-то случайный кусок нарратива.
Состояние игры — это любые параметры, которые могут быть как-то оценены. Местоположение персонажа, его характеристики, есть ли у него какой-то предмет в инвентаре, его действия — что угодно. Hades именно это и учитывает: победил ты Аида в первый раз — при первой же встрече все персонажи тебе про это что-то выскажут.
Моя игра (Кукуха в самоизоляции) устроена гораздо проще. В ней нужно продержаться 30 дней в условиях самоизоляции и каждый день выбирать какое-то занятие из трех предложенных вариантов. Эти варианты зависят от того, что уже произошло в игре. Если есть игровая консоль, то можно поиграть. Если есть подписка на стриминговый сервис, то можно его посмотреть. Если есть блог и есть хобби, то можно написать в блог про свое хобби.
Кроме этого каждый день происходят случайные события, которые зависят от состояния игры, но не от последнего выбора игрока. Например, если игрок часто смотрит стриминговый сервис, то у него появляется любимый сериал. Одно из случайных событий — это закрытие этого любимого сериала.
В игре есть всего два числовых параметра — это текущий день и текущее спокойствие Кукухи, и ни один из них не учитывается в нарративе. Вместе этого я использую систему наличия тех или иных флагов. Например: есть телевизор, есть игровая консоль, есть подписка на стриминговый сервис, есть домашнее животное, есть любимый сериал.
Каждое действие, которое игрок выбирает из предложенных вариантов — это одна нарративная единица, у которой есть свои предусловия и свои постусловия. Рассмотрим пример: «Написать в блог про свою любимую игровую консоль». У этого действия есть предусловия: у персонажа должен быть блог и у него должна быть игровая консоль. Постусловие — персонажу добавляется флаг «консольный воин», поэтому в каком-то из случайных событий Кукухе могут припомнить, что она уже писала про консоли и «сделала неправильный выбор».
В реализации игры на джеме у меня было 19 флагов состояния игры, 27 возможных действий и 23 случайных события. Каждый ход игра берет все возможные действия, проверяет, для каких из них выполнены предусловия, выбирает из них три случайных и предлагает игроку. То же самое со случайными событиями, но выбирается только одно событие в день.
Детали реализации
Чуть-чуть углубимся в программирование. На гейм-джеме я использовал Unreal Engine 4, поэтому код был на С++. Здесь не будет каких-то особенностей C++, поэтому примеры легко переносятся на C# или что угодно еще.
Исходный код игры есть у меня на GitHub:
Состояние игры я храню в виде множества enum’ов. Все доступные действия и случайные события — это два динамических массива.
Enum содержит все 19 флагов, которые я упоминал ранее.
Теперь про конфигурацию действий и случайных событий. В идеальном мире они бы создавались в каком-нибудь особом редакторе, которым пользовались бы нарративные дизайнеры, но у нас тут гейм-джем, поэтому я их просто захардкодил. Я использовал паттерн Builder, так как он отлично подходит для таких задач.
- ShowMenuText — это то, что видит игрок, когда ему предлагаются три варианта на выбор.
- ShowResultText показывается, когда игрок сделал выбор.
- CheckPrecondition — проверка предусловия. Здесь проверяется, что у персонажа есть блог и игровая консоль.
- AddState и RemoveState — постусловия. Задают, какие флаги должны появиться или исчезнуть, если игрок сделает этот выбор. В этом примере добавляется флаг ConsoleWarrior.
- SetDeltaWellBeing — в каком-то роде тоже постусловие. Параметр задает насколько изменится спокойствие Кукухи, если она сделает этот выбор.
У случайных событий конфигурация практически такая же.
Каждый ход игра пробегается по массиву Actions, выбирает те, для которых State содержит все предусловия, затем выбирает три случайных и предлагает игроку. Когда игрок делает выбор, это действие удаляется из Actions, чтобы оно больше не повторялось, а указанные постусловия добавляются в State (или удаляются). Затем почти то же самое делается с RandomEvents.
Некоторые в отзывах писали, что тексты повторяются, но это потому что я схалтурил и специально сделал так, что некоторые действия можно выбирать несколько раз. Например, смотреть телевизор или играть в видеоигры. Это задается через конфигурацию:
Т.е. сама по себе система защищена от повторов, но мне было лень писать много уникальных вариантов или писать разные тексты для повторяющихся действий.
Получилась довольно неплохая система рандомного нарратива или как я более пафосно назвал ее на джеме — процедурного нарратива. Для большой и серьезной игры ее нужно доделывать. Как минимум, нужно заменить enum на что-то другое, так как если он начнет разрастаться, то его станет сложно контролировать. Еще нужно расширить область, которая проверяется предусловиями: учитывать не только флаги, но и характеристики игрока, например. Ну и контента побольше, разумеется.