Данный файл является частью Руководства по TADS для авторов игр.
Copyright © 1987, 1996 Майкл Дж. Робертс (Michael J. Roberts). Все права защищены.

Руководство было преобразовано в формат HTML Н. К. Гайем (N. K. Guy), компания tela design.

Перевод руководства на русский язык - Валентин Коптельцев

Глава седьмая


Более сложные технические приемы

До сих пор мы делали упор на основы написания игр в TADS. Когда вы начнете писать игру сами, у вас, скорее всего, появится масса идей относительно элементов сюжета вашего квеста. В TADS многие элементы приключенческой игры реализуются элементарно, простым использованием основных классов из advr.t и "заполнением" соответствующих описаний, местоположений и т. д. Однако отдельные элементы не удастся реализовать с такой легкостью, и вам придется-таки заняться программированием.

Эта глава предназначена для того, чтобы помочь вам найти подходы к реализации ваших более сложных идей путем наглядной демонстрации способов реализации целого ряда нетривиальных элементов игрового мира. Мы постарались привести здесь примеры наиболее распространенных из таких элементов, поэтому вероятность того, что вы сразу найдете здесь то, что вам нужно, довольно высока. Но даже если здесь не обнаружится прямого примера элемента, который вы хотели бы реализовать в своей игре, чтение этой главы поможет вам понять многие "тонкие" моменты программирования в TADS, что позволит вам получить лучшее представление о том, как реализовать свои идеи.


Создание собственных глаголов

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

Однако, при всем при этом, у вас могут возникать ситуации в игре, требующие глаголов, не определенных в advr.t. Кроме того, нет совершенно ничего зазорного в том, чтобы "научить" игру понимать сравнительно малоупотребительные фразы, которые будут выражать примерно то же самое, что и более распространенные глаголы, определенные в advr.t (например, создавая игру про летчиков и самолеты, логично будет дополнительно к глаголу "сесть", для которого существует стандартное определение, использовать в игре глагол "приземлиться"). Кроме того, во многих играх используются магические заклинания, которые по своей сути являются специальными глаголами. Поскольку игрок будет узнавать эти заклинания в процессе игры, проблем с формулировкой команд возникать не должно.

Способ определения нового глагола зависит от того, как этот глагол будет использоваться. Самый простой случай - это когда глагол используется сам по себе, т. е. в команде, вводимой игроком, помимо глагола, не присутствует других объектов. Пример такого глагола - команда "север" (и вообще все "навигационные" глаголы); заклинания, как правило, попадают в эту же категорию. Чтобы определить глагол, использующийся без других объектов, вам надо создать объект класса deepverb, содержащей лексический атрибут verb (глагол), а также метод action( actor ). (Класс deepverb определен в advr.t; подробности см. в Приложении A). Метод action вызывается, когда глагол используется без объектов. В качестве примера рассмотрим реализацию одного классического магического заклинания; при его произнесении игрок волшебным образом переносится из пещеры в хижину, или из хижины в пещеру.

  magicVerb: deepverb
    verb = 'xyzzy' 'ксиззи'
    sdesc = "xyzzy"
    action( actor ) =
    {
        if ( actor.location = cave )
        {
          "Внезапно пещеру заволакивает облаком оранжевого дыма. 
          Когда оно рассеивается, ты внезапно обнаруживаешь, 
          что находишься в хижине!";
          actor.moveInto(shack);
        }
        else if (actor.location = shack)
        {
          "Внезапно ты чувствуешь головокружение, а комната 
          все быстрее и быстрее начинает вращаться вокруг тебя. 
          Более-менее придя в себя, ты обнаруживаешь, что находишься
          в пещере! ";
          actor.moveInto( cave );
        }
        else
          "Ничего не происходит.";
    }
  ;

Для глаголов, использующих "прямой" объект, глагольный объект остается довольно несложным: все, что требуется - это определить свойства verb и sdesc, а также специальное свойство doAction. Свойство doAction определяет строковое значение, используемое системой в названиях двух новых специальных свойств. В качестве примера рассмотрим реализацию глагола "нюхать", при использовании которого в командах в качестве "прямого" объекта указывается предмет, который требуется понюхать.

  smellVerb: deepverb
    verb = 'нюхать' 'понюхать' 'нюхай' 'понюхай'
    sdesc = "нюхать"
    doAction = 'Smell'
  ;

Когда система видит такое определение, она создает два новых специальных свойства: verDoSmell и doSmell. Теперь для каждого объекта, который игрок может понюхать, вам надо будет добавить методы verDoSmell и doSmell. В качестве примера мы определим цветок (обратите внимание, что из соображений хорошего тона мы предпочли отказаться от грубых шуток, выбрав для нашего примера объект с нейтральным, а не отталкивающим запахом):

  flower: item
    noun = 'роза' 'розы' 'розе' 'розу' 'розой' 'розе#d' 'розой#t'
    adjective = 'красная' 'красной' 'красную' 'красной#d' 'красной#t'
    sdesc = "красная роза"
    rdesc = "красной розы"
    ddesc = "красной розе"
    vdesc = "красную розу"
    tdesc = "красной розой"
    pdesc = "красной розе"
    ldesc = "Это просто красная роза. "
    verDoSmell( actor ) = {}
    doSmell( actor ) =
    {
      "Роза - она и в Африке роза...";
    }
    isHer=true
  ;

Если для объекта не определен метод verDoSmell, то при попытке понюхать этот объект будет выдаваться сообщение "Я не знаю, как нюхать этот объект." Поскольку глагол "нюхать", скорее всего, можно применить для любого предмета в игре, то для него имеет смысл изменить определение класса thing в advr.t так, чтобы на глагол "нюхать" по умолчанию выдавалась более осмысленная реакция. Например, вы можете добавить к определению thing в advr.t следующие строки:

  verDoSmell( actor ) = {}
  doSmell( actor ) =
  {
    "На запах это обычн";
    if(self.isHer)
      {"ая";
      }
    else if(self.isHim)
      {""ый"";
      }
    else
      {"ое";
      }
    <<self.sdesc>>. ";
  }

(isHer и isHim - это флаги, определяющие, соответственно, женский или мужской род соответствующего объекта).

После этого все, что вам потребуется - это переопределить метод doSmell для всех объектов с "нестандартным" запахом.

Определение глагола, использующего два объекта ("прямой" и "косвенный") осуществляется примерно так же. Только вместо того, чтобы определять свойство doAction, вам надо определить свойтсво ioAction(предлог). Например, мы хотим определить глагол "налить прямой-объект в косвенный-объект":

  pourVerb: deepverb
    verb = 'налить' 'налей' 'лить' 'лей'
    sdesc = "налить"
    ioAction( inPrep ) = 'PourIn'
    prepDefault = inPrep
  ;

На основании такого определения система создает свойства verIoPourIn, verDoPourIn и ioPourIn, которые именно в таком порядке и будут вызываться системой при обработке команды игрока (метод verDoPryWith вызывается для "прямого" объекта, а два остальных - для "косвенного"). Обратите внимание, что мы также определили предлог, используемый по умолчанию. Хотя указывать его и необязательно, это позволяет системе лучше угадывать, что именно игрок имел в виду, если в команде пропущены некоторые слова. Например, если единственным имеющимся в данный момент объектом, который можно использовать в качестве "косвенного" в команде, содержащей глагол "налить", система сможет дополнить фразу словами "в стакан", если игрок просто наберет "налить воду".

(Примечание переводчика: для английского языка нехарактерны команды типа "глагол прямой-объект косвенный-объект" (т. е. без предлога посередине), в то время как в русском языке подобные конструкции встречаются довольно часто. Способы определения таких глаголов рассматриваются в разделе, описывающем синтаксический анализатор).

Используете ли вы в игре нестандартные глаголы, или обходитесь набором, определенным в advr.t, никогда не помешает задокументировать их. Например, в инструкциях к вашей игре стоит привести список всех глаголов, которые понимает игра (или которые нужны для ее прохождения). Если игрок зайдет в тупик, такой список поможет ему убедиться в том, что его проблема - не в синтаксическом анализаторе.


Специальные эффекты при переходах между комнатами

Базовые процедуры перемещения игрока, определенные в advr.t, обеспечивают весьма простую реализацию основной карты игры: все, что требуется от вас - это определять для комнат в игре свойства с названиями типа north (север), south (юг) и т. д., и указывать комнаты, на которые эти свойства должны ссылаться:

  kitchen: room    // Это - кухня:
    north = hallway          // северный выход ведет в коридор...
    east = porch             // ...а восточный - на крыльцо.
  ;

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

Основа подхода к реализации специальных эффектов при перемещениях игрока состоит в осознании того, что относящиеся к направлениям свойства не обязательно должны просто ссылаться на другие помещения - вместо этого их можно определить как методы. В этих методах вы сможете определять практически любые действия - правда, при соблюдении некоторых базовых правил. Если вы хотите, чтобы после заверешения метода происходило нормальное перемещение игрока, метод должен возвращать ту комнату, куда игрок должен в итоге попасть. Если же после завершения метода ничего не должно происходить, этот метод должен возвращать nil. Следующие разделы описывают использование свойств, определяющих направления (далее называемые свойства-направления), для реализации разнообразных эффектов при перемещениях и переходах.


Вывод сообщения в процессе перемещения

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

  livingRoom: room
    sdesc = "Гостиная"
    ldesc = "Ты в гостиной. Темная лестница ведет вниз. "
    down =
    {
        "Ты начинаешь спускаться по лестнице, которая скрипит, 
        трещит и проседает при каждом твоем шаге. Остановившись на 
        мгновение, чтобы покрепче ухватиться за перила, ты уже 
        собираешься продолжить свой путь, как вдруг вся лестница 
        отделяется от стены и обрушивается вниз. Ты приземляешься 
        в куче щепок. Через несколько секунд, когда все стихает, ты
        встаешь (к счастью, невредимый) и отряхиваешься.\b";
        return( cellar );
    }
  ;

Символ вставки пустой строки ("\b") в конце сообщения служит для того, чтобы отделить сообщение об обрушившейся лестнице от описания конаты, которое всегда выводится, когда игрок перемещается в новую комнату. Поскольку после вывода сообщения метод возвращает объект-комнату, игрок будет перемещен в эту комнату, как обычно.


Простейшие препятствия

Рушащаяся лестница из предыдущего примера потребует некоторых дополнительных действий при реализации места назначения, поскольку игрок явно не сможет подняться по лестнице обратно после того, как она рухнула. Самое простое - это вообще запретить перемещение вверх, что реализуется элементарно: просто определите метод up (отвечающий за перемещение вверх), который бы выводил соответствующее сообщение, после чего возвращал бы nil.

  cellar: room
    sdesc = "Подвал"
    ldesc = "Ты находишься в подвале. Половину помещения занимает огромная куча 
            из обломков дерева, которая раньше была лестницей. "
    up =
    {
        "По этой лестнице уже вряд ли кому-то удастся подняться. ";
        return( nil );
    }
  ;

Поскольку метод возвращает nil, после вывода сообщения игрок никуда не перемещается.

Обратите внимание, что в целях полноты реализации следует добавить объект для сломанной лестницы, чтобы игрок мог упоминать лестницу в своих командах. Пусть это будет объект класса fixeditem, поскольку сломанную лестницу нельзя ни унести с собой, ни манипулировать ею еще каким-либо образом.

  brokenStairs: fixeditem
    location = cellar
    noun = 'лестница' 'лестницы' 'лестнице' 'лестницу' 'лестницей' 
           'лестнице#d' 'лестницей#t' 'куча' 'кучи' 'куче' 'кучу'
           'кучей' 'куче#d' 'кучей#t' 'обломков' 'обломков#r'
    adjective = 'сломанная' 'сломанной' 'сломанную' 'сломанной#d' 'сломанной#t'
    sdesc = "куча обломков"
    rdesc = "кучи обломков"
    ddesc = "куче обломков"
    vdesc = "кучу обломков"
    tdesc = "кучей обломков"
    pdesc = "куче обломков"
    ldesc = "Эта куча обломков раньше была лестницей, 
            но теперь тебе вряд ли удастся куда-нибудь по ней
            взобраться. "
    isHer=true
  ;


Изменяемые описания комнат

Приведенное в предыдущем примере определение комнаты можно еще улучшить, "разрешив" игроку попасть в нее через другой вход и сделав таким образом возможным увидеть лестницу до того, как она рухнула. Для реализации такой возможности описание подвала и его метод up должны отслеживать, рухнула ли уже лестница, и в зависимости от этого выдавать разный текст.

