О Русском Информе
На Информе можно писать игры с текстовым вводом. Классические примеры таких игр — Adventure и Zork. Здесь на сайте можно поиграть в такие игры на русском языке, а кроме этого скачать инструменты для разработки своих собственных игр.
Русский Информ (RInform) — это перевод стандартной библиотеки популярной на западе системы Inform 6.
Официальный сайт русской версии: https://rinform.org/
В данной книге подробно разбирается разработка простой игры («Хейди»), и есть ответы на часто задаваемые вопросы (FAQ).
Введение и установка
Для того, чтобы написать игру на Информе, нам потребуются:
- текстовый редактор для редактирования исходных файлов игры (официально рекомендуется Sublime Text, но, разумеется, подойдёт любой);
- библиотека Информа, которую нужно будет включить в свою игру, чтобы работала общая модель и стандартное поведение;
- компилятор Информа, с помощью которого из исходного файла мы получим файл игры;
- интерпретатор (плеер), с помощью которого в полученную игру можно будет сыграть.
Библиотеку и компилятор можно скачать с сайта Информа. В архиве помимо прочего есть примеры игр.
Игры можно запускать онлайн, и это самый популярный вариант, но на время разработки обычно пользуются одним из «оффлайн»-плееров:
- Lectrote — Windows, Linux, OS X (использует Electron).
- Windows Glulxe для Windows. Там же есть более быстрый Windows Git.
Установка и проверка
Начиная с версии 0.9, версия Glulx считается приоритетной, а версия для Z-машины вторичной. Поэтому в данной книге все примеры подразумевают работу с Glulx. О различиях можно почитать в FAQ.
Для проверки работоспособности и первоначальной настройки:
-
скачайте архив библиотеки с официального сайта и распакуйте его в отдельную, удобно доступную папку, например
c:\rinform\.В этой папке будет следующее содержимое:
\demos\ демонстрационные игры \demos\demos.bat пакетные файлы для компиляции демонстрационных игр \demos\demos.sh \libext\ расширения для библиотке \library\ русская версия библиотеки inform.exe компилятор Inform (для Windows) -
В папке demos можно увидеть несколько файлов с расширением
.inf— это исходные файлы демонстрационных игр. Их можно открыть и просмотреть в текстовом редакторе. -
Файл
demos.batэто пакетный файл («батник»), при помощи которого упрощается компилирование файлов игр. Запустите этот файл (если на Linux, тоdemos.sh), на экране появится примерно следующее:c:\rinform\demos>..\inform.exe +..\library +language_name=Russian -DG -Cu $DICT_CHAR_SIZE=4 heidi.inf Heidi.ulx Inform 6.34 (16th August 2017) c:\rinform\demos>..\inform.exe +..\library +language_name=Russian -DG -Cu $DICT_CHAR_SIZE=4 AliceR.inf AliceR.ulx Inform 6.34 (16th August 2017)Если прочих сообщений нет, то всё прошло без ошибок.
-
В папке
demosпоявятся файлы с расширением.ulx— это готовые файлы игр, которые можно запустить в интерпретаторе. Если установлены Windows Glulxe или Lectrote, то при запуске такого файла он скорее всего автоматически откроется в одном из этих интерпретаторов.
Организация своей игры:
-
создайте папку для игры, например c:\inform\mygame\
-
создайте главный исходный файл игры, c:\inform\mygame\mygame.inf. Для удобства воспользуйтесь шаблоном:
!% !============================================================================= Constant Story "Новая игра"; Constant Headline "^Шаблон игры на Информе^"; Include "Parser"; Include "VerbLib"; !============================================================================ ! Описание игровых объектов Object first_room "Первая комната" with description "Первая комната.", has light; !============================================================================ [ Initialise; location = first_room; ]; !============================================================================ Include "RussiaG"; !============================================================================ -
создайте .bat-файл c:\inform\mygame\mygame.bat для удобной компиляции:
..\inform.exe +..\library +language_name=Russian -G -Cu $DICT_CHAR_SIZE=4 mygame.inf
Компиляция игры
Чтобы скомпилировать игру, нужно через командную строку вызвать компилятор Информа inform.exe с верными параметрами.
Рассмотрим на примере демонстрационной игры Heidi, что означают параметры:
..\inform.exe +..\library +language_name=Russian -G -Cu $DICT_CHAR_SIZE=4 heidi.inf Heidi.ulx
+..\library— путь к папке, в которой хранится библиотека+language_name=Russian— параметр языка игры-G— формат игры (Glulx)-Cuи$DICT_CHAR_SIZE=4— означают использование Юникода (UTF-8) в исходном коде игрыheidi.inf— код игрыHeidi.ulx— выходной файл (файл игры)
Последними параметрами являются входной и выходной файлы. Перед ними также можно указать необязательные свитчи, например -D для компиляции дебаг-версии игры.
Форматов игры исторически есть несколько, но ныне используется только Glulx. Ранее для формата Z-машины использовались V5 и V8, у которых максимальный размер игры формата V5 — 256 кб, а игры формата V8 — 512 кб. Игры Glulx могут достигать 4 гигабайт.
Пример игры: Хейди
В данной главе будет подробно разобрана разработка простой игры под названием «Хейди».
В готовую игру можно поиграть онлайн: https://iplayif.com/?story=https://rinform.org/demos/Heidi.z5
А конечный исходный код почитать здесь: https://github.com/yandexx/rinform-glulx/blob/master/demos/Heidi.inf
Начало
Пошагово рассмотрим написание простенькой игры Хейди, сюжет которой таков:
«Хейди живёт в маленьком домике в лесу. Одним солнечным днём она слышит писк птички — её гнездо упало с ветки на поляну. Хейди кладёт птичку в гнездо, а гнездо обратно на ветку».
Шаблон для игры
Для начала создадим исходный файл-шаблон. Создайте папку heidi в папке Информа (например, c:\rinform\heidi\), а в ней, при помощи текстового редактора, файл heidi.inf со следующим содержимым:
!% -SD
!=============================================================================
Constant Story "Хейди";
Constant Headline
"^Пример простой игры на Inform.
^Авторы: Роджер Фирт (Roger Firth) и Соня Кессерих (Sonja Kesserich).
^Перевод Юрия Салтыкова a.k.a. G.A. Garinson^";
Include "Parser";
Include "VerbLib";
!============================================================================
! Описание игровых объектов
!============================================================================
! Функции инициализации
[ Initialise; ];
!============================================================================
! Стандартные и расширенные грамматики
Include "RussiaG";
!============================================================================
Пока можно просто скопировать и вставить этот текст; каждая строчка будет рассмотрена позднее. И убедитесь, что файл называется именно heidi.inf, а не, например, heidi.txt (регистр значения не имеет).
В папке игры с помощью текстового редактора создайте следующий файл heidi.bat:
..\inform.exe +..\library +language_name=Russian -G -Cu $DICT_CHAR_SIZE=4 heidi.inf
pause
Запустите этот bat-файл, в консоли появится примерно следующее:
c:\rinform\heidi>..\inform.exe +..\library +language_name=Russian -G -Cu $DICT_CHAR_SIZE=4 heidi.inf
Inform 6.34 (16th August 2017)
c:\rinform\heidi>pause
Для продолжения нажмите любую клавишу . . .
В папке heidi появится файл heidi.ulx, который можно запустить и поиграть:
Хейди
Пример простой игры на Inform.
Авторы: Роджер Фирт (Roger Firth) и Соня Кессерих (Sonja Kesserich).
Перевод Юрия Салтыкова a.k.a. G.A. Garinson
Release 1 / Serial number 180808 / Inform v6.34 Library 6/11 SD
В темноте
Кромешная тьма – не видно ни зги!
>
Теперь разберём каждую строчку исходного файла.
Разбор исходного файла
Для исходных файлов Информа существует несколько правил:
-
Если самая первая (или первые) строчки исходного файла начинаются с
!%, то компилятор воспринимает их как параметры, а не как часть игры. В данном случае мы включаем два режима для будущей игры, Strict (-S) и Debug (-D). Strict режим в случае проблем выполнения в игре будет выдавать сообщение об ошибке, а режим Debug добавляет дополнительные команды в игру для облегчения отладки.На самом деле режим Strict включён по умолчанию и, например, чтобы его выключить (что не рекомендуется), нужно указать параметр
-~S. -
Если строка начинается с восклицательного знака, то это комментарий, и он не обрабатывается компилятором. Комментарий может начинаться и посередине строки — тогда игнорируется всё, что идёт за знаком комментария. Для многострочного комментария нужно начать каждую строку с восклицательного знака.
-
Компилятор пропускает пустые строки, а также объединяет все пробелы, знаки табуляции и знаки переноса строки в один пробел (кроме пробелов внутри строк).
Например, можно было записать наш исходник таким образом, и результат остался бы тем же:
Constant Story "Хейди"; Constant Headline "^Пример простой игры на Inform.^Авторы: Роджер Фирт (Roger Firth) и Соня Кессерих (Sonja Kesserich).^Перевод Юрия Салтыкова a.k.a. G.A. Garinson^"; Include "Parser";Include "VerbLib"; [ Initialise; ]; Include "RussiaG";Но при таком форматировании его гораздо сложнее читать.
-
В каждой игре должны быть описаны строковые константы
Story(название игры) иHeadline(короткое описание, имя автора). Вместе с датой и номером релиза они выводятся при запуске игры в заголовке. -
В каждой игре должны быть строки
Includeдля включения стандартной библиотеки (эти файлы подставляются вместо этих строк при компиляции), и именно в этом порядке:Include "Parser"; Include "VerbLib"; ... Include "RussiaG"; -
В каждой игре должна быть функция
Initialise:[ Initialise; ];В нашем примере она ничего не выполняет, но тем не менее она должна присутствовать.
-
Все команды разделяются символом «точка с запятой» (аналогично C/C++).
Любая новая игра начинается с подобного шаблона, так что можно сохранить его отдельно, чтобы воспользоваться им позднее.
Задание локаций игры
При написании игры неизбежно задумываются о тех локациях, которые в ней встретятся. В нашей игре они будут следующими:

В Информе локации называются комнатами, пусть у них может и не быть стен. Для начала опишем наши комнаты таким образом:
Object "Перед домом"
with description
"Ты стоишь около избушки, на восток от которой раскинулся лес.",
has light;
Object "В лесной чаще"
with description
"На западе, сквозь густую листву, можно разглядеть небольшое строение.
Тропинка ведет на северо-восток.",
has light;
Object "Полянка"
with description
"Посреди полянки стоит высокий платан.
Тропинка вьется меж деревьев, уводя на юго-запад.",
has light;
Object "На верхушке дерева"
with description "На этой высоте цепляться за ствол уже не так удобно.",
has light;
Рассмотрим общие принципы:
-
Задание комнаты начинается со слова
Objectи заканчивается точкой с запятой. Вообще практически всё в игре является объектами — комнаты, предметы, люди, звуки и прочее. -
Строка после слова
Object— это название, под которым объект появится в игре для игрока. -
Ключевое слово
withобозначает компилятору, что дальше идёт перечисление свойств. -
descriptionсодержит в себе подробное описание объекта. В случае комнаты этот текст выводится, когда игрок попадает в эту комнату. -
Ключевое слово
hasобозначает компилятору, что дальше идёт перечисление атрибутов. -
lightобозначает, что объект является источником света, и что игрок сможет увидеть, что происходит. В каждой комнате должен быть хотя бы один источник света, и обычно это сама комната. Иначе игрок ничего не увидит: «Кромешная тьма – не видно ни зги!»
У свойств есть название и значение (например, description и «Ты стоишь около избушки, на восток от которой раскинулся лес»), а у атрибутов есть только название.
Позже, когда игра будет готова, в ней можно будет увидеть следующее:
Перед домом
Ты стоишь около избушки, на восток от которой раскинулся лес.
Можно увидеть, как здесь используются название комнаты и её описание (description).
Соединение комнат
Комнаты заданы, и в тексте описано, как локации находится относительно друг друга — например, что из Лесной чащи можно пойти на запад к домику, либо по тропинке на северо-восток. Однако, в коде нужно явно указать, как соединены наши комнаты:
Object before_cottage "Перед домом"
with description
"Ты стоишь около избушки, на восток от которой раскинулся лес.",
e_to forest,
has light;
Object forest "В лесной чаще"
with description
"На западе, сквозь густую листву, можно разглядеть небольшое строение.
Тропинка ведет на северо-восток.",
w_to before_cottage,
ne_to clearing,
has light;
Object clearing "Полянка"
with description
"Посреди полянки стоит высокий платан.
Тропинка вьется меж деревьев, уводя на юго-запад.",
sw_to forest,
u_to top_of_tree,
has light;
Object top_of_tree "На верхушке дерева"
with description "На этой высоте цепляться за ствол уже не так удобно.",
d_to clearing,
has light;
Здесь мы сделали два изменения:
-
Между ключевым словом
Objectи именем объекта мы ввели внутреннее название, идентификатор этого объекта, которое используется внутри программы. Например, для домика этоbefore_cottage, а для лесной чащи —forest.Идентификатор не может включать в себя пробелы.
-
После описания объектов мы ввели строки, которые показывают, как соединены наши комнаты. Например для
before_cottage:e_to forest,Так игрок, который находится в первой комнате, сможет ввести в игре ИДТИ НА ВОСТОК (или просто ВОСТОК, или В), и игра перенесёт его в комнату с идентификатором
forest. Если игрок попробует пойти в другом направлении, то получит в ответ «Этот путь недоступен».Таким образом мы добавили односторонний переход на восток, из
before_cottageвforest. В объектеforestесть две строки:w_to before_cottage, ne_to clearing,Первая строка вводит путь обратно на запад к объекту
before_cottage(к домику), а вторая — на северо-восток на Полянку.В Информе есть 8 «горизонтальных» направлений:
n_toна север,ne_toна северо-восток,e_toна восток,se_toна юго-восток,s_toна юг,sw_toна юго-запад,w_toна запад,nw_toна северо-запад,а также два «вертикальных» направления
u_toвверх,d_toвниз и два дополнительных —in_toвнутрь иout_toнаружу.
Последнее что нужно добавить — это начальную локацию. В начале игры Хейди стоит перед своим домом, поэтому укажем, что игра начинается в before_cottage. Делается это в функции Initialise:
[ Initialise; location = before_cottage; ];
location — это библиотечная переменная, в которой хранится текущее положение игрока. Здесь мы указываем, что в начале игры он должен находиться в комнате before_cottage.
Теперь можно внести все эти изменения в наш изначальный шаблон и скомпилировать игру:
!=============================================================================
Constant Story "Хейди";
Constant Headline
"^Пример простой игры на Inform.
^Авторы: Роджер Фирт (Roger Firth) и Соня Кессерих (Sonja Kesserich).
^Перевод Юрия Салтыкова a.k.a. G.A. Garinson^";
Include "Parser";
Include "VerbLib";
!============================================================================
! Описание игровых объектов
Object before_cottage "Перед домом"
with description
"Ты стоишь около избушки, на восток от которой раскинулся лес.",
e_to forest,
has light;
Object forest "В лесной чаще"
with description
"На западе, сквозь густую листву, можно разглядеть небольшое строение.
Тропинка ведет на северо-восток.",
w_to before_cottage,
ne_to clearing,
has light;
Object clearing "Полянка"
with description
"Посреди полянки стоит высокий платан.
Тропинка вьется меж деревьев, уводя на юго-запад.",
sw_to forest,
u_to top_of_tree,
has light;
Object top_of_tree "На верхушке дерева"
with description "На этой высоте цепляться за ствол уже не так удобно.",
d_to clearing,
has light;
!============================================================================
! Функции инициализации
[ Initialise; location = before_cottage;];
!============================================================================
! Стандартные и расширенные грамматики
Include "RussiaG";
!============================================================================
В получившуюся игру можно поиграть. Конечно тут практически ничего нет, но можно побродить по локациям.
Добавление птицы и гнезда
Конечно же, птица и её гнездо тоже будут объектами в Информе. Опишем их:
Object bird "птенчик/"
with description "Слишком мал, чтобы летать, птенец беспомощно попискивает.",
has ;
Object nest "птичь/е гнезд/о"
with description "Гнездо сплетено из прутиков и аккуратно устлано мхом.",
has ;
Эти объекты записываются так же, как и комнаты ранее — у них есть идентификатор, название и описание. Описание комнаты выводится при входе в неё игрока, или если игрок пишет ОСМОТРЕТЬСЯ (или ОСМ, или просто О). Описание прочих объектов выводится, когда игрок вводит в игре ОСМОТРЕТЬ ПРЕДМЕТ (или ОСМ, или просто О). У этих объектов нет соединений, например e_to или w_to (они есть только у комнат) или свойства light (оно не нужно, так как освещение предоставляют комнаты).
Слэши в названии объектов необходимы для того, чтобы при упоминании названий объектов в русскоязычной игре они верно склонялись. Отделять стоит окончание, так как оно изменяется при склонении.
Во время игры игрок будет обращаться к этим предметам, например, вводя ОСМОТРЕТЬ ПТЕНЧИКА или ВЗЯТЬ ГНЕЗДО. Чтобы это корректно работало, нужно перечислить слова, которые относятся к данному объекту. Важно указать различные синонимы, чтобы игроку было проще в игре.
Object bird "птенчик/"
with description "Слишком мал, чтобы летать, птенец беспомощно попискивает.",
name 'детеныш' 'птиц' 'птичк' 'птенчик' 'птенц' 'маленьк',
has ;
Object nest "птичь/е гнезд/о"
with description "Гнездо сплетено из прутиков и аккуратно устлано мхом.",
name 'птичь' 'гнезд' 'гнездышк' 'пруть' 'прутик' 'мох',
has ;
В секции name идёт перечисление так называемых словарных (dictionary) слов в одинарных кавычках. В словарных словах нельзя использовать пробелы, запятые или точки, но сам список разделяется пробелами. Интерпретатор проверяет введённые игроком слова и сверяет их со списками словарных слов. Если игрок упоминает ПТИЧКУ, МАЛЕНЬКОГО ПТЕНЦА или ДЕТЕНЫША, значит он имеет в виду птенца (bird), если ПТИЧЬЕ ГНЕЗДО или МОХ, то речь идёт про объект nest. Если игрок введёт ГНЕЗДО ПТЕНЕЦ, то интерпретатор выведет сообщение о том, что не понимает, о чём идёт речь.
В списке нужно указывать слова, обрезая окончания, и стараться учесть все возможные уменьшительно-ласкательные формы и прилагательные, которые может ввести игрок.
Для комнат список name не нужен, так как взаимодействие с ними происходит по-другому. Например, не нужно вводить ОСМОТРЕТЬ ЛЕС, достаточно ввести ОСМ.
Для гнезда нужно ввести дополнительную особенность — чтобы в неё можно было положить птенца. Для этого мы помечаем его как container (контейнер), чтобы игрок мог ввести ПОЛОЖИТЬ ПТИЦУ В ГНЕЗДО. Также мы помечаем его как открытое — open, так как по умолчанию контейнеры закрыты.
Кроме того, для верного склонения названий объектов необходимо указать их род. Гнездо — среднего рода, поэтому помечаем его как neuter. Птенчик — мужского рода, помечаем как male. Объекты с описанием женского рода помечаются как female, а объекты множественного числа — plural.
Object nest "птичь/е гнезд/о"
with description "Гнездо сплетено из прутиков и аккуратно устлано мхом.",
name 'птичь' 'гнезд' 'гнездышк' 'пруть' 'прутик' 'мох',
has container open neuter;
Теперь оба объекта готовы, и осталось ввести их в игру. Пусть птенец будет в лесу, а гнездо на поляне:
Object bird "птенчик/" forest
with description "Слишком мал, чтобы летать, птенец беспомощно попискивает.",
name 'детеныш' 'птиц' 'птичк' 'птенчик' 'птенц' 'маленьк',
has male;
Object nest "птичь/е гнезд/о" clearing
with description "Гнездо сплетено из прутиков и аккуратно устлано мхом.",
name 'птичь' 'гнезд' 'гнездышк' 'пруть' 'прутик' 'мох',
has container open neuter;
Первую строчку можно прочитать так: «описание объекта bird с названием "птенчик", который изначально будет находиться в объекте forest».
Размещать объекты в исходном файле игры можно где угодно, но удобнее всего это делать рядом с соответствующими комнатами.
Часть кода с объектами будет выглядеть так:
!============================================================================
! Описание игровых объектов
Object before_cottage "Перед домом"
with description
"Ты стоишь около избушки, на восток от которой раскинулся лес.",
e_to forest,
has light;
Object forest "В лесной чаще"
with description
"На западе, сквозь густую листву, можно разглядеть небольшое строение.
Тропинка ведет на северо-восток.",
w_to before_cottage,
ne_to clearing,
has light;
Object bird "птенчик/" forest
with description "Слишком мал, чтобы летать, птенец беспомощно попискивает.",
name 'детеныш' 'птиц' 'птичк' 'птенчик' 'птенц' 'маленьк',
has male;
Object clearing "Полянка"
with description
"Посреди полянки стоит высокий платан.
Тропинка вьется меж деревьев, уводя на юго-запад.",
sw_to forest,
u_to top_of_tree,
has light;
Object nest "птичь/е гнезд/о" clearing
with description "Гнездо сплетено из прутиков и аккуратно устлано мхом.",
name 'птичь' 'гнезд' 'гнездышк' 'пруть' 'прутик' 'мох',
has container open neuter;
Object top_of_tree "На верхушке дерева"
with description "На этой высоте цепляться за ствол уже не так удобно.",
d_to clearing,
has light;
Внесите эти изменения, скомпилируйте и запустите. В игре можно будет увидеть следующее:
В лесной чаще
На западе, сквозь густую листву, можно разглядеть небольшое
строение. Тропинка ведет на северо-восток.
Здесь имеется птенчик.
>
Дерево и ветка
В описании полянки есть высокий платан, на который игрок сможет залезть. Опишем его:
Object tree "высок/ий платан/" clearing
with description
"Величавое дерево стоит посреди поляны.
Кажется, по его стволу будет несложно влезть наверх.",
name 'высок' 'платан' 'дерев' 'ствол' 'величав',
has scenery male;
В этом описании нам уже всё знакомо, за исключением scenery. Поскольку мы уже написали о дереве в описании локации, нам не нужно, чтобы в игре выводилось «Здесь имеется высокий платан». Для этого мы помечаем его как scenery. Кроме того, scenery запрещает игроку возможность подобрать объект.
И наконец ветка наверху дерева:
Object branch "надежн/ый толст/ый сук/" top_of_tree
with description "Сук достаточно ровный и крепкий, чтобы на нем надежно
держалось что-то не очень большое.",
name 'надежн' 'ровн' 'толст' 'крепк' 'сук' 'ветк',
has static supporter male;
Здесь встречаются два новых атрибута. static аналогичен scenery, то есть запрещает подбирать объект, но в отличие от scenery всё равно выводит объект отдельно. supporter аналогичен container, но позволяет класть объекты не в, а на сук. (Позднее будет объяснено, что объект не может быть одновременно и container, и supporter).
Получаем следующее:
!============================================================================
! Описание игровых объектов
Object before_cottage "Перед домом"
with description
"Ты стоишь около избушки, на восток от которой раскинулся лес.",
e_to forest,
has light;
Object forest "В лесной чаще"
with description
"На западе, сквозь густую листву, можно разглядеть небольшое строение.
Тропинка ведет на северо-восток.",
w_to before_cottage,
ne_to clearing,
has light;
Object bird "птенчик/" forest
with description "Слишком мал, чтобы летать, птенец беспомощно попискивает.",
name 'детеныш' 'птиц' 'птичк' 'птенчик' 'птенц' 'маленьк',
has male;
Object clearing "Полянка"
with description
"Посреди полянки стоит высокий платан.
Тропинка вьется меж деревьев, уводя на юго-запад.",
sw_to forest,
u_to top_of_tree,
has light;
Object tree "высок/ий платан/" clearing
with description
"Величавое дерево стоит посреди поляны.
Кажется, по его стволу будет несложно влезть наверх.",
name 'высок' 'платан' 'дерев' 'ствол' 'величав',
has scenery male;
Object nest "птичь/е гнезд/о" clearing
with description "Гнездо сплетено из прутиков и аккуратно устлано мхом.",
name 'птичь' 'гнезд' 'гнездышк' 'пруть' 'прутик' 'мох',
has container open neuter;
Object top_of_tree "На верхушке дерева"
with description "На этой высоте цепляться за ствол уже не так удобно.",
d_to clearing,
has light;
Object branch "надежн/ый толст/ый сук/" top_of_tree
with description "Сук достаточно ровный и крепкий, чтобы на нем надежно
держалось что-то не очень большое.",
name 'надежн' 'ровн' 'толст' 'крепк' 'сук' 'ветк',
has static supporter male;
Вновь скомпилируйте игру, запустите её и проверьте, что можно сделать с объектами.
Завершение
Первый вариант игры почти готов, осталось внести два изменения. Во-первых, нельзя чтобы Хейди могла забраться на дерево держа в руках и птенца, и гнездо — нужно чтобы игрок сначала положил птенца в гнездо. Есть простой способ ввести это ограничение:
Constant Story "Хейди";
Constant Headline
"^Пример простой игры на Inform.
^Авторы: Роджер Фирт (Roger Firth) и Соня Кессерих (Sonja Kesserich).
^Перевод Юрия Салтыкова a.k.a. G.A. Garinson^";
Constant MAX_CARRIED 1;
Константа MAX_CARRIED ограничивает количество предметов, которые могут быть одновременно в руках у игрока. Установив её равной 1, мы указываем что игрок может держать либо гнездо, либо птенца, но не оба одновременно. Однако это ограничение не учитывает содержимое container'ов или supporter'ов, поэтому птенец в гнезде считается за один объект.
Второе изменение чуть более сложное и более важное — сейчас нет способа «выиграть» игру, то есть пройти её. Цель игры — положить птенца в гнездо, подняться на верхушку дерева и положить гнездо на сук. Если это условие выполнено, то игра должна закончиться. Вот как можно это сделать:
Object branch "надежн/ый толст/ый сук/" top_of_tree
with description "Сук достаточно ровный и крепкий, чтобы на нем надежно
держалось что-то не очень большое.",
name 'надежн' 'ровн' 'толст' 'крепк' 'сук' 'ветк',
each_turn [; if (nest in branch) deadflag = 2; ],
has static supporter male;
Все нововведения будут рассмотрены подробнее в следующих главах. Рассмотрим их кратко.
Библиотечная переменная deadflag обычно равна 0. Если присвоить ей значение 2, то интерпретатор заметит это и выведет сообщение «Вы выиграли». Строку
if (nest in branch) deadflag = 2;
можно прочитать так: «Проверить, находится ли объект nest в объекте branch (если branch является container) или на этом объекте (если branch является supporter) — если да, то присвоить deadflag значение 2». Далее,
each_turn [; ... ],
стоит понимать как «В конце каждого хода (если игрок находится в той же комнате, где сук), выполнить то, что записано в квадратных скобках». В итоге получаем:
-
В конце каждого хода (когда игрок ввёл команду, нажал Enter, и интерпретатор выполнил эту команду), интерпретатор проверяет, находится ли игрок в той же комнате, где находится
branch. Если нет, то ничего не выполняется. Если да, то интерпретатор проверяет, где находитсяnest. Изначально гнездо на полянке, поэтому ничего не происходит. -
Также, в конце каждого хода интерпретатор проверят значение
deadflag. Обычно оно равно 0, поэтому ничего не происходит. -
Затем игрок кладёт гнездо на сук. Интерпретатор видит это и устанавливает
deadflagравным 2. -
Сразу же после этого интерпретатор видит, что
deadflagравен 2, то есть, что игра закончена, и выводит на экран «Вы выиграли».
На этом глава закончена, перекомпилируйте игру и проверьте, как работают внесённые изменения. Многое можно сделать получше, и это будет рассмотрено позднее. В следующей главе будет обобщение уже изученных особенностей Информа.
Обобщение
Теперь пройдёмся более организованно и подробно по тем принципам, что мы увидели в предыдущей главе.
Константы и переменные
Константа это такое имя, значение которого строго задаётся и не может изменяться. Ранее мы встретили строковую константу:
Constant Story "Хейди";
и числовую константу:
Constant MAX_CARRIED 1;
Это два наиболее частых способа использования констант в Информе.
Значение переменной может изменяться в ходе игры. Мы ещё не задавали собственных переменных, но уже пользовались двумя такими из библиотеки, где они заданы следующим образом:
Global location;
Global deadflag;
Значение глобальной переменной по умолчанию равно 0, но его можно изменить в любой момент, например мы ввели:
location = before_cottage;
для изменения location на before_cottage, а также
if (nest in branch) deadflag = 2;
для изменения deadflag на 2.
В других главах будет освещено использование локальных переменных и свойств как переменных.
Описание объектов
Как можно было заметить из предыдущей главы, вся игра описывается в виде объектов. Каждая комната, предмет и даже сам игрок является объектом (объект игрока задаётся библиотекой).
Общий формат задания объекта следующий:
Object идентификатор "игровое_имя" родительский_объект
with свойство значение,
свойство значение,
...
свойство значение,
has атрибут атрибут ... атрибут
;
Описание начинается с ключевого слова Object и заканчивается точкой с запятой, а между ними идёт три основных блока:
-
Сразу за словом
Objectидёт заголовочная часть; -
со слова
withначинается перечисление свойств; -
со слова
hasначинается перечисление атрибутов.
Заголовок объекта
Заголовок состоит из трёх частей, каждая из которых не обязательна:
-
Внутренний идентификатор, по которому другие объекты обращаются к данному объекту. Это должно быть одно слово (можно с цифрами и знаком подчёркивания), до 32 латинских символов, и оно должно быть уникальным в игре. Идентификатор можно опустить, если к объекту не обращаются другие объекты.
Примеры:
bird,tree,top_of_tree. -
Игровое имя в двойных кавычках. Оно может состоять из нескольких слов и не обязательно быть уникальным (например, можно иметь несколько комнат с именем
"Где-то в пустыне"). Не обязательно, но крайне рекомендуется дать каждому объекту игровое имя.Примеры:
"птенчик/","высок/ий платан/","На верхушке дерева". -
Внутренний идентификатор другого объекта, в котором будет находиться данный в начале игры (такой объект называется «родительским»). Это значение не указывается, если у объекта не будет родительского объекта, а также никогда не указывается для комнат.
Например, птенчик описывается как:
Object bird "птенчик/" forestчто означает что в начале игры он будет находиться в лесной чаще (игрок затем подберёт его с собой). Платан описан следующим образом:
Object tree "высок/ий платан/" clearingОн будет находиться на полянке, но так как он является
scenery, то игрок не сможет его переместить.Есть другой способ описания изначального положения объекта, при помощи стрелочек, например так:
Object -> bird "птенчик/" ...Мы не будем использовать его в примерах, но тем не менее это довольно удобный и наглядный способ.
Свойства объектов
Свойства начинаются с ключевого слова with. У объекта может быть любое количество свойств и заданы они могут быть в любом порядке. Сначала идёт имя свойства, затем через пробел его значение, после значения — символ запятой.
Свойства стоит воспринимать как переменные, относящиеся к объекту. Изначально значение равно заданному, но в ходе игры его можно изменять (тем не менее обычно свойства не изменяют). Вот несколько ранее виденных примеров свойств.
description "Гнездо сплетено из прутиков и аккуратно устлано мхом.",
e_to forest,
name 'детеныш' 'птиц' 'птичк' 'птенчик' 'птенц' 'маленьк',
each_turn [; if (nest in branch) deadflag = 2; ],
В перечисленных примерах встречаются различные варианты значений свойств: description это строка, со свойством e_to ассоциирован объект, в свойстве name идёт список словарных слов, а each_turn содержит в себе локальную функцию. Также значение может быть числовым, например:
capacity 10,
Существует около 50 стандартных свойств наподобие name или each_turn. Позже будут рассмотрены самые важные из них, а также то, как задать собственное свойство.
Атрибуты объектов
Атрибуты начинаются с ключевого слова has. Их может быть любое количество и в любом порядке; друг от друга они отделяются пробелом.
Атрибуты проще свойств — у них нет значения, они могут либо присутствовать, либо отсутствовать (быть включены/выключены). Атрибут можно назвать флагом. Изначально если атрибут указан, то он включён (присутствует), если не указан — то выключен (отсутствует).
Ранее мы встретились со следующими атрибутами:
container light open scenery static supporter
Каждый из них отвечает на вопрос, например, «Является ли объект контейнером?», «Является ли он источником света?» и так далее. Если атрибут указан, то ответом будет «да», если не указан — «нет».
Существует около 30 стандартных атрибутов. Можно также создавать и свои собственные.
Связи между объектами и дерево объектов
Во время игры Информ следит за связями между объектами — то есть помнит, где находится конкретный объект относительно других объектов. При рассмотрении связей по отношению к объектам используются термины «родительский» и «дочерний».
Когда игрок находится в какой-нибудь комнате, например в лесной чаще, то можно сказать следующее:
-
объект лесной чащи является родительским для объекта игрока, или что
-
объект игрока является дочерним для объекта лесной чащи.
Также если игрок держит в руках объект, например гнездо, то
-
объект игрока — родительский для объекта гнезда, или
-
объект гнезда — дочерний для объекта игрока.
У объекта может быть только один «родитель» (или не быть родительских объектов вообще), но может быть сколько угодно «детей» (в том их может и не быть).
Например, рассмотрим следующие объекты:
Object nest "птичь/е гнезд/о" clearing
...
Object tree "высок/ий платан/" clearing
Здесь для гнезда родителем является объект clearing, и также для платана тоже родителем является clearing. То есть и гнездо, и платан являются детьми локации Полянка.
У комнат не бывает родительских объектов, а также одним из их дочерним объектом иногда становится игрок.
Птенчика в лесной чаще мы описали следующим образом:
Object bird "птенчик/" forest
...
В лесной чаще больше ничего нет, поэтому чаща является родителем объекта птенчик, и у чащи есть единственный дочерний объект, птенчик. Когда игрок, который изначально находится в before_cottage, переходит на ВОСТОК в чащу, то происходит следующее: родителем игрока становится forest, а у forest становится два дочерних объекта — птенчик и игрок. В такой манере Информ следит за перемещением объектов и изменением связей.
Далее, пусть игрок подбирает птенца. Происходит изменение связей: птенец теперь — дочерний объект для игрока (уже не для леса), а игрок становится и родительским (для птенца), и дочерним (для леса) объектом.
Ниже изображена схема изменений связей в ходе игры. Связи изображены линиями, дочерние объекты находятся под родительскими.
| 1. В начале игры: | |
| 2. Игрок вводит ИДТИ НА ВОСТОК | |
| 3. Игрок вводит ВЗЯТЬ ПТЕНЦА | |
| 4. Игрок вводит ИДТИ НА СЕВЕРОВОСТОК | |
| 5. Игрок вводит ПОЛОЖИТЬ ПТЕНЦА В ГНЕЗДО | |
| 6. Игрок вводит ВЗЯТЬ ГНЕЗДО | |
| 7. Игрок вводит ИДТИ ВВЕРХ | |
| 8. Игрок вводит ПОЛОЖИТЬ ГНЕЗДО НА СУК | |
Такую схему также называют деревом объектов. Её не нужно держать в голове, потому что библиотека выполняет всю работу за нас, и потому что объектов может быть слишком много, чтобы за всеми ними следить. Главное понять общий принцип.
Позже будут рассмотрены команды parent, child и children, при помощи которых можно получить для конкретного объекта его родителя, дочерние объекты и их количество.
Двойные и одинарные кавычки
Двойные кавычки
В двойные кавычки заключаются строки — это может быть символ, слово, абзац и вообще текст практически любой длины.
Некоторые примеры специальных символов:
-
Для записи двойных кавычек в строке используется тильда:
~ -
Для переноса строки используется символ
^
Длинные строки можно разбить на несколько строк с переносами, Информ просто склеит их, отбросив лишние пробелы (пробелы между словами остаются нетронутыми). Следующие две строки одинаковы для Информа:
"Это строка из разных символов."
"Это
строка
из разных
символов."
При выводе длинного пассажа текста интерпретатор делает автоматический перенос с края экрана. Для собственного переноса используется символ ^.
В игре мы использовали строковую константу:
Constant Headline
"^Пример простой игры на Inform.
^Авторы: Роджер Фирт (Roger Firth) и Соня Кессерих (Sonja Kesserich).
^Перевод Юрия Салтыкова a.k.a. G.A. Garinson^";
которую можно было бы с тем же успехом записать как
Constant Headline
"^Пример простой игры на Inform.^Авторы: Роджер Фирт (Roger Firth) и Соня Кессерих (Sonja Kesserich).^Перевод Юрия Салтыкова a.k.a. G.A. Garinson^";
Строки используются, например, в свойстве description:
with description "Слишком мал, чтобы летать, птенец беспомощно попискивает.",
Также строки применяются в командах print, что можно будет увидеть позже.
Одинарные кавычки
В одинарные кавычки заключаются словарные слова. Это должно быть единственное слово, без пробелов (можно с цифрами и дефисом). Регистр символов не учитывается. Кроме того, значащими являются только первые девять символов.
Когда игрок вводит команду, интерпретатор разбивает ввод на отдельные слова и затем ищет их в словаре. Если эти слова образуют некую верную команду, то он пытается её выполнить.
Пример из нашей игры:
name 'птичь' 'гнезд' 'гнездышк' 'пруть' 'прутик' 'мох',
Функции и инструкции
Функция представляет из себя набор инструкций, которые выполняются интерпретатором. Есть два вида функций и более 20 видов инструкций.
Инструкции
Инструкция представляет из себя команду для интерпретатора. В готовой игре используется множество инструкций, но нам они пока редко встречались, например:
location = before_cottage;
что называется присваиванием. Присваивание задаёт новое значение для переменной, в данном случае глобальной библиотечной переменной location. Далее,
if (nest in branch) deadflag = 2;
содержит сразу две инструкции, присваивание, перед которым идёт инструкция if:
if (nest in branch) ...
Инструкция if проверяет выполнение какого-либо условия. Если условие истинно, то интерпретатор выполняет инструкцию, которая следует далее. Если условие ложно, то следующая инструкция пропускается. В данном случае проверяется условие, находится ли nest на или в объекте branch (то есть является ли дочерним). Практически всегда во время игры это будет ложно, поэтому следующая инструкция игнорируется. Когда же условие выполнится, то интерпретатор выполнит присваивание:
deadflag = 2;
что изменит deadflag на 2. Обычно подчинённые инструкции записываются под if, с отступом, потому что так их проще читать:
if (nest in branch)
deadflag = 2;
Глобальные функции
Глобальная функция представляет собой серию инструкций, у которой есть своё имя. При вызове функции выполняются эти инструкции. Вот одна из глобальных функций:
[ Initialise; location = before_cottage; ];
Поскольку размер нашей функции мал, то мы записали её в одну строчку. Её можно отформатировать иначе:
[ Initialise;
location = before_cottage;
];
Часть [ Initialise; обозначает начало функции и включает её имя, по которому её можно вызвать. ]; — это конец функции. Между ними идёт тело функции, в котором содержатся инструкции. Вызвать функцию очень просто:
Initialise();
При этом выполнятся все инструкции из тела функции, и интерпретатор продолжит свою работу.
Заметьте, что мы описали функцию
Initialise, но в игре её вызывали. На самом деле, функция вызывается при старте игры, самой библиотекой Информа.
Локальные функции
Локальные функции похожи на глобальные, но у них нет имени, и они не заканчиваются точкой с запятой. Пример из нашей игры:
[; if (nest in branch) deadflag = 2; ]
Точнее, мы записали эту функцию как значение свойства:
each_turn [; if (nest in branch) deadflag = 2; ],
Его можно переписать следующим образом:
each_turn [;
if (nest in branch)
deadflag = 2;
],
Любые локальные функции задаются таким образом — как значение свойства объекта. Они привязаны к объекту и находятся в нём.
Локальные функции не вызываются по имени как глобальные функции. Имени у них нет, эти функции вызываются библиотекой автоматически в нужный момент игры. Момент определяется ролью, которую играет свойство. В нашем случае этим моментом будет конец каждого хода, когда игрок находится в той же комнате, где сук. Позже будет показаны другие примеры локальных функций а также способ вручную вызвать локальную функцию, при помощи синтаксиса идентификатор.свойство(), в нашем случае это было бы branch.each_turn().
Пробегитесь по имеющемуся исходному коду и убедитесь, что вам всё понятно. В следующей главе мы будем исправлять недостатки нашей первой игры.
И снова Хейди
Даже в простой игре игрок может попробовать сделать то, о чём заранее не подумал автор. Часто разные действия игрока должны позволить один и тот же результат. Поэтому автору стоит попробовать ввести в игру все разумные варианты, которые могут прийти игроку в голову. Зачастую описания предметов или локаций прямо таки подсказывают, какие здесь есть объекты, и что можно потенциально сделать. Это тоже обязательно нужно учесть. Сделать игру довольно просто, но основное время уйдёт на продумывание различных второстепенных вариантов.
В этой главе мы рассмотрим некоторые из таких вариантов.
Послушать птенчика
Рассмотрим пример прямо из игры:
В лесной чаще
На западе, сквозь густую листву, можно разглядеть небольшое
строение. Тропинка ведет на северо-восток.
Здесь имеется птенчик.
> ОСМОТРЕТЬ ПТЕНЧИКА
Слишком мал, чтобы летать, птенец беспомощно попискивает.
> ПОСЛУШАТЬ ПТЕНЧИКА
Никаких необычных звуков нет.
>
Видна недоработка. Игра сообщает нам, что птенец беспомощно попискивает, но тут же говорит нам, что никаких необычных звуков нет.
В библиотеке есть обширный набор стандартных сообщений-ответов на стандартные действия. «Никаких необычных звуков нет» является стандартным ответом на команду ПОСЛУШАТЬ. Он подходит для ПОСЛУШАТЬ ГНЕЗДО или ПОСЛУШАТЬ ДЕРЕВО, но в данном случае, с птенцом, он неуместен. Нужно добавить собственную реакцию:
Object bird "птенчик/" forest
with description "Слишком мал, чтобы летать, птенец беспомощно попискивает.",
name 'детеныш' 'птиц' 'птичк' 'птенчик' 'птенц' 'маленьк',
before [;
Listen:
print "Жалобный писк испуганной птички разрывает тебе сердце.
Надо помочь!^";
return true;
],
has male;
Рассмотрим эту часть кода по шагам:
-
Для объекта
birdмы ввели новое свойство,before. Интерпретатор обращается к свойствуbeforeперед тем, как выполнить конкретное действие с объектом:before [; ... ], -
Значением свойства является локальная функция, в которой есть метка и две инструкции:
Listen: print "Жалобный писк испуганной птички разрывает тебе сердце. Надо помочь!^"; return true; -
Метка обозначает тип действия, в данном случае
Listen(«послушать»). Меткой мы сообщаем интерпретатору следующее: если действие, которое будет совершено над птенчиком это «послушать», то надо выполнить эти инструкции. В противном случае продолжать как обычно. То есть, если игрок введёт ОСМОТРЕТЬ ПТЕНЦА, ВЗЯТЬ ПТЕНЦА, ПОЛОЖИТЬ ПТЕНЦА В ГНЕЗДО, УДАРИТЬ ПТЕНЦА или ПОГЛАДИТЬ ПТЕНЦА, то он получит стандартный ответ. Если же игрок введёт ПОСЛУШАТЬ ПТЕНЦА, то действие будет «перехвачено», и выполнятся наши инструкции. -
Выполнятся следующие инструкции:
print "Жалобный писк испуганной птички разрывает тебе сердце. Надо помочь!^";что выведет на экран указанную строку (^ выполнит перенос на следующую строку). Далее,
return true;что скажет интерпретатору, что ему больше не нужно ничего делать, потому что мы самостоятельно обработали действие
Listen. Теперь в игре мы получим то, что хотели:> ПОСЛУШАТЬ ПТЕНЧИКА Жалобный писк испуганной птички разрывает тебе сердце. Надо помочь! >
Стоит остановиться подробнее на инструкции return true. Свойство before перехватывает действие над объектом прежде, чем интерпретатор начнёт с ним что-то делать. В этот момент выполняются инструкции из локальной функции. Если последней инструкцией идёт return true, то значит что действие обработано и интерпретатору больше ничего не нужно делать, никаких действий или сообщений. Однако если в конце функции стоит return false, тогда интерпретатор продолжит выполнять действие так, будто оно не было перехвачено. Иногда это полезно, но не в нашем случае. Если записать эту секцию следующим образом:
Object bird "птенчик/" forest
with description "Слишком мал, чтобы летать, птенец беспомощно попискивает.",
name 'детеныш' 'птиц' 'птичк' 'птенчик' 'птенц' 'маленьк',
before [;
Listen:
print "Жалобный писк испуганной птички разрывает тебе сердце.
Надо помочь!^";
return false;
],
has male;
то интерпретатор выведет сначала нашу строку, а затем стандартный ответ:
> ПОСЛУШАТЬ ПТЕНЧИКА
Жалобный писк испуганной птички разрывает тебе сердце. Надо
помочь!
Никаких необычных звуков нет.
>
При написании игр на Информе перехват действий указанным образом используется очень часто.
Вход в домик
В начале игры главная героиня стоит перед избушкой, что подразумевает, что в неё можно зайти.
Перед домом
Ты стоишь около избушки, на восток от которой раскинулся лес.
> ИДТИ ВНУТРЬ
Этот путь недоступен.
>
Опять видим не самый лучший ответ. Но это легко исправить:
Object before_cottage "Перед домом"
with description
"Ты стоишь около избушки, на восток от которой раскинулся лес.",
e_to forest,
in_to "Такой славный денек... Он слишком хорош, чтобы прятаться внутри.",
cant_go "Единственный путь ведет на восток.",
has light;
Обычно свойство in_to вело бы в другую комнату, как, например e_to, но если указать строку, то интерпретатор выведет эту строку, когда игрок попытается пойти ВНУТРЬ. Если пойти в другом неуказанном направлении, например НАВЕРХ или ИДТИ НА СЕВЕР, то игрок всё равно получит ответ «Этот путь недоступен», но это также легко изменить, добавив свойство cant_go с соответствующей строкой. Получим более дружелюбное поведение игры:
Перед домом
Ты стоишь около избушки, на восток от которой раскинулся лес.
> ВНУТРЬ
Такой славный денек... Он слишком хорош, чтобы прятаться внутри.
> СЕВЕР
Единственный путь ведет на восток.
> ВОСТОК
В лесной чаще
...
Здесь есть и другая проблема — мы не реализовали саму избушку, поэтому ОСМОТРЕТЬ ИЗБУШКУ выдаст игроку «Этого предмета здесь нет». Добавим объект cottage и сделаем его с атрибутом scenery, аналогично дереву:
Object cottage "маленьк/ий домик/" before_cottage
with description "Домик мал и неказист, но ты очень счастлива, живя здесь.",
name 'маленьк' 'дом' 'изб' 'терем' 'коттедж' 'хат' 'небольш' 'строен'
'домик' 'избушк' 'теремок' 'хатк',
has scenery male;
Это решает проблему, но приводит к ещё одному неподходящему ответу:
Перед домом
Ты стоишь около избушки, на восток от которой раскинулся лес.
> ВОЙТИ В ДОМИК
Но на/в маленький домик невозможно войти, встать, сесть или лечь.
Это решается аналогично тому, как мы сделали ПОСЛУШАТЬ ПТЕНЧИКА:
Object cottage "маленьк/ий домик/" before_cottage
with description "Домик мал и неказист, но ты очень счастлива, живя здесь.",
name 'маленьк' 'дом' 'изб' 'терем' 'коттедж' 'хат' 'небольш' 'строен'
'домик' 'избушк' 'теремок' 'хатк',
before [;
Enter:
print_ret "Такой славный денек...
Он слишком хорош, чтобы прятаться внутри.";
],
has scenery male;
При помощи свойства before мы перехватываем действие Enter (ВОЙТИ), которое применяется к объекту cottage. Правда, в этот раз мы воспользовались только одной инструкцией, а не двумя. Просто необходимость «вывести строку, сделать перевод строки, и затем return true» встречается так часто, что для этого есть отдельная инструкция, print_ret. То есть:
print_ret "Такой славный денек...
Он слишком хорош, чтобы прятаться внутри.";
идентично
print "Такой славный денек...
Он слишком хорош, чтобы прятаться внутри.^";
return true;
Заметьте, что в print_ret нам не понадобился символ переноса ^.
Залезть на дерево
На полянке предполагается, что игрок введёт ВВЕРХ. Но игрок с большой вероятностью попробует ЗАЛЕЗТЬ НА ДЕРЕВО, но получит в ответ лишь «Забираться на высокий платан бессмысленно». Вновь воспользуемся свойством before, но чуть по-другому.
Object tree "высок/ий платан/" clearing
with description
"Величавое дерево стоит посреди поляны.
Кажется, по его стволу будет несложно влезть наверх.",
name 'высок' 'платан' 'дерев' 'ствол' 'величав',
before [;
Climb:
PlayerTo(top_of_tree);
return true;
],
has scenery male;
Здесь мы перехватываем действие Climb (ЗАЛЕЗТЬ), применяемое к объекту tree, но не для того, чтобы вывести своё сообщение, а для того, чтобы переместить игрока в другую комнату, так как если бы он ввёл ВВЕРХ. Перемещать игрока вручную довольно сложно, но, к счастью, в библиотеке есть стандартная функция, которая делает всё за нас.
Функция называется PlayerTo, и её нужно вызвать с параметром — идентификатором комнаты, куда мы хотим переместить игрока. При вызове параметр указывается внутри скобок: PlayerTo(top_of_tree). Ранее мы встретились с функцией Initialise, эта функция не принимает никаких параметров, поэтому мы сказали, что её можно было бы вызвать как Initialise().
Мы переместили игрока, но всё ещё находимся в перехватчике действия Climb. И поскольку мы уже обработали действие самостоятельно, нам не нужен стандартный ответ, и мы выполняем return true.
Бросить предмет с дерева
В любой комнате если игрок введёт ПОЛОЖИТЬ (БРОСИТЬ) предмет, который он несёт с собой, то он упадёт в той же локации рядом на землю. Это поведение работает убедительно везде, кроме верхушки дерева — там предмет не должен падать рядом, а должен падать вниз на опушку.
Нам нужно перехватить действие Drop, но несколько иначе, чем мы делали раньше. Во-первых, действие должно срабатывать не для конкретных bird или nest, но в общем, то есть для любых предметов. И во-вторых, нужно учесть, что не все предметы можно бросить: например, нельзя БРОСИТЬ СУК.
Для решения второго пункта нужно перехватить действие Drop не до, а после того, как оно произошло. Так мы даём библиотеке разобраться с объектами, которые вообще нельзя бросить, или которых нет в руках у игрока, и вступаем только тогда, когда предмет уже был брошен. А для решения первого пункта мы будем перехватывать Drop не для наших объектов, а прямо на локации, где это происходит, то есть на верхушке дерева, top_of_tree:
Object top_of_tree "На верхушке дерева"
with description "На этой высоте цепляться за ствол уже не так удобно.",
d_to clearing,
after [;
Drop:
move noun to clearing;
return false;
],
has light;
Рассмотрим этот код по шагам:
-
Для нашей комнаты мы добавили свойство
after. Интерпретатор обращается к этому свойству после того, как выполнит любое действие в этой комнате:after [; ... ], -
Значение свойства является локальной функцией, содержащей метку и две инструкции:
Drop: move noun to clearing; return false; -
Метка обозначает имя действия, в данном случае
Drop. Мы сообщаем интерпретатору следующее: если только что было совершено действиеDrop, то выполни эти инструкции перед тем, как сообщить игроку, что действие завершено. Если произошло другое действие, то продолжи как обычно. -
Сначала выполняется инструкция
move noun to clearing;которая берёт объект, который был перенесён из объекта игрока,
player, в объектtop_of_tree(так как выполнилось действиеDrop), и переносит его ещё раз, в объектclearing. В инструкцииnounявляется библиотечной переменной, в которой будет храниться идентификатор объекта, к которому применяется действие. То есть если игрок вводит БРОСИТЬ ГНЕЗДО, тоnounукажет наnest, а если БРОСИТЬ ПТЕНЦА, тоnounстанетbird. Далее мы исполняемreturn false;что говорит интерпретатору, что теперь он может вывести игроку свой ответ, то есть, что произошло.
Вот что мы получим в игре:
На верхушке дерева На этой высоте цепляться за ствол уже не так удобно. Здесь имеется надежный толстый сук. > БРОСИТЬ ГНЕЗДО Птичье гнездо положено. > ОСМОТРЕТЬСЯ На верхушке дерева На этой высоте цепляться за ствол уже не так удобно. Здесь имеется надежный толстый сук. > ВНИЗ Полянка Здесь имеется птичье гнездо (где имеется птенчик). >
Конечно, сообщение «Птичье гнездо положено». совсем не информативно в данном случае, поэтому можно сделать так:
Object top_of_tree "На верхушке дерева"
with description "На этой высоте цепляться за ствол уже не так удобно.",
d_to clearing,
after [;
Drop:
move noun to clearing;
print_ret "Предмет упал вниз на землю.";
],
has light;
Здесь print_ret выводит более корректное сообщение и возвращает true, что означает что интерпретатор больше не должен ничего выводить на экран.
Птица в гнезде
Игра заканчивается, когда игрок кладёт гнездо на сук. Мы предположили, что птенец уже находится в гнезде, но это может быть не так. Необходимо также проверить, лежит ли птенец в гнезде. Это легко сделать:
Object branch "надежн/ый толст/ый сук/" top_of_tree
with description "Сук достаточно ровный и крепкий, чтобы на нем надежно
держалось что-то не очень большое.",
name 'надежн' 'ровн' 'толст' 'крепк' 'сук' 'ветк',
each_turn [; if (bird in nest && nest in branch) deadflag = 2; ],
has static supporter male;
Мы расширили инструкцию if:
if (bird in nest && nest in branch) deadflag = 2;
Её можно прочитать так: «Если bird находится на/в гнезде, и nest находится на/в branch, то установить deadflag равным 2. В противном случае ничего не делать».
Заключение
К этому моменту вы уже поняли, что в игре обязательно надо реализовывать не только свои действия, но и те необязательные, которые могут прийти в голову игроку. Игрок может попробовать вообще что-то глупое, но ему будет интересно получить нестандартный ответ. Непроработка ответов может вообще разрушить эффект погружения.
В этой главе мы рассмотрели:
Свойства объектов
У объектов может быть свойство before, и если оно есть, то интерпретатор обращается к нему перед выполнением действия над объектом. Аналогично есть свойство after, куда происходит обращение уже после выполненного действия, но ещё до того, как выведется сообщение игроку. И before, и after могут быть как у объектов-предметов (где перехватываются действия применимые к этим объектам), так и у комнат (где перехватываются действия, которые происходят с объектами в комнате).
Значениями этих двух свойств являются локальные функции. Если функция заканчивается return false, то интерпретатор продолжит выполнение, а если return true — то интерпретатор больше ничего делать не будет. Так можно либо частично изменить выполнение действия, либо заменить его полностью.
Ранее свойства соединений комнат указывали на объект комнаты, куда произойдёт перемещение. В этой главе также было показано, что это может быть строка (в которой указана причина, почему перемещение невозможно). Также было рассмотрено свойство cant_go, которое учитывает все неуказанные направления:
e_to forest,
in_to "Такой славный денек... Он слишком хорош, чтобы прятаться внутри.",
cant_go "Единственный путь ведет на восток.",
Функции и параметры
В библиотеке есть много удобных функций, и мы воспользовались функцией PlayerTo, которая позволяет перенести игрока в другую комнату, не обязательно соседнюю.
При вызове этой функции нам нужно было указать конечную комнату, и мы сделали это в скобках:
PlayerTo(clearing);
Значения в скобках называются параметрами функции. Их может быть несколько, в таком случае они разделяются запятыми. Например можно перенести игрока без вывода описания комнаты, указав второй аргумент:
PlayerTo(clearing, 1);
Здесь 1 подавляет вывод описания новой комнаты.
Инструкции
Нам встретились новые инструкции:
return true;
return false;
Они использовались для управления интерпретатором в локальных функциях.
print "строка";
print_ret "строка";
Инструкция print выводит указанную строку, а print_ret выводит строку, делает перенос и выполняет return true.
if (условие && условие) ...
Мы расширили условие в инструкции if. && — оператор «И», который используется для проверки выполнения нескольких условий сразу. Также существуют оператор || «ИЛИ» и оператор ~~ «НЕ».
move объект to родитель;
Инструкция move изменяет дерево объектов, устанавливая для объекта нового родителя.
Действия
Мы говорили о перехвате таких действий как Listen, Enter, Climb и Drop. Действие это представление того, что должно быть сделано, в зависимости от глагола, который введёт игрок. Например, ПОСЛУШАТЬ и ПРИСЛУШАТЬСЯ суть одно и то же, поэтому им соответствует действие Listen. Аналогично ВОЙТИ, ЗАЙТИ, СЕСТЬ НА, ЛЕЧЬ НА приводят к действию Enter, а ПОЛОЖИТЬ, ВЫБРОСИТЬ означают Drop. Так облегчается работа писателя-программиста, потому что различных глаголов может быть множество, но количество разных действий гораздо меньше.
В библиотеке каждому действию соответствует номер, и этот номер хранится в переменной action. Также есть переменная noun, в которой хранится идентификатор объекта, над которым производится действие, а также second для второго такого объекта (если он есть).
Вот несколько примеров:
| Ввод игрока | действие | noun | second |
|---|---|---|---|
| СЛУШАТЬ | Listen | nothing | nothing |
| СЛУШАТЬ ПТЕНЧИКА | Listen | bird | nothing |
| ПОДНЯТЬ ПТЕНЧИКА | Take | bird | nothing |
| ПОЛОЖИТЬ ПТЕНЧИКА В ГНЕЗДО | Insert | bird | nest |
| БРОСИТЬ ГНЕЗДО | Drop | nest | nothing |
| ПОЛОЖИТЬ ГНЕЗДО НА СУК | PutOn | nest | branch |
nothing — это встроенная константа, обозначающая отсутствие объекта.
В этих главах мы изучили основные принципы, на которых строятся любые игры на Информе, а также средства для создания более интересных реакций.
Пример игры: Вильгельм Телль
В данной главе сейчас нет материалов, но в будущем будет подробно разобрана разработка второй обучающей игры, под названием «Вильгельм Телль».
А пока в «Вильгельма Телля» можно поиграть онлайн: https://iplayif.com/?story=https://rinform.org/demos/WTellR.z5
И исходный код почитать здесь: https://github.com/yandexx/rinform-glulx/blob/master/demos/WTellR.inf
FAQ
В данной главе покрыты:
- общие вопросы об Информе;
- часто возникающие вопросы о том, как работает русская версия.
Общие вопросы
В чём разница между Z-machine и Glulx?
Это два формата файлов, в которые можно компилировать игры на Информе. Более старая, классическая Z-machine поддерживает размер файла игры до 256 Кб (формат .z5) или 512 Кб (формат .z8) и 16 цветов. Glulx — более современная 32-битная система, поддерживающая файлы размером до 4 Гб и расширенные мультимедийные возможности. Компилятор inform умеет компилировать игры в любом из форматов.
В английской версии Информа библиотека единая, но в русской версии пришлось разнести её на два отдельных проекта.
Начиная с версии 0.9 Русского Информа Glulx считается стабильной и рекомендуемой версией. Разработку стоит вести под неё. Версия для Z-машины считается вторичной.
Glulx имеет следующие преимущества:
- мультимедиа-фичи: картинки, звук, ссылки.
- расширенные возможности типографики.
- возможность разделять окно на произвольные области.
- полная поддержка UTF-8 как в исходниках, так и в готовых играх.
- файлы игр до 4 гигабайт.
Известное ограничение: в онлайн-версии Glulx (Quixe) нельзя в коде игры задавать цвета или размер шрифтов, кроме глобальных настроек через .css.
Какие плееры открывают игры на Информе?
- Glulx (файлы .ulx и .gblorb): Windows Git, Windows Glulxe, Lectrote, а также онлайн.
- Z-machine (файлы .z5, .z8 и .zblorb): Windows Frotz, Lectrote, fizmo, а также онлайн через Parchment.
- На Android плеер Fabularium поддерживает все существующие форматы.
Как опубликовать игру онлайн?
Чтобы запустить произвольную игру в Parchment, нужно указать параметром для сайта iplayif.com путь к файлу игры, уже залитому на какой-нибудь сервер. Например: https://iplayif.com/?story=https://rinform.org/games/photopia/PhotopiaR.z8
Очень просто и разместить игру на своём собственном сайте, даже статическом, т.к. Parchment работает полностью на клиентском JavaScript. Достаточно скачать и разместить у себя на сервере файлы:
- index.html,
- lib/parchment.min.js,
- lib/parchment.min.css,
- lib/jquery.min.js
- lib/zvm.min.js
и отредактировать index.html. В .css файле можно поменять шрифты, цвета и прочее, на что хватит вашей фантазии. Несколько примеров: Винтер, Delightful Wallpaper, Dreamhold.
Особенности русской версии
Для компиляции в Glulx исходники должны быть в UTF-8. Пример командной строки для компиляции игры:
inform.exe +library +language_name=Russian -G -Cu $DICT_CHAR_SIZE=4 game.inf game.ulx
Для компиляции в Z-машину исходные файлы игры должны быть в кодировке Windows 1251. Пример командной строки для компиляции игры, где указаны все обязательные параметры:
inform.exe +library +charset_map=library\cyrwin.cm +language_name=Russian -v5 game.inf game.z5
Как описывать объекты
Нижеперечисленное относится только к объектам. На комнаты (локации) эти правила не распространяются.
-
В имени объекта нужно отделить окончание (или окончания) прямым слешем (/), а точнее — те буквы, которые должны будут изменяться при склонении. Например: латунн/ая ламп/а, надпис/ь на стене, бел/ый туман/. Имя используется при выводе объектов во всевозможных ситуациях, в разных падежах.
-
В свойстве
nameуказываются слова, по которым парсер находит объекты. В русской версии нужно перечислять такие слова без окончаний. Здесь нужно не забыть указать и синонимы, по которым объект тоже должен находиться парсером. -
Каждому объекту (кроме комнат) обязательно нужно выдать один из атрибутов, соответствующий роду или числу объекта. Один из четырёх: male — мужской род, female — женский род, neuter — средний род, pluralname — множественное число.
Примеры:
Object -> "друг/ой склон/ холма" with name 'склон' 'сторон' 'холм', description "Кто мешает изучить его самому?", has scenery male;Object -> "груб/ые каменн/ые ступен/и" with name 'груб' 'каменн' 'ступен', description "Грубые каменные ступени ведут по куполу вверх.", has scenery pluralname; -
Особый случай — это существительные с беглыми гласными: ковёр (ковром), перекрёсток (перекрёстком) и так далее. Их нужно окружить двумя слешами, а к объекту добавить свойство
casegen, по примеру ниже. Четвёртым параметром в функции ICVowel должна идти беглая гласная. Есть более сложные случаи, где новые буквы появляются (пятый параметр), см. пример ниже про «ручей».Object -> "туманн/ый колод/е/ц/" with name 'колодец' 'колодц', casegen [ beg end csID; return ICVowel (csID, beg, end, 'е', 0); ], description "Из колодца поднимаются бесформенные клубы белого тумана.", has scenery male;Treasure -> large_gold_nugget "огромн/ый золот/ой самород/о/к/" with name 'золот' 'огромн' 'слиток' 'слитк' 'самородок' 'самородк' 'кусок' 'куск', description "Массивный кусок самородного золота!", casegen [ beg end csID; return ICVowel (csID, beg, end, 'о', 0); ], initial "На полу поблескивает большой золотой самородок!", has male;Object Stream "бурн/ый руч/е//й" with name 'руче' 'ручь' 'речк' 'поток' 'вод', description "Холодный бурный ручей струится вниз по каменистому руслу.", casegen [ beg end csID; return ICVowel (csID, beg, end, 'е', 'ь'); ], has scenery male; -
Есть случаи, когда нужно, чтобы у мужского объекта была женская логика для склонения имени. Например, мужское имя «Слава». В таком случае необходимо дополнительно выдать объекту атрибут fem_grammar.
-
Если возникает случай, где парсер не в состоянии самостоятельно просклонять имя объекта (на вывод), необходимо указать свойство-функцию
casegenи через неё явно указать все нужные склонения. Кроме того, необходимо изменить имя объекта. Например:Object Key "/неважно_что_здесь_будет_написано_главное_без_пробелов" with name 'ключ', description "Твой любимый ключ на тридцать два.", casegen [ beg end csID; switch (csID) { csNom: print "ключ"; rtrue; csGen: print "ключа"; rtrue; csDat: print "ключу"; rtrue; csAcc: print "ключ"; rtrue; csIns: print "ключом"; rtrue; csPre: print "ключе"; rtrue; } ], has male; -
Если парсер не распознаёт объект автоматически (на ввод), то следует пользоваться свойством-функцией
parse_name. Подробное описание находится в DM4 (pdf) на странице 209. В простом примере ниже имя персонажа «Моро» не склоняется.parse_name [n; while (NextWord() == 'моро') n++; return n; ],
Как проверить, что объект верно склоняется по падежам?
Для этого есть удобная команда мета! форм. Чтобы она работала, убедитесь, что игра скомпилирована в режиме Debug (-D).
>мета! форм самородок
Объект «огромн/ый золот/ой самород/о/к/» (Ед.ч./М.р.):
И.п.: Огромный золотой самородок
Р.п.: Огромного золотого самородка
Д.п.: Огромному золотому самородку
В.п.: Огромный золотой самородок
Т.п.: Огромным золотым самородком
П.п.: Огромном золотом самородке
>
Как вывести объект в каком-либо падеже
Для этого есть следующие функции. Вторая функция нужна для вывода с большой буквы (обычно, в начале предложений).
cNomиССNom— именительный (Nominative).cAccиССAcc— винительный (Accusative).cGenиССGen— родительный (Genitive).cDatиССDat— дательный (Dative).cInsиССIns— творительный (Instrumental).cPreиССPre— предложный (Prepositive).
"Вы тщательно установили неуклюжую монстроподобную фотокамеру,
направили свет лампы на цель и терпеливо дождались,
пока экспонирование ", (cGen) noun, " не завершится.";
print_ret "Вам нет нужды беспокоиться о ", (cPre) self, ".";
[ SaluteSub;
if (noun has animate)
print_ret (CCNom) noun, " приветствует тебя.";
print_ret (CCNom) noun, " не замечает этого.";
];
Вспомогательные функции
Все эти функции принимают объект, и вызывать их в коде нужно как (function) noun.
PronounSвыводит местоимение, подходящее объекту («ты», «он», «она», «оно», «они»).Pronounработает какPronounS, но выводит с заглавной буквы.SAEndвыводит окончание краткой формы прилагательных или причастий («открыта», «открыто», «открыты»).V1aEndвыводит окончания глаголов -ет или -ут.V1bEndвыводит окончания глаголов -ет или -ют.V2aEndвыводит окончания глаголов -ит или -ат.V2bEndвыводит окончания глаголов -ит или -ят.VPEndвыводит окончание для глаголов в прошедшем времени («пропал», «пропала», «пропало», «пропали»).AEnd: окончания прилагательных -ый, -ая, -ое, -ые.AEnd2: окончания прилагательных -ий, -ая, -ое, -ие.AEnd3: окончания прилагательных -ой, -ая, -ое, -ые.PEnding1: -им, -ой, -ими.PEnding2: -ым, -ой, -ыми.GenIt: его, её, их.GenIt2: него, неё, них.DatIt: ему, ей, им.DatIt2: нему, ней, ним.InsIt: им, ей, имиInsIt2: ним, ней, ними.
Как описывать русские глаголы
-
Глаголы нужно перечислять по корневой части. Парсер распознает приставки («по», «за» и т.д.) и суффиксы-окончания («ся», «ять», «ать» и т.д.) автоматически.
-
Нужно учесть и то, что глаголы могут вводиться и в повелительном наклонении: например, не только «взять» (вз), но и «возьми» (возьм). Это необходимо для приказов, которые игрок может отдавать NPC: «гоблин, отдай мне ключ».
-
Для существительных, с которыми оперирует глагол, нужно указывать токены. Какой токен использовать, определяет падеж существительного в данном контексте. Например, «взять ключ» — винительный падеж (Accusative), «дотронуться до двери» — дверь в родительном падеже (Genitive).
Есть следующие токены:
cNom_noun— именительный (Nominative).cAcc_noun— винительный (Accusative).cGen_noun— родительный (Genitive).cDat_noun— дательный (Dative).cIns_noun— творительный (Instrumental).cPre_noun— предложный (Prepositive).
Кроме этого, можно использовать более строгие токены, соответствующие категориям объектов:
cIns_held,cAcc_held,cGen_held— для объектов, которые есть в инвентаре игрока (у которых есть атрибут held).cAcc_creat,cGen_creat,cDat_creat— для живых объектов (NPC с атрибутом animate).cAcc_multi— для группы объектов (например, команда «взять» позволяет брать более одного объекта).cAcc_multiheld,cAcc_multiexcept,cAcc_multiinside.
-
Также, не обязательно, но рекомендуется добавить объект с этими корневыми частями в VerbDepot, как указано в примере ниже. В нём нужно указать полную форму глагола, которая иногда выводится в игре.
! "вязать" Verb 'вяз' 'вяж' * cAcc_noun -> Tie * cAcc_noun 'к' cDat_noun -> Tie ! "привязать" * 'к' cDat_noun cAcc_noun -> Tie reverse * cAcc_noun 'с'/'со' cIns_noun -> Tie ! "связать" * 'с'/'со' cIns_noun cAcc_noun -> Tie reverse; Object "вязать" VerbDepot with name 'вяз' 'вяж';