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

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

  • Система разговора должна быть способна охватить все истории, которые я хочу написать.
  • Мне нужно закончить День Дерби. Если конкретная сюжетная линия не вписывается в систему разговора, я должен ее взломать.

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

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

Вот несколько примеров разговоров, которые мне нужны в игре:

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

Однажды я чувствовал себя потерянным и разочарованным. Я попытался вспомнить другие игры с похожими диалоговыми системами, и мне на ум пришла Stardew Valley. Игроки могут ходить по миру и начинать разговоры с другими персонажами, которые будут отвечать в зависимости от различных факторов, таких как текущий сезон, время суток, насколько вы им нравитесь и где они в настоящее время находятся в мире. Я написал Эрику Бароне, человеку, стоящему за Stardew Valley, чтобы попросить совета. Его ответ:

Для Stardew я не использовал сторонние инструменты. Большая часть диалогового окна содержится в файлах xml, где я связываю каждое диалоговое сообщение с «ключом», который описывает условия, запускающие это конкретное сообщение. Так, например, ключ может указывать на то, что определенное сообщение появляется по средам зимой, если у NPC 4 сердца.

В диалоговом сообщении у меня есть специальные символы, которые выполняют разные функции. Например, строка «# $ b #» означает, что в этот момент сообщение должно быть разбито на два отдельных окна. Это может быть более сложным, например, когда NPC задает вам вопрос. Но суть в том, что я сам написал диалоговую систему с нуля, поэтому у меня был полный контроль над этими «специальными символами» и их поведением.

Ваш супруг (а) «злится» из-за того, что дарит подарок другим игрокам, - это всего лишь проверка кода, когда вы разговариваете со своим супругом.

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

Надеюсь, что это хоть немного поможет, и желаю удачи в игре. Скачки звучат круто

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

Итак, вот как работает моя текущая система:

Каждый тип персонажа (жокей, агент, владелец и т. Д.) Имеет файл данных (json), который описывает все возможные деревья разговоров. Вот пример дерева разговоров для типа персонажа мафиози:

{
  "id": "mafioso_threat",
  "location":"Home",
  "random":true,
  "nextText":
  [
    {
      "id":"player_loses_fixed_race",
      "requirements":
      {
        "dayOfWeek":6,
        "bestLastPlace":4
      },
      "options": [
        {
          "id":"accept_reward",
          "action": {
            "name": "creditPlayer",
            "parameters": 500
          }
        },
        {
          "id":"refuse_reward",
          "nextText":
          {
            "id": "refused_reward",
            "action":
            {
              "name": "adjustRelationship",
              "parameters": -2
            }
          }
        }
      ]
    },
    {
      "id":"player_wins_fixed_race",
      "requirements":
      {
        "dayOfWeek":6,
        "worstLastPlace":3
      }
    }
  ]
}

Первый «узел» этого дерева диалога имеет четыре пары "ключ-значение":

  • id: Это ссылочный идентификатор узла, но, что более важно, это ссылка на токен языка в файле локализации.
  • location: Это то место, где должен быть игрок, чтобы событие произошло.
  • random: При загрузке определенных локаций (прямо сейчас только Дом) игра проверяет запуск случайного события разговора. Если этот узел случайный, то можно выбрать один из разговоров.
  • nextText: после выбора узла разговора его дочерние элементы (узлы в nextText) будут добавлены в очередь разговора.

В этом примере мафиози позвонит игроку и скажет ему намеренно проиграть предстоящую гонку. На этом этапе два узла в _6 _ (_ 7_ и player_wins_fixed_race) будут добавлены в очередь диалога. Они связаны друг с другом, так что инициировать можно только одного. В этих узлах мы видим еще одну пару "ключ-значение":

  • requirements: Каждый раз, когда игрок прибывает в состояние / место, игра просматривает все узлы в очереди разговора. Если кто-то пройдет их требования, разговор начнется.

Вот два примера требований:

  • dayOfWeek: Игра работает по 6-дневному недельному циклу с гонками каждый день 5. День 6 - это день событий, связанных с результатами гонок.
  • bestLastPlace и worstLastPlace: они предназначены для сравнения с тем местом, где игрок финишировал в последней гонке. В этом случае игрок должен финишировать 4-м или хуже, чтобы передать bestLastPlace или 3-м, или лучше, чтобы передать worstLastPlace.

Хорошо, допустим, игрок получает телефонный звонок от мафиози, а затем занимает 6-е место в следующей гонке. Затем мафиози позвонит игроку и произнесет текст, соответствующий player_loses_fixed_race: «Я не уверен, дерьмо у тебя лошадь или ты дергал за меня за ниточки. В любом случае спасибо. Вот небольшой подарок »

Затем игроку предлагаются два варианта:

  • accept_reward: Эта опция вызывает функциюcreditPlayer, которая дает им 500 долларов.
  • refuse_reward: Эта опция отвергает подарок и вызывает функцию adjustRelationship , которая понижает respect, который мафиози имеет для игрока.