Реализация описания комнаты, зависящего от каких-либо внешних условий, выполняется путем написания соответствующего метода ldesc - аналогично тому, как реализуются специальные эффекты при перемещениях игрока посредством использования свойств-направлений.

Поскольку лестницу можно увидеть до того, как она рухнула, нам понадобится определить новый объект для исправной лестницы. Кроме того, так как по лестницам можно подниматься, нам потребуется также определить для этго объекта реакцию на соответствующий глагол (методы verDoClimb и doClimb). Учтите также, что метод actor.travelTo(location) - это тот самый метод, который вызывается, когда игрок перемещается из комнаты в комнату обычным образом, используя навигационные команды (например, "вверх"). Наконец, обратите внимание, что свойству location объекта brokenStairs, представляющего собой рухнувшую лестницу, поначалу не присвоено никакого значения (или, иначе говоря, оно равно nil), поскольку изначально рухнувшей лестницы в новой редакции игры нет.

  workingStairs: fixeditem
    location = cellar
    noun = 'лестница' 'лестницы' 'лестнице' 'лестницу' 'лестницей' 
           'лестнице#d' 'лестницей#t' 
    sdesc = "лестница"
    rdesc = "лестницы"
    ddesc = "лестнице"
    vdesc = "лестницу"
    tdesc = "лестницей"
    pdesc = "лестнице"
    ldesc = "Эта лестница, ведущая вверх, выглядит крайне хлипкой. "
    verDoClimb( actor ) = {}
    doClimb( actor ) =
    {
      actor.travelTo( livingRoom );
    }
    isHer=true
  ;

Теперь изменим метод up объекта cellar так, чтобы он отражал текущее состояние лестницы. Мы сможем обнаружить, рухнула ли уже лестница, путем проверки местоположения (свойства location) объекта workingStairs object: если он находится в подвале (комната cellar), значит, лестница еще цела, а если значение свойства location равно nil (т. е. объект не находится ни в одной из комнат игры), это будет означать, что лестница уже разрушена. (То, каким образом объект для исправной лестницы будет заменен объектом, соответствующим разрушенной лестнице, будет рассмотрено ниже; эта замена будет происходить при выполнении метода down объекта livingRoom). Мы используем эту проверку для того, чтобы оба свойства - ldesc и up - правильно реагировали на состояние лестницы.

 cellar: room
    sdesc = "Подвал"
    ldesc =
    {
        "Ты находишься в подвале. ";
        if ( workingStairs.location = nil )
            "Половину помещения занимает огромная куча 
            из обломков дерева, которая раньше была лестницей. ";
        else
            "Вверх ведет лестница, которая, по правде сказать,
            не внушает никакого доверия. ";
    }
    up =
    {
        if ( workingStairs.location = nil )
        {
            "По этой лестнице уже вряд ли кому-то удастся подняться. ";
            return( nil );
        }
        else
        {
            "Ступеньки лестницы угрожающе скрипят, кряхтят и прогибаются, 
            но тебе все-таки удается каким-то образом взобраться по ним наверх.\b";
            return( livingRoom );
        }
     }
  ;

Нам также надо слегка изменить метод down объекта livingRoom, чтобы осуществить замену объекта workingStairs на brokenStairs. Ниже приведена новая версия метода down, который реализует эту замену.

  livingRoom: room
    sdesc = "Гостиная"
    ldesc = "Ты в гостиной. Темная лестница ведет вниз. "
    down =
    {
        if ( workingStairs.location = nil )
        {
            "Ты вовремя замечаешь, что лестница разрушена и спуститься
            по ней вниз не удастся. ";
            return( nil );
        }
        else
        {
	      "Ты начинаешь спускаться по лестнице, которая скрипит, 
            трещит и проседает при каждом твоем шаге. Остановившись на 
            мгновение, чтобы покрепче ухватиться за перила, ты уже 
            собираешься продолжить свой путь, как вдруг вся лестница 
            отделяется от стены и обрушивается вниз. Ты приземляешься 
            в куче щепок. Через несколько секунд, когда все стихает, ты
            встаешь (к счастью, невредимый) и отряхиваешься.\b";
            workingStairs.moveInto( nil );      /* Заменяем исправную лестницу (workingStairs)... */ 
            brokenStairs.moveInto( cellar );       /* ... на разрушенную (brokenStairs) */           
            return( cellar );   
         }
     }
  ;


Двери

Одним из широко распространенных типов препятствий является дверь. Двери могут быть реализованы в полном соответствии с рассмотренным выше механизмом, который использовался нами для реализации двери: в свойствах-направлениях комнаты, относящихся к двери (дверям), мы проверяем состояние двери (как правило, для этого используется свойство isopen), и в случае, если дверь открыта, возвращаем значение комнаты, куда попадет игрок, а если нет, то выводим сообщение о том, что дверь закрыта, и возвращаем nil. Этот механизм используется для реализации дверей в игре "Блуждания в Окопный День" - см. файл DITCHR.T.

Однако в advr.t определен целый набор классов, упрощающий реализацию дверей. Все эти классы основаны на одном общем классе obstacle (препятствие) (вы как автор игры также можете определять собственные специализированные классы на его основе). Базовым классом для дверей является doorway, а определенный на его основе класс-наследник lockableDoorway упрощает реализацию дверей, отпираемых и запираемых при помощи ключа.

Используя классы doorway и lockableDoorway, вы сможете реализовать двери без дополнительного программирования, поскольку всю работу по определению состояния двери посредством анализа свойств, определенных для соответствующего объекта, выполнят стандартные процедуры, "отвечающие" за перемещения игрока.

Первая особенность, которую следует учитывать при работе с дверями, состоит в том, что дверь существует в двух комнатах сразу, поскольку она соединяет две комнаты. К сожалению, в TADS отсутствуют удобные средства размещения одного и того же объекта сразу в нескольких комнатах. Чтобы обойти эту проблему, вам потребуется создать по два объекта для каждой из дверей, присутствующих в игре: по одному объекту для каждой стороны двери. К счастью, класс doorway обеспечивает автоматическую синхронизацию состояния обоих объектов; например, если вы открываете одну сторону двери, другая ее сторона также будет открыта.

Начните с реализации обеих сторон двери. Для каждой стороны двери определите объект класса doorway, установив для него основные свойства (location, sdesc, noun). Поместите каждый из этих объектов в ту комнату, где они должны находиться (т. е. в комнаты, расположенные по разные стороны данной двери). Теперь определите специализированные свойства. Установите otherside равным второму объекту, составляющим пару сторон двери с текущим; благодаря этому внутренние процедуры объекта-двери будут знать, состояния каких объектов им синхронизировать. Свойство doordest установите равным комнате, в которую эта дверь ведет.

После этого все, что вам потребуется сделать - это установить свойства-направления комнат, в которых расположены двери, равными не другим комнатам, а дверям, которые в эти комнаты ведут. Ниже приведен пример с двумя комнатами, соединенными дверью.

  porch: room
    sdesc = "Крыльцо"
    ldesc = "Ты стоишь на крыльце огромного заброшенного деревянного дома, 
            который когда-то, видимо, был покрашен белым, но сейчас
            приобрел грязно-сероватый цвет. 
            << porchDoor.isopen ? "Открытая" : "Закрытая" >>
            дверь ведет на запад."
    west = porchDoor                   /* В качестве значения свойства-направления используем дверь */
  ;

  frontHall: room
    sdesc = "Холл"
    ldesc = "Ты находишься в просторном холле старого дома. 
            Краска со стен пооблезла, а с потолка свисает густая
            бахрома из паутины. Дверь, ведущая к востоку, в данный момент 
            <<hallDoor.isopen ? "открыта" : "закрыта">>)."
    east = hallDoor
  ;

  porchDoor: doorway
    location = porch
    noun = 'дверь' 'двери' 'дверью' 'двери#d' 'дверью#t'
    sdesc = "дверь"
    rdesc = "двери"
    ddesc = "двери"
    vdesc = "дверь"
    tdesc = "дверью"
    pdesc = "двери"
    otherside = hallDoor
    doordest = frontHall
  ;

  hallDoor: doorway
    location = frontHall
    noun = 'дверь' 'двери' 'дверью' 'двери#d' 'дверью#t'
    sdesc = "дверь"
    rdesc = "двери"
    ddesc = "двери"
    vdesc = "дверь"
    tdesc = "дверью"
    pdesc = "двери"
    otherside = porchDoor
    doordest = porch
  ;

Обратите внимание, что специально определять свойство ldesc для дверей нет необходимости, хотя это и возможно. По умолчанию свойство ldesc, определенное для класса doorway, выводит текст типа "дверь открыта", или "дверь закрыта", или "дверь закрыта и заперта". Если вы хотите определить ldesc по-другому, то для проверки состояния двери и отображения соответствующего текста в ее описании вы можете использовать свойства isopen (открыта-закрыта) и islocked (заперта-отперта).

Следует упомянуть еще ряд свойств, определенных для класса doorway. Если дверь не заперта, то она будет открываться автоматически при попытке игрока пройти через нее (например, если в вышеприведенном примере игрок, находясь на крыльце, двинется на запад). Это обычно удобнее для игрока, поскольку избавляет его от необходимости набирать очевидную команду "открыть дверь" каждый раз, когда надо пройти через дверь. Однако если вы не хотите, чтобы дверь открывалась автоматически, вы можете просто определить для нее noAutoOpen = true; в этом случае игроку придется вводить специальную команду для открывания двери, даже если дверь незаперта.

Если вы хотите, чтобы дверь отпиралась и запиралась при помощи ключа, используйте класс lockableDoorway вместо doorway. После этого определите свойство mykey, установив его значение равным объекту-ключу (этот объект должен принадлежать классу keyItem). Если вы не будете определять свойство mykey, то дверь можно будет отпирать и запирать без помощи ключа, т. е. игрок сможет отпирать и запирать дверь при помощи команд типа "отпереть дверь" и "запереть дверь".

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


Транспортные средства

Транспортные средства (ТС) - это комнаты, которые перемещаются. В своей основе транспортные средства представляют собой развитие идеи сложных соединений между комнатами; при этом метод, вызываемый при переходе игрока из одной комнаты в другую, для транспортных средств не просто выводит на экран сообщение и/или блокирует перемещение в выбранном направлении, а обеспечивает так называемое "подвижное" межкомнатное соединение, при котором в зависимости от выполнения тех или иных условий игрок попадает в разные комнаты. Помимо этого, для транспортных средств, как правило, реализуются и другие объекты и реакции, как например органы управления транспортным средством или окно, позволяющее осматривать местность за пределами транспортного средства.

Первым типом транспортных средств, который мы рассмотрим, будут полностью изолированные средства передвижения, например, вагон метро или лифт. После этого, в разделе "Реализация ТС с помощью 'вложенных' комнат", будут также рассмотрены транспортные средства, которые одновременно являются полноправными предметами игрового мира. Полностью изолированные ТС можно свести к комнате, у которой одно или несколько свойств-направлений возвращают разные значения в зависимости от того, где это транспортное средство в данный момент находится. У такого средства передвижения свойство location отсутствует, поскольку оно (средство) фактически представляет собой еще одну комнату в игре; ему не соответствует никаких предметов в игровом мире.

В качестве примера попробуем реализовать лифт, который ходит между двумя этажами. Лифт будет иметь две кнопки управления - "вверх" и "вниз" и пару автоматических дверей.

Чтобы сделать лифт более реалистичным, сделаем так, чтобы путешествие с этажа на этаж занимало пару ходов. Для этого применим так называемую "функцию-извещатель", которая будет запускаться в тот момент, когда будет нажата кнопка и лифт придет в движение. Мы также будем использовать специальный флаг для того, чтобы отслеживать состояние лифта и блокировать действие кнопок управления во время его движения.

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

  elevDoors: fixeditem
    location = elevRoom
    noun = 'дверь' 'двери#d' 'дверью' 'дверью#t' 
           'двери' 'дверей' 'дверям' 'дверями' 'дверях' 'дверям#d' 'дверями#t'
    adjective = 'автоматическая' 'автоматической' 'автоматическую' 'автоматической#d' 
                'автоматической#t'
                'автоматические' 'автоматических' 'автоматическим' 'автоматическими' 
                'автоматическим#d' 'автоматическими#t'
    sdesc = "автоматические двери"
    rdesc = "автоматических дверей"
    ddesc = "автоматическим дверям"
    vdesc = "автоматические двери"
    tdesc = "автоматическими дверями"
    pdesc = "автоматических дверях"
    ldesc = "Это совершенно обычные автоматические двери. В данный момент они 
       <<self.isopen ? "открыты" : "закрыты">>. "
    isopen = true
    verDoOpen( actor ) =
    {
        "Поскольку двери автоматические, открыть их самостоятельно тебе не удастся. ";
    }
    verDoClose( actor ) =
    {
        "Поскольку двери автоматические, закрыть их самостоятельно тебе не удастся. ";
    }
    isThem=true
  ;

