И снова Хейди
Даже в простой игре игрок может попробовать сделать то, о чём заранее не подумал автор. Часто разные действия игрока должны позволить один и тот же результат. Поэтому автору стоит попробовать ввести в игру все разумные варианты, которые могут прийти игроку в голову. Зачастую описания предметов или локаций прямо таки подсказывают, какие здесь есть объекты, и что можно потенциально сделать. Это тоже обязательно нужно учесть. Сделать игру довольно просто, но основное время уйдёт на продумывание различных второстепенных вариантов.
В этой главе мы рассмотрим некоторые из таких вариантов.
Послушать птенчика
Рассмотрим пример прямо из игры:
В лесной чаще
На западе, сквозь густую листву, можно разглядеть небольшое
строение. Тропинка ведет на северо-восток.
Здесь имеется птенчик.
> ОСМОТРЕТЬ ПТЕНЧИКА
Слишком мал, чтобы летать, птенец беспомощно попискивает.
> ПОСЛУШАТЬ ПТЕНЧИКА
Никаких необычных звуков нет.
>
Видна недоработка. Игра сообщает нам, что птенец беспомощно попискивает, но тут же говорит нам, что никаких необычных звуков нет.
В библиотеке есть обширный набор стандартных сообщений-ответов на стандартные действия. «Никаких необычных звуков нет» является стандартным ответом на команду ПОСЛУШАТЬ. Он подходит для ПОСЛУШАТЬ ГНЕЗДО или ПОСЛУШАТЬ ДЕРЕВО, но в данном случае, с птенцом, он неуместен. Нужно добавить собственную реакцию:
Object bird "птенчик/" forest
with description "Слишком мал, чтобы летать, птенец беспомощно попискивает.",
name 'детеныш' 'птиц' 'птичк' 'птенчик' 'птенц' 'маленьк',
before [;
Listen:
print "Жалобный писк испуганной птички разрывает тебе сердце.
Надо помочь!^";
return true;
],
has male;
Рассмотрим эту часть кода по шагам:
-
Для объекта
bird
мы ввели новое свойство,before
. Интерпретатор обращается к свойствуbefore
перед тем, как выполнить конкретное действие с объектом:before [; ... ],
-
Значением свойства является локальная функция, в которой есть метка и две инструкции:
Listen: print "Жалобный писк испуганной птички разрывает тебе сердце. Надо помочь!^"; return true;
-
Метка обозначает тип действия, в данном случае
Listen
(«послушать»). Меткой мы сообщаем интерпретатору следующее: если действие, которое будет совершено над птенчиком это «послушать», то надо выполнить эти инструкции. В противном случае продолжать как обычно. То есть, если игрок введёт ОСМОТРЕТЬ ПТЕНЦА, ВЗЯТЬ ПТЕНЦА, ПОЛОЖИТЬ ПТЕНЦА В ГНЕЗДО, УДАРИТЬ ПТЕНЦА или ПОГЛАДИТЬ ПТЕНЦА, то игрок получит стандартный ответ. Если же игрок введёт ПОСЛУШАТЬ ПТЕНЦА, то действие будет «перехвачено», и выполнятся наши инструкции. -
Выполнятся следующие инструкции:
print "Жалобный писк испуганной птички разрывает тебе сердце. Надо помочь!^";
что выведет на экран указанную строку (^ выполнит перенос на следующую строку). Далее,
return true;
что скажет интерпретатору, что ему больше не нужно ничего делать, потому что мы самостоятельно обработали действие
Listen
. Теперь в игре мы получим то, что хотели:> ПОСЛУШАТЬ ПТЕНЧИКА Жалобный писк испуганной птички разрывает тебе сердце. Надо помочь! >
Стоит остановиться подробнее на инструкции return true
. Свойство before
перехватывает действие над объектом прежде, чем интерпретатор начнёт с ним что-то делать. В этот момент выполняются инструкции из локальной функции. Если последней инструкцией идёт return true
, то значит что действие обработано и интерпретатору больше ничего не нужно делать, никаких действий или сообщений. Однако если в конце функции стоит return false
, тогда интерпретатор продолжит выполнять действие так, будто оно не было перехвачено. Иногда это полезно, но не в нашем случае. Если записать эту секцию следующим образом:
Object bird "птенчик/" forest
with description "Слишком мал, чтобы летать, птенец беспомощно попискивает.",
name 'детеныш' 'птиц' 'птичк' 'птенчик' 'птенц' 'маленьк',
before [;
Listen:
print "Жалобный писк испуганной птички разрывает тебе сердце.
Надо помочь!^";
return false;
],
has male;
то интерпретатор выведет сначала нашу строку, а затем стандартный ответ:
> ПОСЛУШАТЬ ПТЕНЧИКА
Жалобный писк испуганной птички разрывает тебе сердце. Надо
помочь!
Никаких необычных звуков нет.
>
При написании игр на Информе перехват действий указанным образом используется очень часто.
Вход в домик
В начале игры главная героиня стоит перед избушкой, что подразумевает, что в неё можно зайти.
Перед домом
Ты стоишь около избушки, на восток от которой раскинулся лес.
> ИДТИ ВНУТРЬ
Этот путь недоступен.
>
Опять видим не самый лучший ответ. Но это легко исправить:
Object before_cottage "Перед домом"
with description
"Ты стоишь около избушки, на восток от которой раскинулся лес.",
e_to forest,
in_to "Такой славный денек... Он слишком хорош, чтобы прятаться внутри.",
cant_go "Единственный путь ведет на восток.",
has light;
Обычно свойство in_to
вело бы в другую комнату, как, например e_to
, но если указать строку, то интерпретатор выведет эту строку, когда игрок попытается пойти ВНУТРЬ. Если пойти в другом неуказанном направлении, например НАВЕРХ или ИДТИ НА СЕВЕР, то игрок всё равно получит ответ «Этот путь недоступен», но это также легко изменить, добавив свойство cant_go
с соответствующей строкой. Получим более дружелюбное поведение игры:
Перед домом
Ты стоишь около избушки, на восток от которой раскинулся лес.
> ВНУТРЬ
Такой славный денек... Он слишком хорош, чтобы прятаться внутри.
> СЕВЕР
Единственный путь ведет на восток.
> ВОСТОК
В лесной чаще
...
Здесь есть и другая проблема — мы не реализовали саму избушку, поэтому ОСМОТРЕТЬ ИЗБУШКУ выдаст игроку «Этого предмета здесь нет». Добавим объект cottage и сделаем его с атрибутом scenery
, аналогично дереву:
Object cottage "маленьк/ий домик/" before_cottage
with description "Домик мал и неказист, но ты очень счастлива, живя здесь.",
name 'маленьк' 'дом' 'изб' 'терем' 'коттедж' 'хат' 'небольш' 'строен'
'домик' 'избушк' 'теремок' 'хатк',
has scenery male;
Это решает проблему, но приводит к ещё одному неподходящему ответу:
Перед домом
Ты стоишь около избушки, на восток от которой раскинулся лес.
> ВОЙТИ В ДОМИК
Но на/в маленький домик невозможно войти, встать, сесть или лечь.
Это решается аналогично тому, как мы сделали ПОСЛУШАТЬ ПТЕНЧИКА:
Object cottage "маленьк/ий домик/" before_cottage
with description "Домик мал и неказист, но ты очень счастлива, живя здесь.",
name 'маленьк' 'дом' 'изб' 'терем' 'коттедж' 'хат' 'небольш' 'строен'
'домик' 'избушк' 'теремок' 'хатк',
before [;
Enter:
print_ret "Такой славный денек...
Он слишком хорош, чтобы прятаться внутри.";
],
has scenery male;
При помощи свойства before
мы перехватываем действие Enter
(ВОЙТИ), которое применяется к объекту cottage
. Правда, в этот раз мы воспользовались только одной инструкцией, а не двумя. Просто необходимость «вывести строку, сделать перевод строки, и затем return true
» встречается так часто, что для этого есть отдельная инструкция, print_ret
. То есть:
print_ret "Такой славный денек...
Он слишком хорош, чтобы прятаться внутри.";
идентично
print "Такой славный денек...
Он слишком хорош, чтобы прятаться внутри.^";
return true;
Заметьте, что в print_ret
нам не понадобился символ переноса ^
.
Залезть на дерево
На полянке предполагается, что игрок введёт ВВЕРХ. Но игрок с большой вероятностью попробует ЗАЛЕЗТЬ НА ДЕРЕВО, но получит в ответ лишь «Забираться на высокий платан бессмысленно». Вновь воспользуемся свойством before
, но чуть по-другому.
Object tree "высок/ий платан/" clearing
with description
"Величавое дерево стоит посреди поляны.
Кажется, по его стволу будет несложно влезть наверх.",
name 'высок' 'платан' 'дерев' 'ствол' 'величав',
before [;
Climb:
PlayerTo(top_of_tree);
return true;
],
has scenery male;
Здесь мы перехватываем действие Climb
(ЗАЛЕЗТЬ), применяемое к объекту tree
, но не для того, чтобы вывести своё сообщение, а для того, чтобы переместить игрока в другую комнату, так как если бы игрок ввёл ВВЕРХ. Перемещать игрока вручную довольно сложно, но, к счастью, в библиотеке есть стандартная функция, которая делает всё за нас.
Функция называется PlayerTo
, и её нужно вызвать с параметром — идентификатором комнаты, куда мы хотим переместить игрока. При вызове параметр указывается внутри скобок: PlayerTo(top_of_tree)
. Ранее мы встретились с функцией Initialise
, эта функция не принимает никаких параметров, поэтому мы сказали, что её можно было бы вызвать как Initialise()
.
Мы переместили игрока, но всё ещё находимся в перехватчике действия Climb
. И поскольку мы уже обработали действие самостоятельно, нам не нужен стандартный ответ, и мы выполняем return true
.
Бросить предмет с дерева
В любой комнате если игрок введёт ПОЛОЖИТЬ (БРОСИТЬ) предмет, который он или она несёт с собой, то он (предмет) упадёт в той же локации рядом на землю. Это поведение работает убедительно везде, кроме верхушки дерева — там предмет не должен падать рядом, а должен падать вниз на опушку.
Нам нужно перехватить действие Drop
, но несколько иначе, чем мы делали раньше. Во-первых, действие должно срабатывать не для конкретных bird
или nest
, но в общем, то есть для любых предметов. И во-вторых, нужно учесть, что не все предметы можно бросить: например, нельзя БРОСИТЬ СУК.
Для решения второго пункта нужно перехватить действие Drop
не до, а после того, как оно произошло. Так мы даём библиотеке разобраться с объектами, которые вообще нельзя бросить, или которых нет в руках у игрока, и вступаем только тогда, когда предмет уже был брошен. А для решения первого пункта мы будем перехватывать Drop
не для наших объектов, а прямо на локации, где это происходит, то есть на верхушке дерева, top_of_tree
:
Object top_of_tree "На верхушке дерева"
with description "На этой высоте цепляться за ствол уже не так удобно.",
d_to clearing,
after [;
Drop:
move noun to clearing;
return false;
],
has light;
Рассмотрим этот код по шагам:
-
Для нашей комнаты мы добавили свойство
after
. Интерпретатор обращается к этому свойству после того, как выполнит любое действие в этой комнате:after [; ... ],
-
Значение свойства является локальной функцией, содержащей метку и две инструкции:
Drop: move noun to clearing; return false;
-
Метка обозначает имя действия, в данном случае
Drop
. Мы сообщаем интерпретатору следующее: если только что было совершено действиеDrop
, то выполни эти инструкции перед тем, как сообщить игроку, что действие завершено. Если произошло другое действие, то продолжи как обычно. -
Сначала выполняется инструкция
move noun to clearing;
которая берёт объект, который был перенесён из объекта игрока,
player
, в объектtop_of_tree
(так как выполнилось действиеDrop
), и переносит его ещё раз, в объектclearing
. В инструкцииnoun
является библиотечной переменной, в которой будет храниться идентификатор объекта, к которому применяется действие. То есть если игрок вводит БРОСИТЬ ГНЕЗДО, тоnoun
укажет наnest
, а если БРОСИТЬ ПТЕНЦА, тоnoun
станетbird
. Далее мы исполняемreturn false;
что говорит интерпретатору, что теперь он может вывести игроку свой ответ, то есть, что произошло.
Вот что мы получим в игре:
На верхушке дерева На этой высоте цепляться за ствол уже не так удобно. Здесь имеется надежный толстый сук. > БРОСИТЬ ГНЕЗДО Птичье гнездо положено. > ОСМОТРЕТЬСЯ На верхушке дерева На этой высоте цепляться за ствол уже не так удобно. Здесь имеется надежный толстый сук. > ВНИЗ Полянка Здесь имеется птичье гнездо (где имеется птенчик). >
Конечно, сообщение «Птичье гнездо положено». совсем не информативно в данном случае, поэтому можно сделать так:
Object top_of_tree "На верхушке дерева"
with description "На этой высоте цепляться за ствол уже не так удобно.",
d_to clearing,
after [;
Drop:
move noun to clearing;
print_ret "Предмет упал вниз на землю.";
],
has light;
Здесь print_ret
выводит более корректное сообщение и возвращает true
, что означает что интерпретатор больше не должен ничего выводить на экран.
Птица в гнезде
Игра заканчивается, когда игрок кладёт гнездо на сук. Мы предположили, что птенец уже находится в гнезде, но это может быть не так. Необходимо также проверить, лежит ли птенец в гнезде. Это легко сделать:
Object branch "надежн/ый толст/ый сук/" top_of_tree
with description "Сук достаточно ровный и крепкий, чтобы на нем надежно
держалось что-то не очень большое.",
name 'надежн' 'ровн' 'толст' 'крепк' 'сук' 'ветк',
each_turn [; if (bird in nest && nest in branch) deadflag = 2; ],
has static supporter male;
Мы расширили инструкцию if
:
if (bird in nest && nest in branch) deadflag = 2;
Её можно прочитать так: «Если bird
находится на/в гнезде, и nest
находится на/в branch
, то установить deadflag
равным 2. В противном случае ничего не делать».
Заключение
К этому моменту вы уже поняли, что в игре обязательно надо реализовывать не только свои действия, но и те необязательные, которые могут прийти в голову игроку. Игрок может попробовать вообще что-то глупое, но ему будет интересно получить нестандартный ответ. Непроработка ответов может вообще разрушить эффект погружения.
В этой главе мы рассмотрели:
Свойства объектов
У объектов может быть свойство before
, и если оно есть, то интерпретатор обращается к нему перед выполнением действия над объектом. Аналогично есть свойство after
, куда происходит обращение уже после выполненного действия, но ещё до того, как выведется сообщение игроку. И before
, и after
могут быть как у объектов-предметов (где перехватываются действия применимые к этим объектам), так и у комнат (где перехватываются действия, которые происходят с объектами в комнате).
Значениями этих двух свойств являются локальные функции. Если функция заканчивается return false
, то интерпретатор продолжит выполнение, а если return true
— то интерпретатор больше ничего делать не будет. Так можно либо частично изменить выполнение действия, либо заменить его полностью.
Ранее свойства соединений комнат указывали на объект комнаты, куда произойдёт перемещение. В этой главе также было показано, что это может быть строка (в которой указана причина, почему перемещение невозможно). Также было рассмотрено свойство cant_go
, которое учитывает все неуказанные направления:
e_to forest,
in_to "Такой славный денек... Он слишком хорош, чтобы прятаться внутри.",
cant_go "Единственный путь ведет на восток.",
Функции и параметры
В библиотеке есть много удобных функций, и мы воспользовались функцией PlayerTo
, которая позволяет перенести игрока в другую комнату, не обязательно соседнюю.
При вызове этой функции нам нужно было указать конечную комнату, и мы сделали это в скобках:
PlayerTo(clearing);
Значения в скобках называются параметрами функции. Их может быть несколько, в таком случае они разделяются запятыми. Например можно перенести игрока без вывода описания комнаты, указав второй аргумент:
PlayerTo(clearing, 1);
Здесь 1 подавляет вывод описания новой комнаты.
Инструкции
Нам встретились новые инструкции:
return true;
return false;
Они использовались для управления интерпретатором в локальных функциях.
print "строка";
print_ret "строка";
Инструкция print
выводит указанную строку, а print_ret
выводит строку, делает перенос и выполняет return true
.
if (условие && условие) ...
Мы расширили условие в инструкции if
. &&
— оператор «И», который используется для проверки выполнения нескольких условий сразу. Также существуют оператор ||
«ИЛИ» и оператор ~~
«НЕ».
move объект to родитель;
Инструкция move
изменяет дерево объектов, устанавливая для объекта нового родителя.
Действия
Мы говорили о перехвате таких действий как Listen
, Enter
, Climb
и Drop
. Действие это представление того, что должно быть сделано, в зависимости от глагола, который введёт игрок. Например, ПОСЛУШАТЬ и ПРИСЛУШАТЬСЯ суть одно и то же, поэтому им соответствует действие Listen
. Аналогично ВОЙТИ, ЗАЙТИ, СЕСТЬ НА, ЛЕЧЬ НА приводят к действию Enter
, а ПОЛОЖИТЬ, ВЫБРОСИТЬ означают Drop
. Так облегчается работа писателя-программиста, потому что различных глаголов может быть множество, но количество разных действий гораздо меньше.
В библиотеке каждому действию соответствует номер, и этот номер хранится в переменной action
. Также есть переменная noun
, в которой хранится идентификатор объекта, над которым производится действие, а также second
для второго такого объекта (если он есть).
Вот несколько примеров:
Ввод игрока | действие | noun | second |
---|---|---|---|
СЛУШАТЬ | Listen | nothing | nothing |
СЛУШАТЬ ПТЕНЧИКА | Listen | bird | nothing |
ПОДНЯТЬ ПТЕНЧИКА | Take | bird | nothing |
ПОЛОЖИТЬ ПТЕНЧИКА В ГНЕЗДО | Insert | bird | nest |
БРОСИТЬ ГНЕЗДО | Drop | nest | nothing |
ПОЛОЖИТЬ ГНЕЗДО НА СУК | PutOn | nest | branch |
nothing
— это встроенная константа, обозначающая отсутствие объекта.
В этих главах мы изучили основные принципы, на которых строятся любые игры на Информе, а также средства для создания более интересных реакций.