Пара "ключ-значение" actions предназначена не только для игрока options, но и для предметов разговора.

Вот более сложный пример, в котором игрок вызывает жокей-агента, чтобы нанять жокея:

{
  "id":"jockeys_available",
  "location":"Home",
  "optionListGeneration":{
    "textTemplate":"jockey_name",
    "function":"getAvailableJockeysAsOptions"
  }
},

Этот узел разговора возникает, когда игрок находится дома и звонит жокей-агенту. Агент ответит на звонок и скажет : «Тебе нужен жокей для {next_race.name}»? У меня есть в наличии: ”

В игре постоянно отслеживаются различные значения. Здесь мы видим, что имеется ссылка на объект next_race. Значение {next_race.name} ищется и заменяется текстовой строкой. Он превращается в «Вам нужен жокей для Haskell Stakes? У меня есть в наличии: ». Идея глобальной карты объектов была важным моментом эврики, не только для отображения текста, но и для упрощения большого количества кода, поскольку я мог напрямую ссылаться на общие объекты вместо того, чтобы каждый раз выполнять поиск.

Далее у нас есть ключ optionListGeneration. Это сообщает классу диалога, что мы должны сгенерировать параметры из функции с именем getAvailableJockeysAsOptions. Поскольку мы общаемся с агентом, эта функция будет находиться в модели Agent.js.

Функция getAvailableJockeysAsOptions просматривает список жокеев, связанных с текущим агентом, и генерирует вариант для каждого из них:

options.push({
  "text":jockey.name,
  "nextText":
  {
    "id":this.generateJockeyDescription(jockey),
    "options": [
      {
        "id": "contract",
        "nextText":
        {
          "id":"jockey_contracted",
          "action": {
            "name":"hire_jockey",
            "parameters":jockey.id
          }
        }
      },
      {
        "id": "no",
        "nextText":
        {
          "id":"jockeys_available_return",
          "optionListGeneration":{
            "textTemplate":"jockey_name",
            "function":"getAvailableJockeysAsOptions"
          }
        }
      }
    ],
    "action":
    {
      "name":"set_identifier",
      "parameters":
      {
        "identifier":"subject",
        "value":jockey
      }
    }
  }
});

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

Как только игрок нажимает на имя жокея, агент проверяет тип личности жокея и дает краткое описание. Например, «{subject.name} точно знает, когда толкнуть лошадь {subject.possesive_pronoun}».

Затем агент предоставляет два варианта: contract и no. Выбор no аналогичен перезапуску узла диалога в корне этого дерева, но с другим вводным текстом. При выборе contract вы наймете жокея и выйдете из разговора.

Возможно, вы также заметили action под названием set_identifier, связанный с каждым жокеем. Это устанавливает выбранного жокея в качестве глобальной переменной subject, чтобы мы могли ссылаться на него по имени и полу. Это позволяет нам подставлять переменные в тексте выше так, чтобы получилось «Рози Биддлкомб точно знает, когда толкать свою лошадь».

Я еще не упомянул еще одну пару "ключ-значение" для элемента беседы, которая называется locked. Это используется для разговоров, например, когда другой владелец поймает вас на попытке убить его лошадь. В этом случае действие разблокирует этот разговор, и вы получите телефонный звонок в следующий раз, когда будете дома.

Надеюсь, я все ясно объяснил, не вдаваясь в подробности кодирования. Для тех, кто интересуется, как организован код, вот краткое описание каждого файла / класса и того, как он используется:

  • states/Conversation: Это игровое состояние, в которое игрок войдет для любого разговора. Код здесь отвечает за чтение объекта conversationItem и выполнение / отображение каждого значения ключа.
  • states/LocationState: Это родительское состояние для всех локаций (Дом, Бар, Загон и т. Д.). Это состояние отвечает за проверку доступности разговора и, если да, за вступление в него.
  • models/TextNode: Это представляет собой узел в conversationItem. Объектное представление пар "ключ-значение" из файла данных.
  • models/ConversationItem: conversationItem инкапсулирует TextNode. Он отвечает за определение того, содержит ли он TextNode , который соответствует его требованиям. TextNodes и ConversationItems можно почти объединить в одну модель, но этот дополнительный уровень помогает управлять состоянием TextNodes по символу.
  • managers/ConversationManager: Этот класс отвечает за управление очередью разговоров. Состояния местоположения будут проверяться с помощью этого класса, чтобы увидеть, есть ли допустимый диалог для загрузки.
  • managers/TextManager: В основном менеджер по локализации. Он извлекает текст из en.json и заполняет значения на основе глобальных переменных.

Вот как работает текущая диалоговая система в Derby Day. Я считаю, что я очень близок к завершению, и я очень надеюсь, что не обнаружу крайний случай, который сломает все.

Пожалуйста, дайте мне знать, если у вас есть какие-либо советы или вопросы!