Свойство isThem просто означает, что данный объект имеет множественное число (поскольку у нас здесь пара дверей).

Реализация кнопок управления также не слишком сложна; для них используется базовый класс buttonitem. Используем свойство, которое будет указывать конечный пункт движения лифта при нажатии кнопки - кнопка "вверх" для верхнего этажа и, соответственно, кнопка "вниз" - для нижнего. Метод doPush будет проверять, движется ли лифт, а также не находится ли он уже в конечном пункте, определяемом данной кнопкой.

Чтобы не делать лишней работы, определим общий класс для двух кнопок лифта, а затем определим два объекта, принадлежащих этому классу.

  class elevButton: buttonitem
    location = elevRoom
    doPush( actor ) =
    {
        /* Если лифт движется или находится в конечном пункте, ничего не делаем */
        if ( elevRoom.isActive or elevRoom.curfloor = self.destination )
            "\"Щелк.\" ";
        {
        else
        {
            "Двери закрываются, и лифт приходит в движение. ";
            elevRoom.isActive := true;
            elevDoors.isopen := nil;
            elevRoom.counter := 0;
            elevRoom.curfloor := self.destination;
            notify( elevRoom, &moveDaemon, 0 );
         }
      }
    sdesc = {"кнопка \"<<self.btnDirection>>\"";}
    rdesc = {"кнопки \"<<self.btnDirection>>\"";}
    ddesc = {"кнопке \"<<self.btnDirection>>\"";}
    vdesc = {"кнопку \"<<self.btnDirection>>\"";}
    tdesc = {"кнопкой \"<<self.btnDirection>>\"";}
    pdesc = {"кнопке \"<<self.btnDirection>>\"";}
    noun = 'кнопка' 'кнопки' 'кнопке' 'кнопку' 'кнопкой' 'кнопке#d' 'кнопкой#t'
    isHer=true
   ;

  elevUpButton: elevButton
    adjective='вверх' 'вверх#r'
    btnDirection="вверх"
    destination = floor2                     /* Указывает, куда движется лифт при нажатии кнопки */
  ;

  elevDownButton: elevButton
    adjective='вниз' 'вниз#r'
    btnDirection="вниз"
    destination = floor1
  ;

"Падежные" свойства (rdesc, ddesc и т. д.) класса elevButton используют свойство btnDirection объекта - наследника класса для корректного отображения названия объекта в соответствующем падеже; например, в родительном падеже для объекта elevUpButtonбудет отображаться строка 'кнопки "вверх"'.

Как вы, наверное, помните, notify - это специальная встроенная функция, запускающая "демон", который, в свою очередь, представляет собой процедуру, которая выполняется после каждого хода игрока. Вызов notify в вышеприведенном примере сообщает программе-интерпретатору TADS, что ей следует вызывать метод moveDaemon объекта elevRoom после каждого хода (третий аргумент равен 0, что означает вызов метода после каждого хода; если бы он был ненулевым, то указывал бы число ходов, после которых метод вызывался бы только один раз).

Объект, соответствующий лифту, должен отслеживать свое текущее положение при помощи своих свойств exit и north. Он также должен отслеживать состояние дверей. Движение лифта контролируется демоном (тем самым, который инициализируется при нажатии одной из кнопок).

  elevRoom: room
    sdesc = "Лифт"
    ldesc = "Ты находишься в тесной кабине лифта. Автоматические двери (в данный
       момент они <<elevDoors.isopen ? "открыты" : "закрыты">>) находятся к северу. "
    out = ( self.north)
    north =
    {
        if ( elevDoors.isopen ) return( self.curfloor );
        else
        {
            "Двери лифта закрыты. ";
            return( nil );
         }
     }
    curfloor = floor1          /* В начале игры лифт находится на нижнем этаже */
    moveDaemon =
    {
        self.counter++;
        switch( self.counter )
        {
            case 1:
            case 2:
            case 3:
              "\bЛифт продолжает свое движение. ";
              break;

            case 4:
          "\bЛифт останавливается, раздается мелодичный звонок, 
              и двери открываются. ";
          elevDoors.isopen := true;
              self.isActive := nil;
              unnotify( elevRoom, &moveDaemon );
              break;
       }
        }
     ;

Символы "\b", выводимые перед сообщениями метода moveDaemon, необходимы в связи с тем, что данная процедура выполняется как демон. Вследствие этого оказывается затруднительным предсказать, что именно выводилось на экран непосредственно перед выполнением метода, поэтому на всякий случай лучше вывести пустую строку, отделив таким образом сообщение, выводимое демоном, от предыдущего текста. Как правило, имеет смысл выводить пустую строку перед сообщением любого демона.

Обратите внимание, что вам понадобится проделать некоторую дополнительную работу для того, чтобы придать данному примеру законченный вид. Прежде всего вам потребуется определить комнаты floor1 и floor2. Задача сильно упрощается в случае, если попасть в эти комнаты можно только на лифте, поскольку в этом случае вам не надо учитывать в описании комнаты, находится ли лифт на одном этаже с игроком или нет. Если вы хотите усложнить пример, сделав его одновременно более реалистичным, и обеспечить попадание на этажи не только посредством лифта, но и другим путем (например, по лестнице), то вам потребуется дополнительно сделать две вещи: во-первых, поместить на каждом из этажей по паре дверей, которые будут закрыты, если лифт не находится на данном этаже; и во-вторых, обеспечить возможность вызова лифта с соответствующего этажа.

Ниже приведен общий класс для определения дверей на каждом из этажей. Мы реализуем в методе isopen пары дверей проверку нахождения лифта на текущем этаже (который соотетствует местоположению самих дверей). Методы verDoOpen и verDoClose будут такими же, как и для объекта elevDoors.

  class outerElevDoor: fixeditem
    noun = 'дверь' 'двери#d' 'дверью' 'дверью#t' 
           'двери' 'дверей' 'дверям' 'дверями' 'дверях' 'дверям#d' 'дверями#t'
           'лифт' 'лифта' 'лифту' 'лифтом' 'лифте' 'лифту#d' 'лифтом#t'
    adjective = 'автоматическая' 'автоматической' 'автоматическую' 'автоматической#d' 
                'автоматической#t'
                'автоматические' 'автоматических' 'автоматическим' 'автоматическими' 
                'автоматическим#d' 'автоматическими#t'
                'лифта' 'лифта#r'
    sdesc = "двери лифта"
    rdesc = "дверей лифта"
    ddesc = "дверям лифта"
    vdesc = "двери лифта"
    tdesc = "дверями лифта"
    pdesc = "дверях лифта"
    ldesc = "Двери лифта <<self.isopen ? "открыты" : "закрыты">>. "
    isopen = ( elevDoors.isopen and elevRoom.curfloor = self.location )
    verDoOpen( actor ) =
    {
        "Поскольку двери автоматические, открыть их самостоятельно тебе не удастся. ";
    }
    verDoClose( actor ) =
    {
        "Поскольку двери автоматические, закрыть их самостоятельно тебе не удастся. ";
    }
    isThem=true
  ;

Теперь вам просто надо добавить по одному объекту этого класса для каждого из этажей; все, что вам потребуется - это определить для каждого из объектов свойство location. Кроме того, на каждом из этажей следует обеспечить отслеживание состояния дверей свойством south (аналогично тому, как свойство north объекта elevRoom отслеживает состояние внутренних дверей лифта).

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

  class outerButton: buttonitem
    sdesc = "кнопка вызова"
    rdesc = "кнопки вызова"
    ddesc = "кнопке вызова"
    vdesc = "кнопку вызова"
    tdesc = "кнопкой вызова"
    pdesc = "кнопке вызова"
    noun = 'кнопка' 'кнопки' 'кнопке' 'кнопку' 'кнопкой' 'кнопке#d' 'кнопкой#t'
    adjective = 'вызова' 'вызова#r'
    doPush( actor ) =
    {
        /* Игнорировать, если лифт уже на текущем этаже */
        if ( elevRoom.curfloor = self.location and elevDoors.isopen )
            "Благодаря своей необычайной наблюдательности ты обнаруживаешь,
            что лифт уже здесь. ";
        else
        {
            "\"Щелк.\"";
            if ( not elevRoom.isActive ) notify( elevRoom, &moveDaemon, 0 );
            elevRoom.isActive := true;
            elevDoors.isopen := nil;
            elevRoom.counter := 0;
            elevRoom.curfloor := self.location;
        }
     }
    isHer=true
  ;

Если вы попробуете выполнить данный пример, вы обнаружите, что возникла некоторая проблема: сообщения демона, перемещающего лифт, выводятся независимо от того, находится ли игрок в лифте или нет. Поскольку мы используем один и тот же демон для кнопок управления внутри лифта и кнопок вызова на этажах, нам потребуется изменить порядок его работы так, чтобы при выводе сообщений учитывалось также и местоположение игрока. Сообщения, описывающие движение лифта, должны выводиться только тогда, когда игрок находится внутри лифта; с другой стороны, сообщения об открытии/закрытии дверей должны выводиться также и тогда, когда игрок находится на том этаже, куда приходит лифт. Модифицированный метод moveDaemon объекта elevRoom приведен ниже (в остальном этот объект остался неизменным).

  moveDaemon =
  {
        self.counter++;
        switch( self.counter )
        {
          case 1:
          case 2:
          case 3:
            if ( Me.location = self )
                "\bЛифт продолжает свое движение. ";
            break;
          case 4:
            if ( Me.location = self )
                "\bЛифт останавливается, раздается мелодичный звонок, 
                       и двери открываются.  ";
            else if ( Me.location = self.curfloor )
                "\bТы слышишь мелодичный звонок, 
                и двери лифта открываются. ";
            elevDoors.isopen := true;
            self.isActive := nil;
            unnotify( elevRoom, &moveDaemon );
            break;
         }
    }

Существует еще немало способов для того, чтобы сделать наш лифт более реалистичным. Самое простое - это добавить дополнительные этажи (и, соответственно, кнопки управления). Кроме того, неплохо было бы реализовать табло, которое отображало бы, на каком этаже в данный момент находится лифт. Наконец, в настоящих лифтах (тех, которые используются в офисных зданиях и гостиницах) используется несколько иной алгоритм работы: как правило, при движении в определенном направлении они останавливаются на каждом этаже, с которого был осуществлен вызов (или была нажата кнопка в самом лифте, соответствующая данному этажу). Когда лифт достигает верхнего или нижнего этажа, все кнопки управления внутри лифта "сбрасываются". После этого лифт меняет направление движения, вновь останавливаясь на этажах, где это требуется. Реализация подобного алгоритма несколько сложнее - вы можете попробовать выполнить ее в качестве самостоятельного упражнения.


Стулья и другие "вложенные" комнаты

"Вложенная" комната - это комната, расположенная внутри другой комнаты-локации. В большинстве случаев "вложенные" комнаты одновременно являются и самостоятельными объектами. Основная отличительная черта такой комнаты состоит в том, что она не изолирована от своего местоположения. Типичными примерами "вложенных" комнат являются стулья и кровати; они одновременно являются и объектами внутри другой комнаты, и самостоятельными комнатами - в том смысле, что игрок может находиться внутри них.

Создание "вложенных" комнат в TADS не составляет особого труда, поскольку в файле advr.t определен соответствующий класс (nestedroom), который содержит большую часть необходимых определений. Кроме того, в этом файле определены классы chairitem и beditem, позволяющие реализовать наиболее распространенные типы "вложенных" комнат практически без затрат труда.

