Сердце 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. Я считаю, что я очень близок к завершению, и я очень надеюсь, что не обнаружу крайний случай, который сломает все.
Пожалуйста, дайте мне знать, если у вас есть какие-либо советы или вопросы!