Написание многопользовательского текстового приключенческого движка на Node.js (часть 1)
Опубликовано: 2022-03-10Текстовые приключения были одной из первых форм цифровых ролевых игр, когда в играх не было графики, и все, что у вас было, это ваше собственное воображение и описание, которое вы читали на черном экране вашего ЭЛТ-монитора.
Если мы хотим поностальгировать, может быть, название Colossal Cave Adventure (или просто Adventure, как оно было первоначально названо) звучит как звонок. Это была самая первая текстовая приключенческая игра.

На изображении выше показано, как вы на самом деле видите игру, что сильно отличается от наших текущих лучших приключенческих игр AAA. При этом в них было весело играть, и они крали сотни часов вашего времени, пока вы сидели перед этим текстом в одиночестве, пытаясь понять, как его победить.
Понятно, что с годами текстовые приключения были заменены играми с лучшими визуальными эффектами (хотя можно утверждать, что многие из них пожертвовали сюжетом ради графики) и, особенно в последние друзей и играть вместе. Это особенность, которой не хватало в оригинальных текстовых приключениях, и которую я хочу вернуть в этой статье.
Другие части этой серии
- Часть 2: Дизайн сервера игрового движка
- Часть 3: Создание терминального клиента
- Часть 4: Добавление чата в нашу игру
Наша цель
Весь смысл этой попытки, как вы, наверное, уже догадались из названия этой статьи, заключается в создании текстового движка приключений, который позволит вам поделиться приключением с друзьями, позволяя вам сотрудничать с ними так же, как во время игра Dungeons & Dragons (в которой, как и в старых добрых текстовых приключениях, нет графики, на которую можно было бы смотреть).
При создании движка, чат-сервера и клиента довольно много работы. В этой статье я покажу вам этап проектирования, объясню такие вещи, как архитектура движка, как клиент будет взаимодействовать с серверами и каковы будут правила этой игры.
Просто чтобы дать вам некоторое наглядное представление о том, как это будет выглядеть, вот моя цель:

