Движок для текстовых адвенчур TDZ

Движок для текстовых адвенчур TDZ.

автор: Dimouse


Статья посвящена описанию, комментариям к исходникам (которые прилагаются к журналу), а также примерам использования нового движка для игр жанра Interactive Fiction, который называется TDZ (Textmode Dimouse & Zubik). Сделан он был мной по двум причинам: во-первых, сейчас мной проводится конкурс текстовых игр под названием "Иллюзия Выбора", о подробностях которого вы можете узнать по ссылке http://text-game.old-games.ru

Во-вторых, я хотел сделать систему скриптов для своей РПГ (о которой немножко можно прочитать в прошлом выпуске дискмага). Дело в том, что взаимодействие игрока с игровым миром и влияние на него, разветвления сюжета и т.д. - довольно нетривиальное дело. Обычно все ограничивается тем, что NPC дает задание, вы его выполняете, получаете награду и в дальнейшем никак уже не вспоминаете, что вы это сделали. Невозможность сделать что-то в данный момент времени обычно ограничивается лишь объективными факторами - в пещере с главным боссом слишком сильные враги и только поэтому вам нужно перед этим зачистить большинство остальной территории. Конечно же, бывают и исключения. Довольно интересная система скриптов и взаимодействия с игровым миром была в игре Lord of the Rings vol. 1 и в других похожих играх, например Badblood. Однако в LotR, несмотря на большое количество событий и триггеров, они мало были достаточно независимыми друг от друга. В Badblood же сама игра была очень короткая и там было не так уж сложно просчитать все взаимосвязи, которых было довольно мало. То есть настоящих квестовых элементов в РПГ играх по сути как не было, так и нет. Особенно в нынешнее время, где это только бы отпугнуло потенциальных игроков и тем самым снизило бы уровень продаж.

Для того чтобы реализовать систему скриптов, я решил начать с малого - сделать движок для текстовых игр. Обычно, движки для Интерактивной Литературы с парсерным интерфейсом (ввод фраз из командной строки) представляют собой некий объектно-ориентированный язык, наподобие С++, который позволяет описывать объекты и задавать им свойства, какие только могут пригодиться. Например, что-то лежит под чем-то, что-то находится внутри другого объекта. Можно задавать другие описания и наследовать одни предметы от других, таким образом вам не потребуется многократно описывать результат действия "открыть ящик", если таких ящиков у вас в игре десятки. Однако все это имеет и обратную сторону - таким образом вы теряете уникальность. Варианты ответов на одинаковые действия могут конечно варьироваться случайным образом из заданного списка, но уникальность каждого действия уже потеряна. Вы уже не чувствуете, что попали в историю, приключение. В этом смысле мне ближе механизм классических квестов - когда на каждую, даже самую дурацкую команду или применение предмета из инвентаря на объект на экране следует уникальный и обычно смешной результат. Вспомните Space Quest, Goblins, Simon the Sorcerer, да и многие-многие другие игры, в которых особенный шарм заключался именно в том, чтобы посмотреть, а что будет, если сделать это с этим? Кто может забыть всевозможные забавные смерти в Space Quest? А ведь авторам этих игр приходилось прописывать самим все эти действия, но никто не ленился и получалось отлично! Вот примерно в таком стиле я и захотел написать игру. Единственное отличие - отсутствие графики, вместо нее - текстовые описания. Возможно графика бы была совсем не лишней, но рисовать я не умею, да и сделать что-то несовсем обычное (кто-то скажет, что устаревшее) всегда интересно.

Итак, перейдем к движку. Сначала я дам некоторые комментарии по коду, который вы можете посмотреть в дополнении к журналу, в ходе этого будут разобраны примеры применения скриптов из демо-версии игры Simon the Sorcer Textmode, которую я потихоньку разрабатываю (пока не надоест).