Одно из отличительных свойств "вложенных" комнат, о котором следует сказать особо, состоит в том, что игрок, находясь во "вложенной" комнате, необязательно может манипулировать другими объектами во "внешней" комнате. По умолчанию ему недоступен ни один объект во внешней комнате. Однако вы без труда можете сделать любые объекты доступными из "вложенной" комнаты, определив список доступных объектов в свойстве reachable этой комнаты. Если вы хотите, чтобы из "вложенной" комнаты были доступны все объекты "внешней" комнаты, используйте следующее определение:

  reachable = ( self.location.contents)

Это означает, что игроку будут доступны все объекты внешней комнаты.


Реализация ТС с помощью "вложенных" комнат

Иногда вам может понадобиться создать транспортное средство, которое одновременно является "вложенной" комнатой. Например, вы хотите реализовать резиновый плот, который игрок может носить с собой, но в то же время может надуть его и использовать для перемещения по воде. Задачу сильно упрощает тот факт, что в файле advr.t специально для таких целей определен класс vehicle, представляющий собой объект, который игрок может носить с собой, но в то же время может и размещаться в нем, используя его в качестве ТС.

Рассмотрим реализацию нашего резинового плота. Для этого мы в первую очередь должны определить объект-наследник стандартного класса vehicle, при этом доработав его так, чтобы его можно было надувать. Кроме того, мы сделаем так, чтобы надуть плот игрок мог, только опустив его на землю, а также чтобы игрок не мог взять плот, предварительно его не сдув.

  raft: vehicle
    location = startroom
    sdesc = "надувной резиновый плот"
    rdesc = "надувного резинового плота"
    ddesc = "надувному резиновому плоту"
    vdesc = "надувной резиновый плот"
    tdesc = "надувным резиновым плотом"
    pdesc = "надувном резиновом плоте"
    noun = 'плот' 'плота' 'плоту' 'плотом' 'плоте' 'плоту#d' 'плотом#t'
    adjective = 'надувной' 'надувного' 'надувному' 'надувным' 'надувном' 'надувному#d' 'надувным#t' 
                'резиновый' 'резинового' 'резиновому' 'резиновым' 'резиновом' 'резиновому#d' 'резиновым#t'
    isinflated = nil
    ldesc = "Это надувной резиновый плот. В данный момент он 
             <<self.isinflated ? "надут" : "сдут">>. "
    verDoTake( actor ) =
    {
        if ( self.isinflated )
           "Сперва его придется сдуть. ";
        else
           pass doTake;
    }
    verDoInflateWith( actor, iobj ) =
    {
        if ( self.isinflated) 
          "Плот уже надут! ";
    }
    doInflateWith( actor, iobj ) =
    {
        if ( self.isIn( actor ) )
           "Сперва придется бросить плот. ";
        else
        {
            "Тебе удается надуть плот, хотя и не без труда. ";
            self.isinflated := true;
        }
    }
    verDoDeflate( actor ) =
    {
        if ( not self.isinflated ) 
          "Из плота уже выпущен весь воздух. ";
    }
    doDeflate( actor ) =
    {
        "Ты сдуваешь плот, который превращается в компактную кучку резины. ";
        self.isinflated := nil;
    }
    doBoard( actor ) =
    {
        if ( self.isinflated ) 
          pass doBoard;
        else 
          "Сперва плот необходимо надуть. ";
    }
    isHim=true
  ;

Нам потребуется также определить пару нестандартных глаголов; кроме того, неплохо было бы сразу же определить объект для насоса.

  inflateVerb: deepverb
    sdesc = "надуть" "надуй" "надувать" "надувай" 
    verb = 'надуть' 'надуй' 'надувать' 'надувай' 
    ioAction( withPrep ) = 'InflateWith'
    prepDefault = withPrep
  ;

  deflateVerb: deepverb
    sdesc = "сдуть"
    verb = 'сдуть' 'сдуй' 'сдувать' 'сдувай'
    doAction = 'Deflate'
  ;

  pump: item
    sdesc = "насос"
    rdesc = "насоса"
    ddesc = "насосу"
    vdesc = "насос"
    tdesc = "насосом"
    pdesc = "насосе"
    location = startroom
    noun = 'насос' 'насоса' 'насосу' 'насосом' 'насосе' 'насосу#d' 'насосом#t'
    verIoInflateWith( actor ) = {}
    ioInflateWith( actor, dobj ) =
    {
       dobj.doInflateWith( actor, self );
    }
    isHim = true
  ;

После того, как выполнены все описанные выше действия, реализация движения плота осуществляется практически так же, как и для полностью изолированного ТС (лифта из предыдущего примера). Единственное серьезное отличие будет состоять в том, что дял плота всегда определено его местоположение (свойство location). Как и для большинства подобных ТС, вы, возможно, захотите "запретить" игроку покидать плот, пока последний находится в движении. Для этого нам потребуется заменить методы doUnboard и out стандартного класса vehicle. В нашем случае будем считать, что река, по которой будет плыть плот, будет реализована с помощью ряда комнат класса riverRoom. Таким образом, мы будем проверять местоположение плота и, если он будет находиться в комнате класса riverRoom, не давать игроку покинуть плот. С этой целью мы определим для объекта raft приведенные ниже методы. Обратите внимание, что когда плот не плывет по реке, используются соответствующие методы, унаследованные от класса vehicle.

  doUnboard( actor ) =
  {
    if ( isclass( self.location, riverRoom ) )
        "оставайся на плоту, пока он плывет. ";
    else
      pass doUnboard;
  }
  out =
  {
    if ( isclass( self.location, riverRoom ) )
        "Сперва необходимо причалить к берегу. ";
        return( nil );
    else
      pass out;
  }

Все, что нам осталось сделать - реализовать перемещение плота. Для нашего примера мы сделаем следующее: каждая комната, сама по себе не относящаяся к реке, но граничащая с ней, должна иметь свойство toRiver, указывающее на граничащую с ней комнату (локацию) реки. Для каждой "речной" локации, где можно причалить к берегу, мы определим свойство toLand, ссылающееся на граничащую с ней локацию, не относящуюся к реке. Кроме того, у каждой "речной" комнаты будет определено также свойство downRiver, определяющее следующую локацию, куда плот должен попадать, плывя по течению. Мы также определим два новых глагола: "отплыть" и "причалить". Когда игрок будет находится на плоту, команда "отплыть" приведет плот в движение вниз по реке (если плот находится на суше), а по команде "причалить" игрок причалит к берегу (если это возможно в текущей локации). Определения глаголов приведены ниже.

  launchVerb: deepverb
  sdesc = "отплыть" 
  verb = 'отплыть' 'отплывай' 'отплыви' 'отплывать' 
         'отчалить' 'отчаливать' 'отчаль' 'отчаливай'
  action(actor)=
  {
    if ( isclass( raft.location, riverRoom ) )
        "Если ты еще не заметил - ты уже плывешь. ";
    else if ( raft.location.toRiver = nil )
        "Похоже, плыть тут некуда. ";
    else if ( Me.location <> raft )
        {if ( Me.location <> raft.location )
           "Сначала надо найти, на чем плыть. ";
         else
           "Сперва надо забраться на плот. ";
        }
    else
    {
        "Плот начинает неспешно скользить по речной глади. ";
        notify( raft, &moveDaemon, 0 );
        raft.counter := 0;
        raft.moveInto( raft.location.toRiver );
    }
  }

  ;

  landVerb: deepverb
  sdesc = "причалить" 
  verb = 'причалить' 'причаливать' 'причаль' 'причаливай'
  action(actor)=
  {
    if (not  isclass( raft.location, riverRoom ) )
        "Ты уже на суше. ";
    else if ( raft.location.toLand = nil )
        "Здесь негде причалить. ";
    else
    {
        "Ты причаливаешь к берегу. ";
        unnotify( raft, &moveDaemon );
        raft.moveInto( raft.location.toLand );
    }
  }
  ; 

Теперь добавим для объекта raft демон, который будет обеспечивать движение плота вниз по течению.

  moveDaemon =
  {
    "\bПлот продолжает плыть вниз по течению. ";
    self.counter++;
    if ( self.counter > 1 )
    {
        self.counter := 0;
        if ( self.location.downRiver = nil )
        {
             "Плот садится на мель.";
             self.moveInfo( self.location.toLand );
             unnotify( self, &moveDaemon );
         }
        else
        {
            self.moveInto( self.location.downRiver );
            self.location.riverDesc;
        }
     }
  }

Обратите внимание, что данный демон "ожидает", что все локации, относящиеся к реке, будут иметь свойство riverDesc, выводящее сообщение при перемещении плота в соответсвующую комнату. Метод moveDaemon будет задерживать плот в каждой комнате на два хода, а затем перемещать его в следующую "речную" локацию, осуществляя при этом вызов riverDesc для вывода соответствующего текста. Когда плот достигнет конца реки, метод автоматически выполнит причаливание; это значит, что для последней "речной" локации должно быть определено свойство toLand, не равное nil. В качестве альтернативы можно поместить в конце реки водопад или другой спецэффект.

Чтобы реализовать реку в игре, вам понадобится создать последовательность комнат класса riverRoom и определить для каждой из них свойство downRiver так, чтобы оно указывало на следующую локацию в последовательности. Места, где можно причалить к берегу, реализуются посредством установки свойств toRiver и toLand соответственно берега и соседнего с ним участка реки так, чтобы обеспечивалась перекрестная ссылка этих локаций друг на друга.


Скрытие объектов

Скрытие предметов игрового мира в TADS осуществляется довольно просто благодаря тому, что в advr.t определен целый набор классов, автоматически прячущих объект определенным образом.

Эти классы позволяют прятать предметы под или за другими предметами, а также определить объект таким образом, чтобы его содержимое стало видно только после того, как этот объект обыщут. Основными классами для объектов-укрытий являются underHider (спрятанный объект можно найти при помощи команды "посмотреть под..."), behindHider (команда "посмотреть за...") и searchHider (команда "обыскать"). Эти классы служат для реализации объектов, которые служат укрытием; для объектов, которые нужно скрыть, вместо свойства location следует определить свойство underLoc, behindLoc или searchLoc в зависимости от класса объекта-укрытия. Все объекты, которые вы прячете, должны принадлежать классу hiddenItem.

Например, чтобы спрятать ключ под кроватью, определите кровать как наследник класса underHider, а свойство underLoc ключа определите так, чтобы оно ссылалось на кровать.

  bed: beditem, underHider
    noun = 'кровать' 'кровати' 'кроватью' 'кровати#d' 'кроватью#t'
           'постель' 'постели' 'постелью' 'постели#d' 'постелью#t'
    location = startroom
    sdesc = "кровать"
    rdesc = "кровати"
    ddesc = "кровати"
    vdesc = "кровать"
    tdesc = "кроватью"
    pdesc = "кровати"
    isHer = true
  ;

  key: item, hiddenItem
    noun = 'ключ' 'ключа' 'ключу' 'ключом' 'ключе' 'ключу#d' 'ключом#t'
    sdesc = "ключ"
    rdesc = "ключа"
    ddesc = "ключу"
    vdesc = "ключ"
    tdesc = "ключом"
    pdesc = "ключе"
    underLoc = bed
    isHim = true
  ; 

Объекты behindHider и searchHider работают точно так же, однако вместо свойства underLoc для скрываемых объектов следует определить, соответственно, свойства behindLoc и searchLoc.

Учтите также, что для правильной реализации спрятанных предметов в игре в процессе инициализации игры необходимо вызвать функцию initSearch, определенную в advr.t (вызов осуществляется, как правило, из функции preinit). Функция initSearch инициализирует специальные списки содержимого для всех объектов-укрытий. При этом она включает в эти списки только объекты, принадлежащие классу hiddenItem; именно поэтому вам необходимо использовать этот класс в качестве родительского для всех ваших спрятанных объектов.

Не следует думать, что простота реализации скрытых объектов служит оправданием для того, чтобы прятать предметы в игре без всякого удержу. С точки зрения дизайна игры поиск спрятанного объекта редко бывает хорошей загадкой. Если уж вы прячете объект, обеспечьте какую-нибудь подсказку, что здесь что-то скрыто. В противном случае игроку придется педантично заглядывать под и за каждый предмет в игре, что будет довольно монотонным и утомительным занятием. Как и с большинством загадок, написать сложную для прохождения игру не составляет большого труда - гораздо сложнее написать игру, в которую будет интересно играть.


Наборы объектов