Это наша цель. Как только мы туда доберемся, у вас будут скриншоты вместо быстрых и грязных мокапов. Итак, приступим к процессу. Первое, что мы рассмотрим, это дизайн всего этого. Затем мы рассмотрим наиболее важные инструменты, которые я буду использовать для написания кода. Наконец, я покажу вам некоторые из наиболее важных фрагментов кода (со ссылкой на полный репозиторий, конечно).
Надеюсь, к концу вы обнаружите, что создаете новые текстовые приключения, чтобы попробовать их с друзьями!
Этап проектирования
На этапе проектирования я расскажу о нашем общем плане. Я постараюсь не утомить вас до смерти, но в то же время я думаю, что важно показать некоторые закулисные вещи, которые должны произойти, прежде чем вы напишете свою первую строку кода.
Четыре компонента, которые я хочу охватить здесь с приличным количеством деталей:
- Двигатель
Это будет основной игровой сервер. Здесь будут реализованы правила игры, и он обеспечит технологически независимый интерфейс для любого типа клиентов. Мы реализуем терминальный клиент, но вы можете сделать то же самое с клиентом веб-браузера или любым другим типом, который вам нравится. - Сервер чата
Поскольку он достаточно сложен, чтобы иметь собственную статью, у этого сервиса также будет свой собственный модуль. Сервер чата позаботится о том, чтобы игроки могли общаться друг с другом во время игры. - Клиент
Как было сказано ранее, это будет терминальный клиент, который в идеале будет похож на предыдущий макет. Он будет использовать услуги, предоставляемые как движком, так и сервером чата. - Игры (файлы JSON)
Наконец, я пройдусь по определению настоящих игр. Весь смысл в том, чтобы создать движок, способный запускать любую игру, если файл вашей игры соответствует требованиям движка. Так что, хотя это и не потребует написания кода, я объясню, как я буду структурировать файлы приключений, чтобы в будущем писать наши собственные приключения.
Двигатель
Игровой движок или игровой сервер будет представлять собой REST API и будет обеспечивать все необходимые функции.
Я выбрал REST API просто потому, что для игр такого типа задержка, добавляемая HTTP, и его асинхронный характер не вызовут никаких проблем. Однако для сервера чата нам придется пойти другим путем. Но прежде чем мы начнем определять конечные точки для нашего API, нам нужно определить, на что будет способен движок. Итак, приступим.
Характерная черта | Описание |
---|---|
Присоединяйтесь к игре | Игрок сможет присоединиться к игре, указав идентификатор игры. |
Создать новую игру | Игрок также может создать новый экземпляр игры. Механизм должен возвращать идентификатор, чтобы другие могли использовать его для присоединения. |
Сцена возвращения | Эта функция должна возвращать текущую сцену, где находится вечеринка. По сути, он вернет описание со всей связанной информацией (возможные действия, объекты в нем и т. д.). |
Взаимодействие со сценой | Это будет один из самых сложных, потому что он примет команду от клиента и выполнит это действие — например, переместить, толкнуть, взять, посмотреть, прочитать, и это лишь некоторые из них. |
посмотри инвентарь | Хотя это способ взаимодействия с игрой, он не имеет прямого отношения к сцене. Таким образом, проверка инвентаря для каждого игрока будет считаться отдельным действием. |
Слово о движении
Нам нужен способ измерения расстояний в игре, потому что перемещение по приключению — одно из основных действий, которое может предпринять игрок. Мы будем использовать это число как меру времени, чтобы упростить игровой процесс. Измерение времени с помощью реальных часов может быть не лучшим решением, учитывая, что в играх такого типа есть пошаговые действия, такие как бой. Вместо этого мы будем использовать расстояние для измерения времени (это означает, что расстояние, равное 8, потребует больше времени для прохождения, чем одно из 2, что позволит нам делать такие вещи, как добавление эффектов к игрокам, которые действуют в течение установленного количества «очков расстояния»). ).
Еще один важный аспект движения, который следует учитывать, заключается в том, что мы играем не в одиночку. Для простоты движок не позволит игрокам разделить группу (хотя это может быть интересным улучшением на будущее). Первоначальная версия этого модуля позволит всем двигаться только туда, куда решит большинство участников. Таким образом, перемещение должно быть сделано на основе консенсуса, а это означает, что каждое действие перемещения будет ждать, пока большинство сторон не запросит его, прежде чем произойдет.
Бой
Бой — еще один очень важный аспект таких игр, и мы должны подумать о том, чтобы добавить его в наш движок; в противном случае мы пропустим часть веселья.
Это не то, что нужно изобретать заново, если честно. Пошаговые групповые бои существуют уже несколько десятилетий, поэтому мы просто реализуем версию этой механики. Мы будем смешивать его с концепцией «инициативы» в Dungeons & Dragons, выбрасывая случайное число, чтобы сделать бой более динамичным.
Другими словами, порядок, в котором все участники боя могут выбирать свои действия, будет случайным, включая врагов.
Наконец (хотя я расскажу об этом более подробно ниже), у вас будут предметы, которые вы можете подобрать с установленным числом «урона». Это предметы, которые вы сможете использовать во время боя; все, что не имеет этого свойства, нанесет 0 урона вашим врагам. Мы, вероятно, добавим сообщение, когда вы попытаетесь использовать эти объекты для боя, чтобы вы знали, что то, что вы пытаетесь сделать, не имеет смысла.
Клиент-серверное взаимодействие
Давайте теперь посмотрим, как данный клиент будет взаимодействовать с нашим сервером, используя ранее определенную функциональность (пока не думая о конечных точках, но мы вернемся к этому через секунду):