Начнем с начала - глобальные переменные. Что делать, но я настолько ленив, что все время обзовожусь кучей глобальных переменных. Я знаю, что это нехорошо, но каждый раз не могу удержаться от того, чтобы облегчить себе жизнь таким некрасивым способом. Итак у нас есть определенная дефайном константа SN - количество сцен в игре. Сцены - это, грубо говоря, экраны в обычном квесте. В пределах этого экрана можно воздействовать на объекты сцены. Воздействие производится с помощью класса "действий" - основной класс всего движка (поскольку на сами объекты практически никаких функций не возлагается). Эти действия имею некий индикатор, похожий на рычажок "on/off". Сделал действие - оно становится "off". Но конечно не все так легко. Каждое действие также содержит массив "условий" и "результатов". "Условия" - это статус (on или off) действий, которые необходимы для совершения данного. Если все условия выполнены в данный момент, то действие происходит. При этом всем действиям из списка "результатов" меняются статусы. Кроме того, действие может добавить или убрать объект из инвентаря (объекты в инвентаре - те же самые объекты, что и на экранах). Те действия, которые можно совершить на любой сцене, относятся к так называемой "глобальной сцене". Главным образом - это взаимодействие с объектами, которые могут находиться в инвентаре. Например, прочитать письмо, съесть сыр и т.д.

Приведем пример использования действия из демо:
9 1 4 1 / 0 0 0 / 21 1 0 0 0 0 / 0 0 1 20 Вы открыли дверь. /
10 1 4 1 / 0 0 0 / 20 1 0 0 0 0 / 0 0 0 21 Вы закрыли дверь на засов. /

Эта строчка содержится в файле scenes.dat, который можно редактировать (однако это нужно делать осторожно, так как никакой "защиты от дурака" я не делал и если что-то будет написано не по законам игры, то игра вылетит без всяких предупреждений). "Что же обозначают эти цифры?" - спросите вы. Первое число - это глагол, которым нужно действовать на данный объект. На данный момент глаголов не слишком много, но я не думаю, что их количество будет увеличено. Итак, вот таблица соответствия глаголов и чисел:
1 поговорить
2 взять
3 съесть
4 совместить
5 обнюхать
6 одеть
7 снять
8 ощупать
9 открыть
10 закрыть
11 отдать
12 прочитать

Дальше идет количество слов в "магической фразе", то есть той строчке, которую необходимо ввести игроку, чтобы произвести данное действие. Например, "открыть дверь" или "поговорить с барменом" (кстати, на данный момент я еще не сделал все необходимые падежи и писать надо "поговорить с бармен"). Число, записанное в scenes.dat должно быть на единицу меньше, чем число слов в "магической фразу", то есть для "открыть дверь" нужно писать 1, а для "поговорить с бармен" - 2. Далее идет объект сцены, на который собственно и производится действие (обычно последнее слово в "магической фразе" и также как правило в винительном падеже) и номер сцены, на котором он находится. Сцены нумеруются последовательно с нулевой (которая является глобальной).

После первого слеша идут "условия" - номер действия, статус и сцена. Заканчиваться этот список обязательно должен трями нулями и слешем. Далее идет список "результатов", аналогичный за тем лишь исключением, что -1 в номере действия там означает, что результат - смерть. После следующего слеша идут последовательно номера прибавляющихся предметов в инвентаре с номерами их сцен, заканчивающиеся двумя нулями. Если предмет надо отнять из инвентаря, а не прибавить, то номер сцены должен быть отрицательным. Далее идет начальный статус сцены и наконец ее номер (уникальный для данной сцены). Потом идет фраза, которая пишется, когда действие производится (не более 300 знаков).

