Начало

Пошагово рассмотрим написание простенькой игры Хейди, сюжет которой таков:

«Хейди живёт в маленьком домике в лесу. Одним солнечным днём она слышит писк птички — её гнездо упало с ветки на поляну. Хейди кладёт птичку в гнездо, а гнездо обратно на ветку».

Шаблон для игры

Для начала создадим исходный файл-шаблон. Создайте папку 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, то есть, что игра закончена, и выводит на экран «Вы выиграли».

На этом глава закончена, перекомпилируйте игру и проверьте, как работают внесённые изменения. Многое можно сделать получше, и это будет рассмотрено позднее. В следующей главе будет обобщение уже изученных особенностей Информа.