Начальное взаимодействие между клиентом и сервером (с точки зрения сервера) — это начало новой игры, и шаги для него следующие:
- Создайте новую игру .
Клиент запрашивает создание новой игры с сервера. - Создать чат .
Хотя в названии это не указано, сервер не только создает чат на сервере чата, но и настраивает все необходимое, чтобы группа игроков могла участвовать в приключении. - Вернуть метаданные игры .
Как только игра будет создана сервером и чат для игроков создан, клиенту потребуется эта информация для последующих запросов. В основном это будет набор идентификаторов, которые клиенты могут использовать для идентификации себя и текущей игры, к которой они хотят присоединиться (подробнее об этом чуть позже). - Вручную поделиться идентификатором игры .
Этот шаг придется сделать самим игрокам. Мы могли бы придумать какой-то механизм обмена, но я оставлю это в списке пожеланий для будущих улучшений. - Присоединяйтесь к игре .
Это довольно просто. Поскольку у всех есть идентификатор игры, они присоединятся к приключениям, используя свои клиентские приложения. - Присоединяйтесь к их чату .
Наконец, клиентские приложения игроков будут использовать метаданные игры, чтобы присоединиться к чату своего приключения. Это последний шаг, необходимый перед игрой. Как только все это будет сделано, игроки будут готовы к приключениям!

После того, как все предварительные условия выполнены, игроки могут начать играть в приключение, делиться своими мыслями в групповом чате и продвигаться по сюжету. На приведенной выше диаграмме показаны четыре шага, необходимые для этого.
Следующие шаги будут выполняться как часть игрового цикла, а это означает, что они будут повторяться постоянно, пока игра не закончится.
- Сцена запроса .
Клиентское приложение запросит метаданные для текущей сцены. Это первый шаг в каждой итерации цикла. - Вернуть метаданные .
Сервер, в свою очередь, отправит обратно метаданные для текущей сцены. Эта информация будет включать в себя такие вещи, как общее описание, объекты, найденные внутри него, и то, как они связаны друг с другом. - Отправить команду .
Здесь начинается самое интересное. Это основной вклад игрока. Он будет содержать действие, которое они хотят выполнить, и, при необходимости, цель этого действия (например, задуть свечу, схватить камень и т. д.). - Вернуть реакцию на отправленную команду .
Это может быть просто второй шаг, но для ясности я добавил его как дополнительный шаг. Основное отличие состоит в том, что второй шаг можно считать началом этого цикла, а этот учитывает, что вы уже играете, и, таким образом, сервер должен понимать, на кого это действие повлияет (будь то одиночный игрок или все игроки).
В качестве дополнительного шага, хотя и не являющегося частью процесса, сервер будет уведомлять клиентов об актуальных для них обновлениях статуса.
Причина этого дополнительного повторяющегося шага в том, что игрок может получать обновления от действий других игроков. Напомним требование о перемещении с одного места на другое; как я уже говорил, как только большинство игроков выбрали направление, все игроки будут двигаться (ввод от всех игроков не требуется).
Интересным моментом здесь является то, что HTTP (мы уже упоминали, что сервер будет REST API) не допускает такого поведения. Итак, наши варианты:
- выполнять опрос каждые X секунд от клиента,
- использовать какую-то систему уведомлений, которая работает параллельно с клиент-серверным соединением.
По моему опыту, я предпочитаю вариант 2. На самом деле, я бы (и буду использовать для этой статьи) Redis для такого поведения.
На следующей диаграмме показаны зависимости между службами.