Каждому предмету в TADS-игре, с которым игрок может взаимодействовать, обычно соответствует объект, определенный в тексте программы игры. Чтобы игрок мог обращаться к каждому предмету в отдельности, автору игры необходимо определить уникальный набор лексических свойств для каждого объекта. Например, если у вас в игре имеется две книги, у них должно быть по крайней мере по одному прилагательному, относящемуся только к одной из книг, чтобы игрок мог точно определить, какую книгу он имеет в виду в своей команде.

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


Наборы объектов в качестве декораций

Предположим, что в вашей игре имеется книжный магазин. Вам, безусловно, захочется разместить на его прилавках большое количество разнообразных книг, но при этом избежать создания множества индивидуальных объектов для каждой книги - не только потому, что это потребует колоссальной работы, но и из-за того, что большинство книг не будут иметь прямого отношения к сюжету игры. Скорее всего, в магазине будет одна или две важных книги, а остальные будут присутствовать только для создания реалистичности.

В такой ситуации одним из способов обхода ограничений TADS является создание единого объекта, объединяющего весь набор "декоративных" книг. Поскольку большинство книг неважны для прохождения игры, их можно реализовать при помощи одного объекта, который будет определен таким образом, чтобы каждый раз при попытке взаимодействия с ним сообщать игроку о том, что эти предметы неважны для игры. Кроме того, этот общий объект может служить для того, чтобы обратить внимание игрока на другой, важный объект.

В качестве примера мы реализуем книги в книжном магазине. Все неважные для игрока книги будут представлены одним общим объектом. Однако при осмотре книг этот объект будет обращать внимание игрока на пару важных книг.

  manybooks: fixeditem
    sdesc = "книги"
    rdesc = "книг"
    ddesc = "книгам"
    vdesc = "книги"
    tdesc = "книгами"
    pdesc = "книгах"
    noun = 'книги' 'книг' 'книгам' 'книгами' 'книгах' 'книгам#d' 'книгами#t'
    location = bookstore
    ldesc =
    {
        "На полках магазина представлен широкий ассортимент книг - 
        от научных трудов до подборки свежих комиксов 
        серии "Люди Хе".";
        if ( not self.isseen )
        {
            "Название одной из книг приковывает твое внимание -
            \"О нашем холодном друге Жидком Азоте\". ";
            ln2book.moveInto( bookstore );
            self.isseen := true;
        }
    }
    verDoRead( actor ) =
    {
        "Ты с удовольствием сел бы и почитал книги, если бы не знал
        по собственному болезненному опыту, насколько неодобрительно относятся
        к такому поведению покупателей неофашиствующие сотрудники
        магазина. ";
    }
    verDoTake( actor ) =
    {
        "К сожалению, у тебя не хватит денег на все эти книги. ";
    }
    isThem = true
  ;

Чтобы избавить игрока от бесцельных потерь времени в попытках что-то сделать с книгами, мы реализовали специальные сообщения, которые известят игрока о том, что взаимодействовать с книгами бесполезно. Когда вы создаете объект-декорацию, всегда неплохо подумать о том, какие команды игрок может попытаться применить к нему, и обеспечить вывод сообщений о том, что данный предмет неважен джля прохождения игры. В данном примере мы ясно даем понять игроку, что книги невозможно взять или прочитать.

Однако самым важным из всех определений является свойство ldesc. Оно не только выводит общее описание книг, но также обращает внимание игрока на единственную важную книгу, когда игрок осматривает книги в первый раз. (Свойство isseen используются в качестве флага, отмечающего, выполнялось ли уже свойство ldesc. При первом вызове ldesc флаг isseen устанавливается равным true). Помимо вывода сообщения об определенной книге ldesc перемещает соответствующий этой книге объект в магазин.


Выбор объекта из набора

Вполне возможно, что вам захочется переработать вышеприведенный пример так, чтобы игрок мог как бы случайным образом выбрать одну или несколько отдельных книг из всего набора. Беря книгу из набора в первый раз, игрок получает одну книгу, во второй раз - другую, и т. д. Реализовать это проще всего путем замены метода doTake набора книг: вместо того, чтобы помещать в инвентарь игрока весь набор книг, этот метод переместит туда одну книгу, выбранную из некоторого списка объектов, соответствующих отдельным книгам.

 manybooks2: fixeditem
    sdesc = "книги"
    rdesc = "книг"
    ddesc = "книгам"
    vdesc = "книги"
    tdesc = "книгами"
    pdesc = "книгах"
    noun = 'книги' 'книг' 'книгам' 'книгами' 'книгах' 'книгам#d' 'книгами#t'
    location = bookstore
    ldesc =
    {
        "На полках магазина представлен широкий ассортимент книг - 
        от научных трудов до подборки свежих комиксов 
        серии "Люди Хе". Почему бы тебе просто не взять 
        какую-нибудь из них?";
    }
    verDoTake( actor ) =
    {
        if ( length( self.booklist ) = 0 )
            "Похоже, больше ничего интересного для тебя не осталось. ";
    }
    doTake( actor ) =
    {
        local selection;
        selection := self.booklist[ 1 ];
        self.booklist := cdr( self.booklist );
        selection.moveInto( actor );
        "Просмотрев книги, ты выбираешь << selection.thedesc >>. ";
    }
    booklist = [ ln2book chembook cartoonbook ]
    isThem = true
  ;

Свойство booklist содержит список отдельных книг, объекты для которых мы определили заранее. Каждый раз, когда игрок берет книгу, метод doTake выберет первый объект из списка booklist и действует так, как если бы игрок взял именно этот объект. Таким образом, игрок может просто дать команду "взять книгу", и игра сама выберет книгу вместо него. Метод verDoTake проверяет, остались ли еще объекты в списке booklist, и выводит соответствующее сообщение, если их не осталось. Кроме того, мы также переписали свойство ldesc так, чтобы игроку сразу стало ясно, что ему следует попытаться взять книгу.


Набор одинаковых предметов

Еще одной из вариаций на ту же тему является набор неотличимых друг от друга объектов. Например, мы хотим реализовать спичечный коробок, полный спичек. Игрок должен иметь возможность взять одну спичку из коробка, чтобы зажечь ее.

Однако, поскольку TADS требует индивидуального набора лексических свойств для каждого объекта, нам придется-таки наложить некоторые ограничения на действия игрока. В частности, в каждый конкретный момент времени игрок сможет держать в руках только одну спичку - все остальные спички должны оставаться в коробке.

С точки зрения реализации это означает, что нам нужен всего один объект, соответствующий спичке. У этого объекта может быть два возможных состояния: он существует или его не существует. Для проверки состояния объекта мы будем использовать его свойство location; если значение этого свойства равно nil, объекта не существует, в противном случае он присутствует в игре. Когда спичка не существует, игрок сможет взять спичку из коробка, при этом спичка будет помещена в инвентарь игрока и, разумеется, в игру. Пока спичка присутствует в игре, игрок не сможет взять еще одну спичку из коробка. Однако, когда спичка сгорит, она будет удалена из игры, и игрок сможет взять еще одну спичку.

Для коробка реализуем счетчик оставшихся спичек. Когда игрок будет пытаться взять спичку, мы сначала проверим, есть ли они еще в коробке; если да, то мы проверим, не была ли уже взята спичка (иными словами, не присутствует ли уже в игре объект, соответствующий отдельной спичке).

  matchbook: item
    location = bookstore
    noun = 'коробок' 'коробка' 'коробку' 'коробком' 'коробке' 'коробку#d' 'коробком#t'
    adjective='спичечный' 'спичечного' 'спичечному' 'спичечным' 'спичечном' 'спичечному#d' 'спичечным#t'
    sdesc = "спичечный коробок"
    rdesc = "спичечного коробка"
    ddesc = "спичечному коробку"
    vdesc = "спичечный коробок"
    tdesc = "спичечным коробком"
    pdesc = "спичечном коробке"
    matchcount = 4  /* количество спичек, оставщшихся в коробке */
    ldesc =
    {
        if ( self.matchcount > 0 )
            "В коробке лежит << self.matchcount >> 
            спич<<self.matchcount = 1 ? "ка." : "ки. " >>";
        else
            "Коробок пуст. ";
    }
    isHim = true
  ;

  match: item
    noun = 'спичка' 'спички' 'спичке' 'спичку' 'спичкой' 'спичке#d' 'спичкой#t'
    adjective = 'отдельная' 'отдельной' 'отдельную' 'отдельной#d' 'отдельной#t'
    sdesc = "отдельная спичка"
    rdesc = "отдельной спички"
    ddesc = "отдельной спичке"
    vdesc = "отдельную спичку"
    tdesc = "отдельной спичкой"
    pdesc = "отдельной спичке"
    ldesc =
    {
      if ( self.isBurning )
        "В данный момент она горит.";
      else
        "Это просто спичка.";
    }
    verDoLight( actor ) =
    {
      if ( self.isBurning )
        "Спичка уже горит!";
    }
    burnFuse =
    {
      "\bСпичка догорает. Ты бросаешь ее,
      и она распадается на кучку пепла где-то на полу.";
      self.isBurning := nil;
      self.moveInto( nil );             /* спички больше нет */
    }
    doLight( actor ) =
    {
      "Спичка загорается.";
      self.isBurning := true;
      notify( self, &burnFuse, 2 );
    }
    isHer = true
 ;
  compoundWord 'в' 'коробке' 'вкоробке'    /* Это необходимо, чтобы корректно отрабатывать 
                                               команду вроде "взять спичку в коробке" */
  fakematch: fixeditem
    noun = 'спичка' 'спички' 'спичке' 'спичку' 'спичкой' 'спичке#d' 'спичкой#t'
    adjective = 'вкоробке' 'вкоробке#r'
    sdesc = "спичка в коробке"
    rdesc = "спички в коробке"
    ddesc = "спичке в коробке"
    vdesc = "спичку в коробке"
    tdesc = "спичкой в коробке"
    pdesc = "спичке в коробке"
    location = matchbook
    verDoTake( actor ) =
    {
      if ( matchbook.matchcount = 0 )
        "Коробок пуст.";
      else if ( match.location <> nil )
        "Ты уже взял спичку.";
    }
    verDoLight( actor ) =
    {
      "Сначала возьми спичку из коробка.";
    }
    doTake( actor ) =
    {
      "Ты вытряхиваешь на ладонь спичку из коробка.";
      match.moveInto( actor );      /* move a match into inventory */
      matchbook.matchcount - ;     /* one less match */
    }
    isHer = true
  ;

Объект fakematch позволяет игроку обращаться к спичке в командах, даже когда объекта match в игре нет (его свойство location равно nil). Для объекта fakematch определены методы verDoTake и doTake, благодаря которым игрок может взять спичку из коробка.

Обратите внимание на один недостаток такой реализации спичек. Может случиться так, что у игрока не будет в руках спички, но взять еще одну из коробка он не сможет. Это может смутить игрока. Например, игрок берет спичку, потом бросает ее из-за того, что и так держит в руках слишком много вещей, а потом перейдет в другую комнату. С точки зрения игрока, никаких спичек у него нет - ту, которая у него была, он бросил, причем он с тех пор мог даже забыть, где. Однако он не сможет взять еще одну спичку, поскольку предыдущая по-прежнему находится в игре. Решение этой проблемы нетривиально. Допустим, можно перемещать спичку в инвентарь игрока и из другой комнаты. Это будет менее странным для одних игроков, но может смутить других, которые как раз хорошо запомнили, где они бросили спичку, и впоследствии захотят вернуться за ней - эта спичка исчезнет после того, как объект, соответствующий отдельной спичке, будет помещен в инвентарь игрока.

Примечание переводчика: одним из способов предотвращения возникновения вышеописанной ситуации является запрет игроку выпускать спичку из рук, а также явное извещение игрока о том, что спичка потерялась, когда он ее бросает. При этом важно учесть и другие команды, которые приводят к удалению предмета из инвентаря игрока - например, "положить в", "положить на", "швырнуть".


Деньги

Вписать естественным образом деньги в TADS-игру затруднительно по ряду причин. Если вам все-таки обязательно нужно реализовать деньги в игре, то это можно сделать, используя те же базовые приемы, что и для спичек. Единственное отличие для денег состоит в том, что, скорее всего, понадобится определить некоторые новые глаголы, а также реализовать возможность получения игроком денег (увеличения их количества в его инвентаре).

