Как я делал рандомный нарратив в игре для гейм-джема

Почти как в Hades, если не учитывать, что они свою систему улучшали годами, а я свою сделал за пару вечеров.

Как это сделано у них

В ноябре вышло видео, где рассказывается, как устроен нарратив в Hades, а несколько дней назад вышел его перевод на DTF.

В видео нет технических деталей, а просто объясняется дизайн и основные идеи. Если кратко, нарратив у них опирается на 4 принципа:

  • Огромное количество текста. Каждому персонажу всегда есть что сказать по любому поводу.
  • Случайность. Из кучи возможных реплик и тем для обсуждения нужно выбрать одну.
  • Учет текущего состояния игры. Какие-то диалоговые ветки появляются только после каких-то событий или действий игрока. Реплики, которые уже были показаны, убираются из списка доступных и появляются опять, только если больше нет никаких других вариантов.
  • Приоритет. Даже если у персонажа есть несколько тем на обсуждение, иногда какую-то конкретную тему нужно обсудить раньше.
Как я делал рандомный нарратив в игре для гейм-джема

Как это сделано у меня

Моя реализация попроще и покрывает только пункты 2 и 3: случайность и учет состояния игры. Сделано это с помощью концепции предусловий и постусловий. Эти термины я подцепил из теории разработки программного обеспечения, а туда они попали, скорее всего, из какой-нибудь математической области, типа мат.логики, теории групп или чего-то такого.

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

Состояние игры — это любые параметры, которые могут быть как-то оценены. Местоположение персонажа, его характеристики, есть ли у него какой-то предмет в инвентаре, его действия — что угодно. Hades именно это и учитывает: победил ты Аида в первый раз — при первой же встрече все персонажи тебе про это что-то выскажут.

Моя игра (Кукуха в самоизоляции) устроена гораздо проще. В ней нужно продержаться 30 дней в условиях самоизоляции и каждый день выбирать какое-то занятие из трех предложенных вариантов. Эти варианты зависят от того, что уже произошло в игре. Если есть игровая консоль, то можно поиграть. Если есть подписка на стриминговый сервис, то можно его посмотреть. Если есть блог и есть хобби, то можно написать в блог про свое хобби.

Как я делал рандомный нарратив в игре для гейм-джема

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

Как я делал рандомный нарратив в игре для гейм-джема

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

Каждое действие, которое игрок выбирает из предложенных вариантов — это одна нарративная единица, у которой есть свои предусловия и свои постусловия. Рассмотрим пример: «Написать в блог про свою любимую игровую консоль». У этого действия есть предусловия: у персонажа должен быть блог и у него должна быть игровая консоль. Постусловие — персонажу добавляется флаг «консольный воин», поэтому в каком-то из случайных событий Кукухе могут припомнить, что она уже писала про консоли и «сделала неправильный выбор».

В реализации игры на джеме у меня было 19 флагов состояния игры, 27 возможных действий и 23 случайных события. Каждый ход игра берет все возможные действия, проверяет, для каких из них выполнены предусловия, выбирает из них три случайных и предлагает игроку. То же самое со случайными событиями, но выбирается только одно событие в день.

Детали реализации

Чуть-чуть углубимся в программирование. На гейм-джеме я использовал Unreal Engine 4, поэтому код был на С++. Здесь не будет каких-то особенностей C++, поэтому примеры легко переносятся на C# или что угодно еще.

Исходный код игры есть у меня на GitHub:

Состояние игры я храню в виде множества enum’ов. Все доступные действия и случайные события — это два динамических массива.

TSet<EStateKey> State; TArray<FAction*> Actions; TArray<FRandomEvent*> RandomEvents;

Enum содержит все 19 флагов, которые я упоминал ранее.

enum class EStateKey { HasTvWithOneChannel, NeedsMoreTvChannels, HasTvWithManyChannels, BelievesInPropaganda, HasStreamingSubscription, HasPremiumStreamingSubscription, HasFavoriteSeries, FavoriteSeriesClosed, HasBadInternet, NeedsBetterInternet, HasGoodInternet, HasGamingConsole, HasFavoriteGame, ConsoleWarrior, HasPet, HasTwoPets, HasThreePets, HasHobby, GoodAtHobby, HasBlog, };

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

Actions.Add( FActionBuilder().ShowMenuText("Написать в блог про свою любимую игровую консоль") .ShowResultText("Я написала, что лучшие консоли - это те, у которых больше цифр. А у моей консоли очень много цифр!") .CheckPrecondition(EStateKey::HasBlog) .CheckPrecondition(EStateKey::HasGamingConsole) .AddState(EStateKey::ConsoleWarrior) .SetDeltaWellBeing(7) .Build() );
  • ShowMenuText — это то, что видит игрок, когда ему предлагаются три варианта на выбор.
  • ShowResultText показывается, когда игрок сделал выбор.
  • CheckPrecondition — проверка предусловия. Здесь проверяется, что у персонажа есть блог и игровая консоль.
  • AddState и RemoveState — постусловия. Задают, какие флаги должны появиться или исчезнуть, если игрок сделает этот выбор. В этом примере добавляется флаг ConsoleWarrior.
  • SetDeltaWellBeing — в каком-то роде тоже постусловие. Параметр задает насколько изменится спокойствие Кукухи, если она сделает этот выбор.

У случайных событий конфигурация практически такая же.

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

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

... .HasUnlimitedActivations(true) .SetDeltaWellBeing(8) .SetDiminishingReturnModifier(1.2) ...

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

Получилась довольно неплохая система рандомного нарратива или как я более пафосно назвал ее на джеме — процедурного нарратива. Для большой и серьезной игры ее нужно доделывать. Как минимум, нужно заменить enum на что-то другое, так как если он начнет разрастаться, то его станет сложно контролировать. Еще нужно расширить область, которая проверяется предусловиями: учитывать не только флаги, но и характеристики игрока, например. Ну и контента побольше, разумеется.

3535
6 комментариев

Это так интересно читать. Спасибо!

4
Ответить

Строки не через константы. Осуждаю (¬‿¬ )

3
Ответить

Ладно

3
Ответить

Комментарий недоступен

2
Ответить

А в Unreal Engine 4 неймспейсов нету в C++?

Ответить

Есть, просто я их отсюда убрал, чтобы лаконичнее смотрелось. В оригинале там так:

1
Ответить