3. И снова Хейди

Даже в простой игре игрок может попробовать сделать то, о чём заранее не подумал автор. Часто разные действия игрока должны позволить один и тот же результат. Поэтому автору стоит попробовать ввести в игру все разумные варианты, которые могут прийти игроку в голову. Зачастую описания предметов или локаций прямо таки подсказывают, какие здесь есть объекты, и что можно потенциально сделать. Это тоже обязательно нужно учесть. Сделать игру довольно просто, но основное время уйдёт на продумывание различных второстепенных вариантов.

В этой главе мы рассмотрим некоторые из таких вариантов.

Послушать птенчика

Рассмотрим пример прямо из игры:

В лесной чаще
На западе, сквозь густую листву, можно разглядеть небольшое
строение. Тропинка ведет на северо-восток.

Здесь имеется птенчик.

> ОСМОТРЕТЬ ПТЕНЧИКА
Слишком мал, чтобы летать, птенец беспомощно попискивает.

> ПОСЛУШАТЬ ПТЕНЧИКА
Никаких необычных звуков нет.

>

Видна недоработка. Игра сообщает нам, что птенец беспомощно попискивает, но тут же говорит нам, что никаких необычных звуков нет.

В библиотеке есть обширный набор стандартных сообщений-ответов на стандартные действия. «Никаких необычных звуков нет» является стандартным ответом на команду ПОСЛУШАТЬ. Он подходит для ПОСЛУШАТЬ ГНЕЗДО или ПОСЛУШАТЬ ДЕРЕВО, но в данном случае, с птенцом, он неуместен. Нужно добавить собственную реакцию:

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 — это встроенная константа, обозначающая отсутствие объекта.

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