Необходимость определения новых глаголов будет зависеть от конкретной игры. Например, можно определить глаголы "оплатить" и "купить" для покупки вещей. Основной причиной, по которой они вам понадобятся, является отсутствие возможности у игрока (или, по крайней мере, простой реализации такой возможности) указывать в команде количество денег. Из-за этого вам понадобится определить своего рода протокол для покупки предметов. Например, вы определите локацию-магазин, и создадите в ней персонажа-продавца. Чтобы что-нибудь купить, игрок просит продавца дать ему соответствующий предмет (типа "продавец, дай топор"). Продавец в ответ называет цену предмета, после чего игрок дает команду "заплатить продавцу" и получает предмет; при этом количество денег в его инвентаре уменьшается на соответствующую величину.

Если ваша игра использует подобный неочевидный протокол, очень важно будет задокументировать его для игрока. Можно поместить его описание в инструкции по игре, но, поскольку большинство игроков их не читает, гораздо лучше привести такое описание в самой игре. Существует множество способов для этого; например, можно просто перечислить нужные для совершения покупок команды в тот момент, когда игрок первый раз заходит в магазин. Другой способ - выводить инструкции в качестве сообщений об ошибках; скажем, когда игрок пытается взять объект в магазине, можно вывести сообщение: "Пожалуйста, обращайтесь к продавцу; например, наберите 'продавец, дай мне вазу'."

Еще лучше избегать подобных ситуаций в принципе. Если у вас не получается реализовать что-либо естественным образом, без использования громоздких последовательностей команд, лучше отказаться от этого вовсе. Всегда помните, что абсолютная реалистичность не слишком важна в текстовой игре; единственное, что имеет значение - это то, насколько игроку будет интересно играть.

Примечание переводчика: на самом деле, значительно проще реализовать вышеописанный протокол командой "купить предмет", при которой предмет будет автоматически перемещен в инвентарь игрока, а сумма денег, имеющаяся у игрока в наличии, уменьшена на стоимость купленной вещи. Тем не менее, утверждение автора, что неочевидных последовательностей команд и коммуникационных протоколов в игре лучше избегать, остается справедливым.


Персонажи

Одним из наиболее эффективных приемов для придания глубины и живости игровому миру является использование персонажей. Ранние приключенческие игры зачастую казались пустыми и статичными; игрок перемещался по огромному лабиринту пустынных пещер. В более современных играх широко используются персонажи (которых мы начиная с этого момента будем называть "актерами"), которые перемещаются по комнатам игры, общаясь с игроком и самостоятельно выполняя различные действия. При качественной реализации актеры несказанно обогащают игру, делая весь игровой мир более живым и "настоящим".

Актеры являются одними из наиболее сложных элементов TADS-игры, поскольку они действуют на большем количестве уровней, чем большинство других объектов. Прежде всего это касается взаимодействия с игроком: актер, как правило, может взаимодействовать с игроком, как посредством общения (игрок может пытаться командовать актером, а также задавать ему вопросы), так и посредством действий (например, нападение или обмен предметами). В общем, способов взаимодействия существует великое множество, и TADS позволяет обеспечить ту степень интерактивности актера, какая вам потребуется - от самой высокой до полной неинтерактивности. Во-вторых, актеры часто совершают самостоятельные действия. Иногда это значит, что актер выполняет некую заранее определенную последовательность действий. В других случаях актер просто реагирует на действия игрока; например, актер может большую часть игры следовать за игроком, время от времени комментируя действия последнего. Однако большинство актеров являются комбинациями обоих подходов - большую часть времени они просто реагируют на действия игрока, и выполняют некую последовательность действий при определенных условиях (например, наступлении какого-либо события). Наконец, в-третьих, в некоторых случаях актеры обладают "памятью" и сведениями о различных частях игры. Некоторые из действий, которые выполняет актер, будут зависеть от того, что ранее делал игрок.

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

При реализации актера вам потребуется определить два метода, которые, как правило, не требуется определять для других объектов. Первый - это метод actorAction, который обеспечивает большую часть интерактивного взаимодействия актера с игроком; он определяет, в какой степени актер будет реагировать на команды игрока. Второй необходимый метод - это "демон" актера. Как вы, наверное, помните из предыдущих разделов, демоном называется функция или метод, вызываемый после каждого хода (т. е. программа-интерпретатор TADS вызывает демон каждый раз после окончания обработки очередной команды игрока). Именно этот демон обеспечивает самостоятельные действия актера; поскольку вызов демона происходит после каждого хода, актер может совершать самостоятельные действия также после каждого хода.

Если вы играли в Ditch Day Drifter (в русском варианте - Блуждания в Окопный День), вы наверняка вспомните несколько актеров, присутствовавших в ней. Одним из них, довольно сильно ограниченным в своих возможностях, является охранник, сидящий перед дверью в лабиринт туннелей: он практически ничего не делает, лишь препятствует входу игрока в туннель. Большая часть общения с охранником сводится к тому, что игрок предлагает ему объекты. Например, можно попытаться подкупить охранника деньгами или предложить ему что-нибудь выпить. Демон охранника также практически ничего не делает, кроме вывода выбранного случайным образом сообщения (из предварительно определенного списка сообщений) после каждого хода. Примером более сложного актера является Ллойд, страховой робот-агент. Ллойд способен воспринимать объекты (например, можно купить у него страховой полис), и игрок может также говорить с ним, хотя и в ограниченных пределах (его можно спросить о полисе). Как и для охранника, демон Ллойда выводит случайное сообщение после каждого хода. Кроме того, Ллойд перемещается из комнаты в комнату вместе с игроком. Ллойд также реагирует на определенные события и локации, а также обладает памятью: заплатив раз по страховому полису, он не будет больше платить по такому же страховому случаю.

Рассмотрим пример реализации актера. Начнем с простого "актера-препятствия", наподобие уже упомянутого охранника из Блужданий. Это довольно распространенный элемент приключенческих игр: персонаж, стоящий на пути к цели. В данном примере мы реализуем секретаршу, которая не дает игроку войти в некий офис. Пока она бодрствует, попасть в это помещение не удастся.

  receptionist: Actor
    noun = 'секретарша' 'секретарши' 'секретарше' 'секретаршу' 'секретаршей' 'секретарше#d' 'секретаршей#t'
           'секретарь' 'секретаря' 'секретарю' 'секретарем' 'секретаршем' 'секретарю#d' 'секретарем#t'
    sdesc = "секретарша"
    rdesc = "секретарши"
    ddesc = "секретарше"
    vdesc = "секретаршу"
    tdesc = "секретаршей"
    pdesc = "секретарше"
    isHer = true
    isawake = true
    ldesc =
    {
        "Секретаршей здесь работает женщина, выглядящая весьма,
        как бы это сказать, крепкой особой. Она носит высокую прическу 
        и очки с толстыми стеклами. Она напоминает тебе твою учительницу
        начальных классов, которая была просто помешана на дисциплине. ";
        if ( self.isawake )
            "Секретарша подозрительно косится на тебя поверх бумаг, которые 
            она якобы просматривает.";
        else
            "Секретарша крепко спит, уронив голову на стол.";
    }
    location = lobby
    actorDesc =
    {
        if ( self.isawake )
            " За столом рядом с дверью сидит секретарша, которая 
            с подозрением косится на тебя. ";
        else
            " За столом рядом с дверью крепко спит секретарша. ";
    }
    actorDaemon =
    {
        if ( self.location <> Me.location or not self.isawake ) return;
        "\b";
        switch( rand( 5 ) )
        {
         case 1:
            " Секретарша натачивает карандаши. ";
            break;
         case 2:
            "Секретарша просматривает личную почту сотрудников, 
            пытаясь прочесть на свет содержимое конвертов.";
            break;
         case 3:
            "Секретарша изучает личные дела.";
            break;
         case 4:
            "Секретарша снимает трубку зазвонившего телефона и
            тут же бросает ее, злорадно хмыкнув. ";
        break;
         case 5:
            "Секретарша перекладывает бумаги на столе.";
            break;
         }
     }
   ;

  lobby: room
    enterRoom( actor ) =
    {
      if ( not self.isseen )
        notify( receptionist, &actorDaemon, 0 );
      pass enterRoom;
    }
    sdesc = "Приемная"
    ldesc = "Ты находишься в большой приемной, украшенной дорогими
    с виду абстрактными картинами и большим количеством хромированных
    деталей интерьера, натертых до блеска. Рядом с дверью кабинета,
    расположенной к востоку, стоит большой стол. Выход из приемной
    находится к западу. "
    west = outsideBuilding
    out = outsideBuilding
    east =
    {
      if ( receptionist.isawake )
      {
        "Ты с небрежным видом пытаешься пройти мимо секретарши, 
        игнорируя ее, как если бы ты и был хозяином кабинета, 
        но ее этим не обмануть: она вскакивает и с неожиданной силой
        отталкивает тебя от двери. Довольная тем, что ты не смог 
        прошмыгнуть мимо нее, она возвращается к своей работе.";
        return( nil );
      }
      else
        return( office );
    }
  ;

Вы, конечно, заметили, что реализация этой загадки во многом напоминает реализацию двери или другого препятствия, с той лишь разницей, что само препятствие здесь несколько более сложное. Однако базовые действия, которые выполняются, чтобы не дать игроку пройти мимо секретарши, точно такие же, как и для любых других препятствий: в свойстве-направлении мы проверяем, бодрствует ли секретарша (свойство isawake), и если да, то возвращаем nil (естественно, после вывода соответствующего сообщения), не давая тем самым игроку пройти.

Основными особенностями, характерными именно для актеров, являются методы actorDesc и actorDaemon. actorDesc - это специальное свойство, которое следует определять для всех актеров; он вызывается методом общего класса room, который отвечает за вывод развернутого описания комнаты. После вывода описания самой комнаты процедура, осуцществляющая этот вывод, выполнит вызовы методов actorDesc для всех актеров, присутствующих в комнате (за исключением актера, соответствующего игроку - по умолчанию это актер Me). Метод actorDesc предназначен для вывода короткого сообщения о том, что актер находится в комнате; в этом плане он похож на свойство ldesc, но, как правило, содержит менее развернутое описание, поскольку его назначение - проинформировать игрока о присутствии актера в комнате, а не описывать актера в подробностях. Обычно в этом методе указывается, что актер находится в комнате, а также чем он занимается в данный момент.

Название метода actorDaemon не является каким-либо специальным или зарезервированным - его можно назвать и по-другому. Это демон, который "оживляет" актера, выполняя некие действия после каждого хода. В нашем примере с секретаршей, которая на самом деле практически ничего и не делает, этот метод просто выводит случайно выбранное сообщение после каждого хода. Обратите внимание, что если игрок (в нашем примере ему соответствует объект Me) и секретарша (объект receptionist) находятся в разных комнатах, то метод сразу прекращает свою работу; совершенно очевидно, что вывод сообщений о секретарше не имеет смысла, если игрок находится в другой локации и не может ее видеть.

Также обратите внимание, что метод actorDaemon не запускается автоматически, а должен быть инициализирован явным образом. В нашем случае мы выполняем активацию actorDaemon в тот момент, когда игрок в первый раз попадает в приемную (объект lobby). Метод enterRoom этого объекта, вызываемый каждый раз, когда игрок входит в комнату (этот метод определен в advr.t для класса room), проверяет, посещал ли уже игрок приемную (путем контроля значения свойства isseen, которое автоматически устанавливается равным true, когда игрок впервые попадает в соответствующую комнату). Если игрок в приемной еще не был, этот метод осуществляет вызов стандартной функции notify() для инициализации демона actorDaemon. Также обратите внимание, что метод enterRoom завершает свою работу инструкцией pass, которая передает управление одноименному методу enterRoom, наследуемому от родительского класса room.


Следование за игроком

Существует несколько основных типов "блуждающих" актеров. Актеры первого типа следуют за игроком, как это делает, например, робот-страховщик Ллойд в Блужданиях в Окопный День. Реализация таких актеров сравнительно несложна, поскольку все, что нужно сделать - это осуществить проверку в процессе выполнения демона, находятся ли актер и игрок в одной комнате, и если нет, то переместить актера в комнату, где находится игрок.

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

Ниже приведен пример базового метода actorDaemon, который обеспечит следование актера за игроком.

  actorDaemon =
  {
    if ( self.location = Me.location )
    {
        switch( rand( 5 ) )
        {
         case 1:
             "Ллойд напевает одну из своих любимых песенок страховщиков.";
             break;
         /* и т. д. - вывод других случайно выбираемых сообщений... */
         }
    }
    else
    {
        self.moveInto( Me.location );
        "Ллойд вкатывается в комнату, проверяя, 
        насколько здесь безопасно.";
    }
  }