Чат-сервер
Я оставлю детали дизайна этого модуля для этапа разработки (который не является частью этой статьи). При этом есть вещи, которые мы можем решить.
Одна вещь, которую мы можем определить, — это набор ограничений для сервера, что упростит нашу работу в дальнейшем. И если мы правильно разыграем наши карты, мы можем получить сервис с надежным интерфейсом, что позволит нам в конечном итоге расширить или даже изменить реализацию, чтобы обеспечить меньше ограничений, не влияя на игру вообще.
- На вечеринку будет только одна комната.
Мы не позволим создавать подгруппы. Это идет рука об руку с недопущением раскола партии. Возможно, как только мы внедрим это усовершенствование, хорошей идеей будет создание подгрупп и пользовательских чатов. - Личных сообщений не будет.
Это сделано исключительно для упрощения, но наличие группового чата уже достаточно; нам не нужны личные сообщения прямо сейчас. Помните, что всякий раз, когда вы работаете над своим минимально жизнеспособным продуктом, старайтесь не залезать в кроличью нору с ненужными функциями; это опасный путь, с которого трудно сойти. - Мы не будем сохранять сообщения.
Другими словами, если вы покинете вечеринку, вы потеряете сообщения. Это значительно упростит нашу задачу, поскольку нам не придется иметь дело с каким-либо типом хранилища данных, а также нам не придется тратить время на выбор наилучшей структуры данных для хранения и восстановления старых сообщений. Все это останется в памяти, и будет оставаться там до тех пор, пока активен чат. Как только он закроется, мы просто попрощаемся с ними! - Связь будет осуществляться через сокеты .
К сожалению, нашему клиенту придется обрабатывать двойной канал связи: RESTful для игрового движка и сокет для чат-сервера. Это может немного увеличить сложность клиента, но в то же время он будет использовать лучшие методы связи для каждого модуля. (Нет смысла принудительно использовать REST на нашем чат-сервере или принудительно использовать сокеты на нашем игровом сервере. Такой подход увеличит сложность кода на стороне сервера, который также обрабатывает бизнес-логику, поэтому давайте сосредоточимся на этой стороне. на данный момент.)
Вот и все для чат-сервера. Ведь это не будет сложно, по крайней мере, изначально. Когда придет время начать программировать, нужно сделать еще больше, но для этой статьи информации более чем достаточно.
Клиент
Это последний модуль, требующий написания кода, и он будет самым глупым из всех. Как правило, я предпочитаю, чтобы мои клиенты были тупыми, а серверы — умными. Таким образом, создание новых клиентов для сервера становится намного проще.
Чтобы мы были на одной волне, вот высокоуровневая архитектура, к которой мы должны прийти.

Наш простой клиент CLI не реализует ничего очень сложного. На самом деле, самое сложное, с чем нам придется столкнуться, — это фактический пользовательский интерфейс, потому что это текстовый интерфейс.
При этом функциональность, которую клиентское приложение должно будет реализовать, выглядит следующим образом:
- Создайте новую игру .
Поскольку я хочу, чтобы все было как можно проще, это можно будет сделать только через интерфейс командной строки. Фактический пользовательский интерфейс будет использоваться только после присоединения к игре, что подводит нас к следующему пункту. - Присоединяйтесь к существующей игре .
Учитывая код игры, возвращенный из предыдущего пункта, игроки могут использовать его для присоединения. Опять же, это то, что вы должны иметь возможность делать без пользовательского интерфейса, поэтому эта функциональность будет частью процесса, необходимого для начала использования текстового пользовательского интерфейса. - Разобрать файлы определения игры .
Мы обсудим это немного позже, но клиент должен понимать эти файлы, чтобы знать, что показывать, и знать, как использовать эти данные. - Взаимодействуйте с приключением.
По сути, это дает игроку возможность взаимодействовать с описываемой средой в любой момент времени. - Поддерживайте инвентарь для каждого игрока .
Каждый экземпляр клиента будет содержать в памяти список элементов. Этот список будет сохранен. - Чат поддержки .
Клиентское приложение также должно подключиться к серверу чата и зарегистрировать пользователя в комнате чата группы.
Подробнее о внутренней структуре и дизайне клиента позже. А пока давайте закончим стадию дизайна последней частью подготовки: игровыми файлами.
Игра: файлы JSON
Здесь становится интересно, потому что до сих пор я рассматривал основные определения микросервисов. Некоторые из них могут говорить на языке REST, а другие могут работать с сокетами, но по сути все они одинаковы: вы их определяете, кодируете, а они предоставляют сервис.
Для этого конкретного компонента я не планирую ничего кодировать, но нам нужно его спроектировать. По сути, мы реализуем своего рода протокол для определения нашей игры, сцен внутри нее и всего, что внутри них.
Если подумать, текстовое приключение по своей сути представляет собой набор комнат, соединенных друг с другом, а внутри них находятся «вещи», с которыми вы можете взаимодействовать, и все они связаны между собой, надеюсь, приличной историей. Теперь наш движок не позаботится об этой последней части; эта часть будет зависеть от вас. Но в остальном есть надежда.
Теперь, возвращаясь к набору взаимосвязанных комнат, для меня это звучит как график, и если мы также добавим понятие расстояния или скорости движения, о котором я упоминал ранее, мы получим взвешенный график. И это всего лишь набор узлов, у которых есть вес (или просто число — не беспокойтесь о том, как оно называется), которое представляет этот путь между ними. Вот наглядное изображение (я люблю учиться, видя, так что просто посмотрите на изображение, хорошо?):

