О Русском Информе

На Информе можно писать игры с текстовым вводом. Классические примеры таких игр — 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.

Для проверки работоспособности и первоначальной настройки:

  1. скачайте архив библиотеки с официального сайта и распакуйте его в отдельную, удобно доступную папку, например c:\rinform\.

    В этой папке будет следующее содержимое:

    \demos\           демонстрационные игры
    \demos\demos.bat  пакетные файлы для компиляции демонстрационных игр
    \demos\demos.sh
    \libext\          расширения для библиотке
    \library\         русская версия библиотеки
    inform.exe        компилятор Inform (для Windows)
    
  2. В папке demos можно увидеть несколько файлов с расширением .inf — это исходные файлы демонстрационных игр. Их можно открыть и просмотреть в текстовом редакторе.

  3. Файл 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)
    

    Если прочих сообщений нет, то всё прошло без ошибок.

  4. В папке demos появятся файлы с расширением .ulx — это готовые файлы игр, которые можно запустить в интерпретаторе. Если установлены Windows Glulxe или Lectrote, то при запуске такого файла он скорее всего автоматически откроется в одном из этих интерпретаторов.

Организация своей игры:

  1. создайте папку для игры, например c:\inform\mygame\

  2. создайте главный исходный файл игры, 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";
    
    !============================================================================
    
  3. создайте .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;

Рассмотрим эту часть кода по шагам:

  1. Для объекта bird мы ввели новое свойство, before. Интерпретатор обращается к свойству before перед тем, как выполнить конкретное действие с объектом:

    before [; ... ],
    
  2. Значением свойства является локальная функция, в которой есть метка и две инструкции:

    Listen:
        print "Жалобный писк испуганной птички разрывает тебе сердце. 
            Надо помочь!^";
        return true;
    
  3. Метка обозначает тип действия, в данном случае Listen («послушать»). Меткой мы сообщаем интерпретатору следующее: если действие, которое будет совершено над птенчиком это «послушать», то надо выполнить эти инструкции. В противном случае продолжать как обычно. То есть, если игрок введёт ОСМОТРЕТЬ ПТЕНЦА, ВЗЯТЬ ПТЕНЦА, ПОЛОЖИТЬ ПТЕНЦА В ГНЕЗДО, УДАРИТЬ ПТЕНЦА или ПОГЛАДИТЬ ПТЕНЦА, то он получит стандартный ответ. Если же игрок введёт ПОСЛУШАТЬ ПТЕНЦА, то действие будет «перехвачено», и выполнятся наши инструкции.

  4. Выполнятся следующие инструкции:

    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;

Рассмотрим этот код по шагам:

  1. Для нашей комнаты мы добавили свойство after. Интерпретатор обращается к этому свойству после того, как выполнит любое действие в этой комнате:

    after [; ... ],
    
  2. Значение свойства является локальной функцией, содержащей метку и две инструкции:

    Drop:
        move noun to clearing;
        return false;
    
  3. Метка обозначает имя действия, в данном случае Drop. Мы сообщаем интерпретатору следующее: если только что было совершено действие Drop, то выполни эти инструкции перед тем, как сообщить игроку, что действие завершено. Если произошло другое действие, то продолжи как обычно.

  4. Сначала выполняется инструкция

    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.

Какие плееры открывают игры на Информе?

Как опубликовать игру онлайн?

Чтобы запустить произвольную игру в 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

Как описывать объекты

Нижеперечисленное относится только к объектам. На комнаты (локации) эти правила не распространяются.

  1. В имени объекта нужно отделить окончание (или окончания) прямым слешем (/), а точнее — те буквы, которые должны будут изменяться при склонении. Например: латунн/ая ламп/а, надпис/ь на стене, бел/ый туман/. Имя используется при выводе объектов во всевозможных ситуациях, в разных падежах.

  2. В свойстве name указываются слова, по которым парсер находит объекты. В русской версии нужно перечислять такие слова без окончаний. Здесь нужно не забыть указать и синонимы, по которым объект тоже должен находиться парсером.

  3. Каждому объекту (кроме комнат) обязательно нужно выдать один из атрибутов, соответствующий роду или числу объекта. Один из четырёх: male — мужской род, female — женский род, neuter — средний род, pluralname — множественное число.

    Примеры:

    Object -> "друг/ой склон/ холма"
      with name 'склон' 'сторон' 'холм',
      description "Кто мешает изучить его самому?",
      has  scenery male;
    
    Object -> "груб/ые каменн/ые ступен/и"
      with name 'груб' 'каменн' 'ступен',
      description "Грубые каменные ступени ведут по куполу вверх.",
      has  scenery pluralname;
    
  4. Особый случай — это существительные с беглыми гласными: ковёр (ковром), перекрёсток (перекрёстком) и так далее. Их нужно окружить двумя слешами, а к объекту добавить свойство 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;
    
  5. Есть случаи, когда нужно, чтобы у мужского объекта была женская логика для склонения имени. Например, мужское имя «Слава». В таком случае необходимо дополнительно выдать объекту атрибут fem_grammar.

  6. Если возникает случай, где парсер не в состоянии самостоятельно просклонять имя объекта (на вывод), необходимо указать свойство-функцию 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;
    
  7. Если парсер не распознаёт объект автоматически (на ввод), то следует пользоваться свойством-функцией 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: ним, ней, ними.

Как описывать русские глаголы

  1. Глаголы нужно перечислять по корневой части. Парсер распознает приставки («по», «за» и т.д.) и суффиксы-окончания («ся», «ять», «ать» и т.д.) автоматически.

  2. Нужно учесть и то, что глаголы могут вводиться и в повелительном наклонении: например, не только «взять» (вз), но и «возьми» (возьм). Это необходимо для приказов, которые игрок может отдавать NPC: «гоблин, отдай мне ключ».

  3. Для существительных, с которыми оперирует глагол, нужно указывать токены. Какой токен использовать, определяет падеж существительного в данном контексте. Например, «взять ключ» — винительный падеж (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.
  4. Также, не обязательно, но рекомендуется добавить объект с этими корневыми частями в 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 'вяз' 'вяж';