Обратите внимание, что эта процедура должна быть проинициализирована путем вызова функции notify(), чтобы выполняться после каждого хода (см. предыдущий пример с секретаршей).

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

В реальной игре вы, вероятно, расширили бы данный демон таким образом, чтобы он выполнял определенные действия, когда актер находится в определенных местах. Например, в Блужданиях в Окопный День дополнительно обеспечивается вывод специальных сообщений, когда Ллойд перемещается в труднодоступных для робота местах, как то пробирается через узкий лаз или карабкается по веревке.


Перемещение по фиксированному маршруту

"Блуждающие" актеры второго типа перемещаются по фиксированному маршруту. Реализация актеров этого типа гораздо сложнее, чем сопровождающих игрока; вместо того, чтобы использовать местоположение игрока в демоне, отвечающем за перемещение актера, нам необходимо определить список комнат, которые актер будет посещать в определенном порядке, после чего демон перемещения будет выбирать очередную локацию из этого списка.

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

В качестве примера мы рассмотрим реализацию робота-уборщика из игры Deep Space Drifter, который перемещается по помещениям космической станции в определенном порядке.

  vacRobot: Actor
    sdesc = "робот-уборщик"
    rdesc = "робота-уборщика"
    ddesc = "роботу-уборщику"
    vdesc = "робота-уборщика"
    tdesc = "роботом-уборщиком"
    pdesc = "роботе-уборщике"
    noun = 'робот' 'робота' 'роботу' 'роботом' 'роботе' 'роботу#d' 'роботом#t'
           'уборщик' 'уборщика' 'уборщику' 'уборщиком' 'уборщике' 'уборщику#d' 'уборщиком#t'
           'робот-уборщик' 'робота-уборщика' 'роботу-уборщику' 'роботом-уборщиком'
           'роботе-уборщике' 'роботу-уборщику#d' 'роботом-уборщиком#t'
    adjective = 'уборочный' 'уборочного' 'уборочному' 'уборочным' 'уборочном' 
                'уборочному#d' 'уборочным#t'
    isHim = true
    location = stationMain
    tracklist = [ 'юго-восток' bedroomEast 'восток' bathroom 'запад'
                  bedroomWest 'северо-восток' stationMain 'север'
                  stationKitchen 'юг' stationMain ]
    trackpos = 1
    moveCounter = 0
    actorDaemon =
    {
        if ( not self.isActive ) return;          /* если робот выключен, ничего не делаем */
        self.moveCounter++;
        if ( self.moveCounter = 3 )          /* по истечении трех ходов перемещаемся в следующую комнату */
        {
            self.moveCounter := 0 ;
            if ( self.location = Me.location )
                "\bРобот перемещается на <<self.tracklist[self.trackpos]>>.";
            self.moveInto( self.tracklist[ self.trackpos + 1 ] );
            if ( self.location = Me.location )
                "\bНеожиданно в комнату вкатывается робот-уборщик и начинает с шумом 
                перемещаться по помещению, пылесося пол и стирая пыль.";

                 /* перемещаемся в следующую комнату списка; если достигнут конец списка, возвращаемся
                    к его началу */
                 self.trackpos += 2;
                 if ( self.trackpos > length( self.tracklist ) ) self.trackpos := 1;
         }
         else
         {
             /* на данном ходу мы не перемещаемся, поэтому просто выводим сообщение */
             if ( self.location = Me.location )
               "\bУборочный робот продолжает с шумом пылесосить комнату.";
         }
     }
   ;

Список комнат, посещаемых роботом, определен в свойстве tracklist. Этот список выглядет несколько непривычно, поскольку содержит не только объекты, но и строковые константы (в одинарных кавычках). Это вызвано тем, что элементы списка всегда анализируются парами: первый элемент пары - это строка, выводимая для того, чтобы указать, в каком направлении (через какой выход) робот покидает комнату; второй элемент пары определяет локацию, куда робот перемещается из текущей комнаты. Так, первая пара содержит строку 'юго-восток' и объект bedroomEast; из текущей комнаты робот переместится на юго-восток, в восточную спальню (которой соответствует этот объект).

Демон actorDaemon по-прежнему выполняет всю работу по "оживлению" актера, однако он несколько более сложен по сравнению с первыми двумя примерами. Сначала он проверяет, включен ли робот; если нет, демон заканчивает свою работу. Затем происходит увеличение на единицу значения счетчика числа ходов, в течение которых робот оставался в одной и той же комнате; когда он становится равным трем, робот двигается дальше, в противном случае робот остается на прежнем месте.

Если робот должен быть перемещен, его демон прежде всего обнуляет счетчик ходов. Затем демон проверяет, находится ли игрок в той же комнате, что и робот; если да, то выводится сообщение, информирующее игрока о том, что робот покидает комнату, и о направлении, в котором он движется (это направление определяется в соответствии с текущим элементом списка tracklist, как было описано выше). Затем происходит собственно перемещение робота в следующую комнату (которая опять же определяется списком tracklist). Далее, если игрок находится в комнате, куда перемещается робот, демон выводит сообщение, информирующее игрока о прибытии робота-уборщика. Наконец, мы увеличиваем счетчик позиции маршрута для свойства trackpos (на 2, поскольку элементы списка выбираются попарно). Если значение счетчика вышло за пределы длины списка, мы сбрасываем счетчик в единицу.

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


Перемещение по случайному маршруту

"Блуждающие" актеры третьего типа перемещаются по комнатам игры случайным образом. Этот тип более сложен, чем предыдущие.

Основной сложностью при реализации таких актеров является то, что мы должны избегать выполнения свойств-направлений, если они не просто ссылаются на комнату, а являются методами. Это необходимо из-за того, что в противном случае возможен вывод странных сообщений и другие побочные эффекты (например, гибель игрока или изменение состояния игры). Вспомним, например, один из предыдущих примеров с лестницей, ведущей в подвал - вряд ли вам нужно, чтобы сообщение о рухнувшей лестнице выводилось бы, если соответствующим выходом из помещения воспользовался один из "блуждающих" актеров.

Самым надежным способом избежать подобных нежелательных побочных эффектов является отказ от использования актером свойств-направлений, которые не являются простыми объектами. В TADS имеется специальная встроенная функция - proptype, которая позволяет определить тип свойства без его выполнения. Эта функция позволит нам определить, является ли свойство-направление простым объектом или методом.

Если исключить механизм выбора комнаты, куда должен быть перемещен актер, "блуждающие" актеры ничем не отличаются от актеров, перемещающихся по фиксированному маршруту. Демон, отвечающий за движение актера, генерирует сообщения одинаково в обоих случаях, проверяя, не находится ли актер в настоящее время в одной комнате с игроком и что он делает на данном ходу - покидает комнату, перемещается в нее, или ни то, ни другое.

  actorDaemon =
  {
    local dir, dirnum;
    local tries;
    local newloc;
    for ( tries := 1 ; tries < 50 ; tries++ )
    {
        dirnum := rand( 6 );
        dir := [ &north &south &east &west &up &down ][ dirnum ];
        if ( proptype( self.location, dir ) = 2 /* тип свойства-направления - объект */ )
        {
            newloc := self.location.( dir );
            if ( not isclass( newloc, room ) ) continue;
            if ( self.location = Me.location )
                "Робот перемещается 
                 <<[ 'на север' 'на юг' 'на восток' 'на запад' 'вверх' 'вниз' ][ dirnum ]>>.";
            self.moveInto( newloc );
            if ( self.location = Me.location )
                 "В комнату вкатывается робот-уборщик и начинает с шумом
                  пылесосить пол.";
         }
      }
   }

Строка, в которой устанавливается значение локальной переменной dir, вероятно, требует дополнительного рассмотрения, поскольку здесь применена одна из хитростей TADS-программирования. Первая часть выражения в правой части строки - это список адресов свойств объекта; это не сами свойства, а своего рода ссылки на них (о чем говорит знак & перед ними), которые могут впоследствии быть использованы для получения непосредственно самих свойств. Мы выбираем из этого списка один элемент путем задания случайного индекса в пределах от 1 до 6 (переменная dirnum). При этом в переменную dir записывается адресная ссылка на свойство, которую мы впоследствии сможем использовать. Обратите внимание, что точно так же можно было бы включить в список свойства, соответствующие другим направлениям (северо-восток, северо-запад, юго-восток, юго-запад); мы не сделали этого, чтобы пример выглядел менее громоздким.

В следующей строке мы используем встроенную функцию proptype() для того, чтобы проверить, является ли выбранное свойство-направление (ссылка на которое содержится в переменной dir) объектом. Если это не так, мы продолжаем перебор свойств-направлений в цикле.

Если же выбранное свойство действительно содержит объект, мы прежде всего сохраняем этот объект в локальной переменной newloc. Затем мы выполняем еще одну проверку: выясняем, принадлежит ли данный объект классу room. Связано это с тем, что свойство-направление может указывать не только на другие комнаты, но и на объекты типа obstacle (например, дверь; реализация дверей описана ранее в этой главе). Если свойство-направление указывает на такой объект, а не на другую комнату, мы пропускаем его и продолжаем цикл.

После того, как newloc окажется будет ссылаться на объект класса room, мы осуществим перемещение актера в комнату newloc точно так же, как это делалось для актера, путешествующего по фиксированному маршруту; в частности, для указания направления, в котором покинул комнату актер, используется точно такой же прием со списком направлений и индексом.

Вы можете задаться вопросом, зачем нам понадобился цикл со счетчиком tries, который завершается после 50-ти итераций; на самом деле это просто страховочная мера для того, чтобы предотвратить зацикливание демона. Связано это с тем, что (особенно на этапе разработки игры) вполне вероятна ситуация, что робот попадет в комнату, из которой не сможет выйти. Вспомните, что робот может использовать только выходы (свойства-направления), которые явным образом указывают на объекты класса room; если для какого-нибудь из помещений мы определим все свойства-направления как методы или как объект-дверь, то робот не сможет найти выход; если мы не ограничим максимальное число циклов, то в такой комнате произойдет зацикливание метода, и игра "зависнет".

В реальной игре вам может потребоваться определить дополнительные обработчики, которые позволят "блуждающему" актеру воспользоваться некоторыми выходами, которые являются методами и вследствие этого недоступны этому актеру. Одним из способов для достижения этого является реализация внутри демона дополнительной проверки на предмет того, находится ли актер в помещении с такими "специальными" выходами, и, если это так, разрешить актеру использовать этот выход. Другой способ состоит в том, чтобы определить для некоторых помещений дополнительный набор свойств-направлений (таких как robotEast), которые будут указывать направления, доступные только для робота; демон перемещения в этом случае будет сначала проверять именно эти "направления для робота", и будет использовать обычные свойства-направления только в том случае, если дополнительный набор направлений не определен.

Одним из изменений, которые вы можете внести в эту концепцию случайного блуждания по всей игре, является ограничение перемещений актера определенным набором комнат. Здесь также возможен целый ряд способов реализации. Простейшим является определение класса помещений, которые актер может посещать; при каждом перемещении актера будет выполняться проверка, принадлежит ли данная комната этому классу.

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


Более сложные скрипты актеров

В некоторых играх вам может потребоваться, чтобы ваши актеры самостоятельно выполняли более сложные последовательности действий, чем рассмотренные нами ранее. Одной из типичных моделей поведения актера является выполнение им определенных действий в определенной комнате или в случае выполнения ряда условий. Например, вам может потребоваться создать вора, который подбирал бы сокровища, оставленные в одном из помещений лабиринта. Для этого нам прежде всего потребуется создать класс для комнат лабиринта; пусть этот класс носит название mazeroom:

  mazeroom: room
  ;

Точно таким эе образом мы реализуем класс объектов, являющихся сокровищами:

  treasure: item
  ;

Обратите внимание, что единственная цель данных классов - служить признаком, на основании которого мы можем определить, является ли данная локация помещением в лабиринте, а объект - сокровищем. Никаких дополнительных реакций в этих классах не определеяется.