Это взвешенный график — вот и все. И я уверен, что вы уже поняли это, но для полноты картины позвольте мне показать вам, как вы это сделаете, когда наш движок будет готов.
Как только вы начнете готовить приключение, вы создадите свою карту (как вы видите слева на изображении ниже). А затем вы переведете это на взвешенный график, как вы можете видеть справа на изображении. Наш движок сможет подобрать его и позволить пройти по нему в правильном порядке.

С помощью приведенного выше взвешенного графика мы можем убедиться, что игроки не могут пройти от входа до левого крыла. Им придется пройти через узлы между этими двумя, и на это потребуется время, которое мы можем измерить, используя вес соединений.
Теперь о «весёлой» части. Давайте посмотрим, как будет выглядеть график в формате JSON. Потерпите меня здесь; этот JSON будет содержать много информации, но я пройду через нее столько, сколько смогу:
{ "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } }
{ "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } }
{ "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } }
{ "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } }
Я знаю, что это выглядит много, но если вы сведете это к простому описанию игры, у вас есть подземелье, состоящее из шести комнат, каждая из которых связана с другими, как показано на диаграмме выше.

Ваша задача - двигаться по нему и исследовать его. Вы обнаружите, что есть два разных места, где вы можете найти оружие (либо на кухне, либо в темной комнате, сломав стул). Вы также столкнетесь с запертой дверью; так что, как только вы найдете ключ (находится в комнате, похожей на офис), вы сможете открыть его и сразиться с боссом любым оружием, которое вы собрали.
Вы либо выиграете, убив его, либо проиграете, если он убьет вас.
Давайте теперь перейдем к более подробному обзору всей структуры JSON и ее трех разделов.
График
Этот будет содержать отношения между узлами. По сути, этот раздел напрямую переводится на график, который мы рассматривали ранее.
Структура этого раздела довольно проста. Это список узлов, где каждый узел содержит следующие атрибуты:
- идентификатор, который однозначно идентифицирует узел среди всех других в игре;
- имя, которое в основном представляет собой удобочитаемую версию идентификатора;
- набор ссылок на другие узлы. Об этом свидетельствует существование четырех возможных ключей: север, юг, восток и запад. В конечном итоге мы могли бы добавить дополнительные направления, добавляя комбинации этих четырех. Каждая ссылка содержит идентификатор связанного узла и расстояние (или вес) этого отношения.
Игра
Этот раздел будет содержать общие настройки и условия. В частности, в приведенном выше примере этот раздел содержит условия выигрыша и проигрыша. Другими словами, с этими двумя условиями мы сообщим движку, когда игра может закончиться.
Для простоты я добавил всего два условия:
- вы либо побеждаете, убивая босса, либо
- или проиграть, будучи убитым.
Номера
Вот откуда взялась большая часть из 163 строк, и это самый сложный из разделов. Здесь мы опишем все комнаты в нашем приключении и все, что внутри них.
Для каждой комнаты будет свой ключ с идентификатором, который мы определили ранее. И у каждой комнаты будет описание, список предметов, список выходов (или дверей) и список неигровых персонажей (NPC). Из этих свойств обязательным должно быть только описание, потому что оно необходимо для того, чтобы движок сообщал вам, что вы видите. Остальные будут там только в том случае, если есть что показать.
Давайте посмотрим, что эти свойства могут сделать для нашей игры.
Описание
Этот пункт не так прост, как можно подумать, ведь ваш взгляд на комнату может меняться в зависимости от разных обстоятельств. Если, например, вы посмотрите на описание первой комнаты, то заметите, что по умолчанию вы ничего не видите, если, конечно, у вас нет с собой зажженного факела.
Таким образом, подбор предметов и их использование могут вызвать глобальные условия, которые повлияют на другие части игры.
Предметы
Они представляют собой все, что вы можете найти в комнате. Каждый элемент имеет тот же идентификатор и имя, что и узлы в разделе графика.
У них также будет свойство «назначение», которое указывает, где этот предмет должен храниться после того, как его взяли. Это актуально, потому что вы сможете иметь только один предмет в руках, тогда как в инвентаре вы сможете иметь столько, сколько захотите.
Наконец, некоторые из этих предметов могут запускать другие действия или обновления статуса, в зависимости от того, что игрок решит с ними сделать. Одним из примеров этого являются зажженные факелы у входа. Если вы возьмете одну из них, вы вызовете обновление статуса в игре, что, в свою очередь, заставит игру показать вам другое описание следующей комнаты.
Предметы также могут иметь «подпредметы», которые вступают в игру после уничтожения исходного предмета (например, с помощью действия «сломать»). Элемент можно разбить на несколько, что определяется в элементе «subitems».
По сути, этот элемент представляет собой просто массив новых элементов, который также содержит набор действий, которые могут инициировать их создание. Это в основном открывает возможность создавать различные подэлементы на основе действий, которые вы выполняете с исходным элементом.
Наконец, некоторые предметы будут иметь свойство «урон». Таким образом, если вы используете предмет, чтобы ударить NPC, это значение будет использоваться для вычитания из них жизни.
Выходы
Это просто набор свойств, указывающих направление выхода и его свойства (описание, если вы хотите проверить его, его имя и, в некоторых случаях, его статус).
Выходы — это отдельный объект от элементов, потому что движку необходимо понять, можете ли вы на самом деле пройти по ним, основываясь на их статусе. Заблокированные выходы не позволят вам пройти через них, если вы не придумаете, как изменить их статус на разблокированные.
NPC
Наконец, NPC будут частью другого списка. В основном это элементы со статистикой, которую движок будет использовать, чтобы понять, как должен вести себя каждый из них. В нашем примере мы определили «hp», обозначающие очки здоровья, и «damage», который, как и в случае с оружием, представляет собой число, которое каждое попадание будет вычитаться из здоровья игрока.
Вот и все для подземелья, которое я создал. Да, это много, и в будущем я мог бы подумать о создании своего рода редактора уровней, чтобы упростить создание файлов JSON. Но пока в этом нет необходимости.
Если вы еще этого не поняли, главное преимущество определения нашей игры в файле, подобном этому, заключается в том, что мы сможем переключать файлы JSON, как вы это делали с картриджами в эпоху Super Nintendo. Просто загрузите новый файл и начните новое приключение. Легкий!
Заключительные мысли
Спасибо, что дочитали до сих пор. Надеюсь, вам понравился процесс проектирования, через который я прохожу, чтобы воплотить идею в жизнь. Помните, однако, что я придумываю это на ходу, поэтому позже мы можем понять, что то, что мы определили сегодня, не будет работать, и в этом случае нам придется вернуться и исправить это.
Я уверен, что есть масса способов улучшить представленные здесь идеи и сделать чертовски крутой движок. Но для этого потребуется гораздо больше слов, чем я могу вместить в статью, не делая ее скучной для всех, так что пока остановимся на этом.
Другие части этой серии
- Часть 2: Дизайн сервера игрового движка
- Часть 3: Создание терминального клиента
- Часть 4: Добавление чата в нашу игру