Таким образом в вышеприведенном примере - дверь изначально открыта (поэтому ее не получится открыть, а можно только закрыть). Зато когда дверь закрывается (тем самым меняя статус 21-го действия на 1), также меняется статус и 20-го действия на 0, после чего можно уже только открыть, но нельзя закрыть. С действием 20 происходит ровно та же самая картина. Есть и более сложные примеры, например, холодильник, в котором лежат два объекта. Каждый из них можно достать, закрыть холодильник и открыть заново, при этом должно писаться что там лежит. И это можно сделать на примере скриптов, причем колчество действий будет равно количеству различных ответов. Этот пример также реализован в демо, кому интересно - могут посмотреть.

Файл scenes.dat содержит данные для инициализации каждой сцены. Эти данные разделены амперсантами &, потом идет название сцены, ее описание (не более 700 знаков), выходы с нее (четыре выхода в следующем порядке - восток, запад, север, юг), если выхода в данном направлении нет - то ставится -1, если есть - то номер сцены, на которую он ведет. Далее идет список объектов на сцене: номер, имя (одно слово), имя в винительном падеже, описание (не более 500 знаков). И наконец потом идут все действия, принадлежащие данной сцене, в уже разобранном ранее формате.

Но все же вернемся к собственно исходникам, в которых мы не успели разобрать даже самое начало. Да, кстати, я предпочитаю писать программы в виде больших файлов, а не разбивать все на маленькие. Мне так удобно редактировать, однако я понимаю, что остальным это уже не так удобно. Приношу извинения за свой ужасающий многих стиль программирования. Не буду касаться всяких вспомогательных функций, призванных облегчить работу в режиме интерфейса "Casual", поскольку они являются второстепенными и мало кому интересными (тем более что можно ведь работать и в режиме Classic). Описания классов action, scene и object теперь уже должны быть вам понятны. Класс hero пока что не нужен и возможно и не будет нужен.

Функция scene::decl() инициализирует сцену из файла scenes.dat. Это нужно производить один раз, перед главным циклом в main(). scene::show_descr() показывает описание сцены на экран. Как можно видеть функция довольно длинная, посколько позволяет работать с двумя типами интерфейсов и с двумя вариантами переносов слов (либо они переносятся, либо слово обрывается на середине и продолжается с новой строчки). Аналогичные функции есть и в классах object и action (все они идут подряд).

А вот дальше уже идет нечто интересное - класс parser! Собственно вся эта штука должна брать на себя взаимодействие с игроком. Главное в нем - это буфер слов, которые ввел игрок. За ввод этих слов отвечает фукция read(), суть которой - это как раз определение этого буфера. В буфере дожно быть не больше 10 слов (попробуйте ввести больше 10 пробелов в игре!). Кроме того ввод русских слов сделан довольно интересно - он должен осуществляться в аглийской раскладке, после чего конфертируется в русские буквы. Ввод непосредственно русских букв у меня сделать почему-то не получилось.


Не будем особенно касаться ввода строки в парсер, так как это все-таки техническая сторона дела и не столь интересна. Перейдем к анализу того, что игрок ввел в буфер. За это отвечают две функции - analyze() и test_scene(). Основная - analyze вызывает потом test_scene на текущую сцену и на глобальную, при этом просматриваются все действия в этих сценах на пример того - а не ввел ли игрок именно ту "магическую фразу", которая вызывает это действие и возможно ли его сделать в данных условиях (если игрок ввел корректную фразу, но данное действие уже или еще невозможно сделать, то пишется специальная фраза). В самом же анализаторе проверяются всевозможные команды, не относящиеся к действиям, такие как "осмотреться", "помощь", "инвентарь", "выход", "идти", а также "осмотреть объект", которая в данный момент всегда воспроизводит единственный вариант описания объекта. Это не слишком хорошо, но пока что я еще не придумал, как эту проблему решить.

И, наконец, последний штрих программы - функия main(), в которой инициализируются настройки игры из файла tdz.cfg, а также содержится главный цикл игры, который всего лишь последовательно вызывает read() и analyze(). Вот и все!

Последнее изменение Sun, 24 Jun 2018 автором Dimouse


Назад в раздел Old-games Diskmag 6