Затем мы реализуем в соответствующем демоне актера проверку на наличие объектов-сокровищ в помещении лабиринта, в котором находится демон. Соответствующий код мы добавим в конце того же демона, который был реализован нами для "блуждающего" актера из предыдущего примера; мы не будем воспроизводить здесь весь этот метод. Приведенные ниже инструкции следует вставить после цикла for, занимающего большую часть вышеуказанного демона.

  if ( isclass( self.location, mazeroom ) )
  {
        /* Проверить объекты в текущей комнате */
        local cont := self.location.contents;
        local len  := length( cont );
        local found := nil;
        local i;
        for ( i := 1 ; i <= len ; ++i )
        {
            if ( isclass( cont[ i ], treasure ) )
            {
              found := true;
              cont[ i ].moveInto( self );
            }
        }
        if ( found and Me.location = self.location )
            "Ты обнаруживаешь, что вор кое-что здесь прихватил.";
    }

Это довольно сложный пример того, как реализовать выполнение актерами определенных действий при соответствующих условиях. Более простой пример - попытка робота Ллойда из игры Блуждания в Окопный День продать страховой полис спящему охраннику; она реализована простым сообщением, выводящимся, когда Ллойд входит в соответствующую комнату.

У вас может возникнуть желание реализовать для некоторых актеров гораздо более сложные скрипты, которые выполняли бы нетривиальные последовательности действий, а не просто перемещали бы актера. Например, в игре у вас может действовать актер, который будет жить собственной жизнью, посещая различные помещения и делая там определенные вещи для достижения собственных целей. Лучше всего реализовать такого актера, используя так называемый "механизм состояний". Одним из простейших способов реализации подобного механизма является использование числа для обозначения текущего состояния; с каждым ходом это число увеличивается на единицу, а внутри демона актера будет использоваться инструкция switch для выполнения того или иного действия в зависимости от текущего значения счетчика состояний.

Представим, например, что вы хотите реализовать актера, который входит в здание и идет в кабинет. Если в кабинете есть кто-то еще, он просто с нетерпением ждет, пока этот кто-то уйдет; когда он окажется в одиночестве, он откроет ключом сейф, уронив ключ в том же помещении, возьмет что-то из сейфа и покинет здание.

Представим набор состояний при помощи номеров. Состояние № 1 будет означать, что актер находится вне здания; иными словами, в этом состоянии актер входит в здание. Состояние № 2 будет соответствовать фойе здания; актер будет перемещаться в кабинет. В состоянии № 3 будет выполняться проверка, есть ли в кабинете еще кто-то, кроме актера; если да, то актер останется в состоянии № 3, изображая нетерпеливое ожидание. Как только он останется один, он извлечет ключ и откроет сейф. В состоянии № 4 актер достанет из сейфа некий объект и выйдет из помещения, вернувшись назад в фойе. В состоянии № 5 актер покинет здание.

Для реализации такого поведения актера могут использоваться следующие методы.

  actorMessage( msg ) =
  {
    if ( Me.location = self.location )
    {
        "\b";
        say( msg );
    }
    actorMove( newloc, todir, fromdir ) =
    {
      if ( Me.location = self.location )
        "<<self.sdesc>> выходит через <<self.todir>> выход. ";
      self.moveInto( newloc );
      if ( Me.location = self.location )
        "<<self.sdesc>> входит в комнату с <<self.fromdir>>. ";
    }
    actorState = 1
    actorDaemon =
    {
      switch( self.actorState++ )
      {
         case 1:
            self.actorMove( frontHall, 'северный', 'юга' );
            break;
         case 2:
            self.actorMove( study, 'восточный', 'запада' );
            break;
         case 3:
            if ( self.location = Me.location and not Me.ishidden )
            {
                "Джек нетерпеливо смотрит на часы.";
                self.actorState := 3;         /* остаемся в состоянии № 3 */
            }
            else
            {
                 actorMessage( 'Джек осматривается, проверяя, один ли он в комнате.
                  Убедившись в этом, он шарит по карманам и достает ключ, которым 
                  он отпирает сейф. Воровато оглядываясь, он вытаскивает что-то из сейфа.
                  Он настолько нервничает, что, уронив ключ на пол, даже не замечает
                  этого.');
                 safeKey.moveInto( self.location );
            }
            break;
         case 4:
            self.actorMove( frontHall, 'западный', 'востока' );
            break;
         case 5:
            self.actorMove( outsideBuilding, 'южный', 'севера' );
            break;
        }
    }

Обратите внимание, что методы actorMove и actorMessage обеспечивают удобный механизм для отображения сообщений в зависимости от наличия игрока в одной комнате с актером.

Разумеется, у вас может возникнуть желание дополнить этот скрипт, придав появлению Джека в игре некое логичное и плавное завершение. Используя описанный выше базовый принцип, можно реализовать актеров любой сложности. Обратите внимание, что актер не обязательно должен проходить все состояния последовательно - он вполне может "перескакивать" через некоторые из них; для этого достаточно просто присвоить переменной, "отвечающей" за состояние актера (в нашем случае actorState), номер, соответствующий требуему состоянию актера. Это позволит актеру действовать более "разумно": вместо простого выполнения предписанной последовательности действий без учета происходящего в игре актер будет делать разные вещи в зависимости от выполнения ряда условий. В нашем рассмотренном выше примере единственной зависимостью от сложившихся в игре условий является то, что актер не будет открывать сейф до тех пор, пока не останется один (или не будет уверен, что он остался один) в комнате; реализация более сложных проверок - лишь дело техники.

При выборе механизма реализации актера вам следует учитывать, какие действия актер должен выполнять и каковы его основные функции в игре. Если ваш актер в основном движется по некоему маршруту и лишь от случая к случаю выполняет какие-либо специфические действия в некоторых из комнат или при выполнении каких-либо условий, проще всего реализовать его при помощи рассмотренных нами ранее алгоритмов перемещения по фиксированному или произволдьному маршруту (в зависимости от конкретной ситуации); при этом надо только выполнять проверку на выполнение специфических условий до и/или после перемещения. С другой стороны, если актер выполняет разнообразные действия в достаточно большом количестве комнат, проще использовать механизм состояний.


Диалог с актерами

Одной из основных сфер взаимодействия с актерами в приключенческих играх является разговор с актерами и, в частности, задавание вопросов. TADS несколько ограничен в области искусственного интеллекта; к сожалению, здесь нет возможности задавать актерам вопросы общего порядка типа "Что вы думаете о роли средств массовой информации в грядущих выборах?" или "Почему небо голубое?" Все, что возможно в TADS - это задавать актеру вопросы относительно конкретных предметов в игре. Кроме того, все, что игрок может - это задать вопрос об объекте в целом; нельзя задать вопрос типа "Почему предохранитель находится вне челнока?", а только дать команду "спросить Ллойда о предохранителе".

Другой способ взаимодействия - это сказать актеру что-либо. Как и в случае с вопросами, TADS не позволяет игроку сообщить актеру что-либо сложное. Все, что игрок может сказать - это нечто вроде "рассказать Ллойду о документе".

Примечание переводчика: речь в данном случае, разумеется, не идет о системах общения с персонажами, построенных на основе меню. Спор о преимуществах и недостатках традиционной и менюшной систем общения продолжается до сих пор. Реализация менюшной системы в TADS осложняется тем, что в стандартных библиотеках не предусмотрено никаких средств для ее построения.

С точки зрения дизайна игры эти ограничения сильно упрощают вашу задачу. Реализация команд "спросить" и "рассказать" в принципе ничем не отличается от реализации других глаголов; ниже приведен пример метода doAskAbout (для глагола "спросить") для актера (в данном случае уже знакомого нам робота Ллойда из Блужданий в Окопный День).

  verDoAskAbout( actor, iobj ) = {}
  doAskAbout( actor, iobj ) =
  {
    switch( iobj ) /* косвенный объект - это то, о чем спрашивают */
    {
     case insurancePolicy:
        if ( insurancePolicy.isbought )
            "\"Условия страховки очень сложные, 
            но будьте уверены - это очень хороший полис.\"";
        else
            "\"Великолепный полис! И стоит всего доллар!\"";
        break;
      default:
        "\"Ничего не могу сказать об этом.\"";
      }
    }

Таким же образом можно добавить и другие объекты, чтобы увеличить "объем знаний" актера.

Обратите внимание на один тонкий момент: передаваемый методам параметр actor в данном случае - это персонаж, задавший вопрос; в нашем случае это игрок (по умолчанию ему соответствует актер Me). Актеру, которому задают вопрос, соответствует объект self, поскольку эот актер является прямым объектом для глагола "спросить". Кроме того, чтобы актеру вообще можно было задавать вопросы, нам потребовалось определить для него "пустой" метод verDoAskAbout; по умолчанию все объекты наследуют для этого метода некое общее сообщение об ошибке.

Примечание переводчика: в текущей версии стандартных библиотек TADS в соответствующих методах (AskAbout и TellAbout) проверяется не сам объект, а слово, которое игрок использовал в команде (см. определение объекта movableActor в файле advr.t - обратите внимание на инструкцию lst := objwords(2);); это позволяет в ряде случаев избежать неоднозначных ситуаций (например, если в игре имеется два и более объектов, для которых совпадают некоторые из существительных; в этом случае при использовании непосредственно объектов система выбирает один из них случайным (или, по крайней мере, труднопредсказуемым для автора игры) образом, что может приводить к "ответам невпопад"). Однако базовый принцип реализации от этого не меняется - просто вместо самих объектов в инструкции switch анализируются строковые константы со словами, этим объектам соответствующими.

Для глагола "рассказать" все реализуется аналогично, только методы называются verDoTellAbout и doTellAbout. Точно таким же образом можно позволить игроку дать что-нибудь актеру, определив методы verIoGiveTo и ioGiveTo. Обратите внимание, что метод ioGiveTo должен обеспечивать перемещение соответствующего объекта в инвентарь актера (посредством инструкции dobj.moveInto( self )), если актер взял этот предмет.


Взятие предметов у актеров

По умолчанию актеры не дадут игроку взять предметы, находящиеся в их инвентаре. Это связано с тем, что все процедуры, связанные с обработкой соответствующих команд, будут вызывать метод verGrab( actor ) для всех контейнеров, содержащих объект. По умолчанию метод verGrab актера выводит сообщение о том, что актер не отдаст предмет; поскольку метод verGrab вызывается синтаксическим анализатором на этапе валидации (т. е. предварительной оценки допустимости команды), то вывод сообщения автоматически означает отказ от дальнейшей обработки.

Если вы хотите позволить игроку отбирать предметы у актера, перепишите метод verGrab этого актера. Его можно сделать пустым, чтобы актер позволял брать любые предметы, либо сделать так, чтобы этот метод выводил сообщение только для определенных объектов, не давая игроку брать только эти объекты.


Выдача команд актеру

Игрок может давать актеру команды, вводя инструкции типа "Ллойд, иди на север". Если дается такая команда, синтаксический анализатор TADS обрабатывает ее, как обычную команду, за исключением того, что в качестве актера, выполняющего команду, используется не Me (или другой объект, соответствующий игроку), а актер, указанный в команде. (На самом деле обработка действительно происходит совершенно одинаково в обоих случаях; если актер не указан, то это все равно, как если бы игрок набрал "я, иди на север").

Поскольку большая часть всей обработки осуществляется в методах - обработчиках глаголов соответствующего актера, вам не потребуется определять слишком много специальных процедур для команд, отданных актеру. Обработка таких команд осуществляется системой почти автоматически.

Однако в большинстве случаев вы сами не захотите, чтобы актер выполнял все команды игрока. Большинство ваших актеров будут достаточно самостоятельными, а некоторые даже будут стоять на пути игрока.

Чтобы обеспечить вам как автору игры управление возможностью и степенью контроля над актерами со стороны игрока, используется метод actorAction соответствующего актера; этот метод вызывается синтаксическим анализатором TADS всякий раз перед обработкой команды, отданной актеру. Если этот метод содержит инструкцию exit, обработка прерывается, и игрок не сможет командовать актером. Поскольку методу в качестве параметров передаются все объекты, фигурирующие в данной команде (глагол, прямой объект, предлог, косвенный объект), можно заставить актера выполнять только отдельные команды игрока. Для выполняемых актером команд метод не будет делать ничего; для "запрещенных" же команд будет выводиться соответствующее сообщение и выполняться инструкция exit.




В настоящее время у нас имеются все необходимые средства, как материальные, так и психологические, чтобы обеспечить полнокровную и удовлетворительную жизнь для каждого.
БУРРХУС ФРЕДЕРИК СКИННЕР (BURRHUS FREDERIC SKINNER), Валден Два (1948)


Глава шестая Содержание Приложение A