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

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

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


Глава третья


Введение в язык программирования

Система разработки текстовых приключенческих игр (TADS) предоставляет авторам игр мощный и гибкий язык программирования, хорошо приспособленный для моделирования игровых миров, лежащих в основе текстовых квестов.

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

Данная глава содержит обзор основных средств языка.


Функции

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

  showSum: function( arg1, arg2, arg3 )
  {
    "Сумма равна: ";
    say( arg1 + arg2 + arg3 );
    "\n";
  }

Эта функция носит название showSum. Именем функции может быть любое сочетание букв и цифр, начинающееся с буквы. По умолчанию TADS различает прописные и строчные буквы в названиях, поэтому showSum, showsum и ShowSum будут определять разные функции.

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

Заключенные в скобки arg1, arg2 и arg3 - это аргументы функции. У некоторых функций нет аргументов; для таких функций скобки и список аргументов в заголовке отсутствуют. А вообще-то при вызове функции аргументы заменяются значениями. Например, при выполнении следующего кода:

  showsum( 1, 2, 3 );
  showsum( 9, 8, 7 ); 

функция showsum вызывается дважды: при первом вызове вместо аргументов arg1, arg2 и arg3 подставляются, соответственно, значения 1, 2 и 3; во втором случае они заменяются на 9, 8 и 7. В результате выполнения функции выводится следующее:

  Сумма равна: 6
  Сумма равна: 24 

Аргументы целиком "локализованы" внутри функции; например, arg1 имеет смысл только внутри функции showSum. Функция также может определять собственные локальные переменные посредством инструкции local; подробнее об этом будет рассказано далее.

Функции способны на гораздо большее, чем просто отображать сумму трех чисел. Инструкция if позволяет организовать ветвление, а while - циклическое исполнение инструкций. В TADS реализована целочисленная арифметика, а также операции над строками и списками (которые вскоре будут описаны). Кроме того, функция может возвращать значение вызывающей ее программе. И, разумеется, функция может вызвать другую функцию (в том числе и саму себя). В общем, практически любую программу, написанную на языках программирования типа Си, Паскаля или БЭЙСИКА, в принципе можно написать и на TADS.


Объекты

Объекты являются базовыми элементами любой игры, написанной на TADS. Любой предмет реального мира, моделируемый текстовой приключенческой игрой, описывается объектом TADS.

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

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

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


Атрибуты

В следующем примере для объекта определены только атрибуты и не определены методы.

  robot: object
    name = 'Ллойд'
    weight = 350
    height = 72
    speed = 5
  ;

Здесь определен объект robot с определенным набором атрибутов и их значений. Строка name = 'Lloyd' означает, что атрибут name имеет значение 'Lloyd', т. е. строковое значение. Аналогично, weight = 350 указывает на то, что атрибут weight имеет числовое значение 350, и т. д. (Учтите, что TADS поддерживаются только целые числа; нельзя указывать числа с плавающей запятой, например, 3.1415926. Максимальное значение числа в TADS составляет около 2 миллиардов.) Точка с запятой означает окончание определения объекта.

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

  showName: function( obj )
  {
    say( obj.name );
  } 

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

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

  newobj: object
    greeting = "Привет!\n"
  ; 

Знак \n в конце строковой переменной выполняет перевод строки (т. е. переводит курсор в начало новой строки). Существует похожий управляющий код \b, выводящий пустую строку. Вы можете задаться вопросом, зачем этот символ вообще нужен, поскольку можно использовать несколько символов \n подряд; дело в том, что TADS игнорирует повторяющиеся переводы строки. В большинстве случаев это упрощает форматирование текста, поскольку позволяет избегать нежелательных пустых строк; в том же случае, если вывод пустой строки действительно необходим, можно воспользоваться управляющим кодом \b.

При обращении к атрибуту greeting будет просто выведена строка Привет! и выполнен перевод строки. Таким образом, вместо вызова встроенной функции say для отображения значения newobj.greeting, вы можете просто обратиться к самому этому атрибуту:

  printGreeting: function( obj )
  {
    obj.greeting;
  }

Атрибуты также могут иметь значения списочного типа. Список (list) представляет собой набор значений, заключенных в квадратные скобки, например:

  listobj: object
    mylist = [ 1 2 3 ]
  ; 

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

Многие операторы и встроенные функции способны работать со списками. Операторы позволяют добавлять в список и удалять из него элементы, а также обращаться к конкретному элементу списка. Встроенные функции используются для перебора списка и поиска конкретного элемента в нем.

Пример организации перебора списка - нижеприведенная функция, которая выводит на экран все элементы, содержащиеся в listobj.mylist. В этом примере для получения отдельных элементов списка используется оператор индекса списка - [индекс]. Кроме того, используется встроенная функция length() - для определения количества элементов в списке.

  showList: function
  {
    local ind, len;
    len := length( listobj.mylist );            // Определяем длину списка
    ind := 1;                                   // Начинаем с первого элемента
    while ( ind <= len )                        // Цикл по всем элементам списка
    {
      say( listobj.mylist[ ind ] );                 // Выводим на экран текущий элемент
      "\n";                                          // Выводим перевод строки
      ind := ind + 1;                      // Переходим к следующему элементу
    }
  }

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

  showList: function
  {
    local ind;
    local len := length(listobj.mylist);           // Запоминаем длину списка
    for ( ind := 1 ; ind <= len ; ind++ )
    {
      say( listobj.mylist[ ind ] );                // Выводим на экран текущий элемент
      "\n";                                         // Выводим перевод строки
    }
  } 

Другой способ перебора списка - использование встроенных функций car() и cdr(). Нижеприведенный пример иллюстрирует работу этих функций.

  showList2: function
  {
    local cur;                 // Переменная для хранения "хвоста" списка
    cur := listobj.mylist;                    // Начинаем со всего списка целиком
    while ( car ( cur ) )      // car(list) возвращает первый элемент списка list
    {
      say( car ( cur ) );     // Выводим на экран первый элемент оставшегося "хвоста" списка
      "\n";                                           // Выводим перевод строки
      cur := cdr( cur );             // cdr(list) возвращает "хвост" списка;
                            // т. е. весь список без первого элемента (возвращаемого функцией car)
    }
  }

Данная функция перебирает список по элементам. Каждый раз при выполнении цикла while переменная cur заменяется на результат выполнения функции cdr с аргументом cur, который представляет собой список без его первого элемента; выполнение цикла завершается, когда car(cur) возвращает nil, что означает, что в списке больше нет элементов (nil - это специальный тип данных, который по сути означает отсутствие значения). Цикл типа while выполняется до тех пор, пока проверяемое условие не становится равным нулю или nil.

Обратите внимание, что прежде, чем начать выполнение цикла, мы присвоили значение списка локальной переменной. Если бы мы не сделали этого, то, поскольку мы берем "хвост" списка посредством функции cdr() при каждом выполнении цикла и присваиваем полученное значение переменной цикла, после окончания работы функции свойство listobj.mylist содержало бы пустой список. За счет использования локальной переменной атрибут listobj.mylist остается без изменений.

Вот базовые типы данных, используемых в TADS: числовые, строковые (заключенные в одинарные кавычки, представляющие собой просто значения, которые можно передавать), выводимые строки (заключенные в двойные кавычки; они не имеют конкретного значения, но выводятся на печать при каждом обращении к ним), списки (значения, заключенные в квадратные скобки), nil и true (истина) (возвращаемые логическими выражениями типа 1 < 2), а также объекты. Атрибут может принимать значения любого из этих типов.

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


Методы

Мы подошли к тому аспекту, где проявляется объектно-ориентированная природа этого языка: свойства могут содержать не только данные, но и исполняемый код. Если свойство содержит код, его называют методом.

  methodObj: object
    c =
    {
      local i;
      i := 0;
      while ( i < 100 )
      {
        say( i ); " ";
        i := i + 1;
      }
      return( 'вот и все' );
    }
  ;

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

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

  string1: object
    myString = "Это строка."
  ;

  string2: object
    myString =
    {
      "Это строка.";
    }
  ;

  string3: object
    myString =
    {
      say( 'Это строка.' );
    }
  ; 

Можно сказать, что в данном случае единственная разница между методом и атрибутом состоит в способе записи: тело метода всегда записывается в фигурных скобках, а атрибут - без них. Прямо скажем, это довольно зыбкое различие.

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

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

  argObj: object
    sum( a, b, c ) =
    {
      "Сумма равна: ";
      say( a + b + c );
      "\n";
    }
  ; 

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

  argObj.sum( 1, 2, 3 ); 


Наследование

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

  book: object
    weight = 1                                    // Книги обычно не слишком тяжелы
  ;

  redbook: book
    description = "Это красная книга. "
  ;

  bluebook: book
    weight = 2                                 // Книга более тяжелая, чем обычно
    description = "Это большая синяя книга. "
  ;

Первый объект, book (книга), определяет самую общую категорию. Объект redbook определяет конкретную книгу, что означает, что он обладает всеми свойствами книги, плюс некоторым количеством дополнительных свойств, характерных только для него. Точно так же bluebook определяет другую книгу. Опять же, этот объект, в свою очередь, обладает всеми свойствами общей категории book; в то же время, поскольку он определяет собственный атрибут weight (вес), свойство weight более общего объекта book игнорируется. (Оговорка насчет "свойства" здесь неслучайна: если бы свойство weight объекта book было не атрибутом, а методом, оно бы точно так же игнорировалось при новом его определении в объекте bluebook). Таким образом, атрибут redbook.weight имеет значение 1, тогда как bluebook.weight равно 2.

Следует еще раз подчеркнуть, что при наследовании не имеет никакого значения, о каком именно свойстве идет речь - методе или атрибуте.


Классы

Когда объект наследует свойства другого объекта, этот другой объект называют родительским классом или родительским объектом первого объекта. Когда объект является родительским для других объектов, он называется классом. Некоторые объектно-ориентированные языки программирования четко разграничивают объекты и классы (подробнее об этом см. в соответствующей главе); в TADS разница между этими двумя понятиями невелика. Тем не менее, в языке TADS имеется ключевое слово class, которое указывает, что объект используется исключительно как класс, т. е. в качестве шаблона для определения других объектов. Скажем, объект book можно было бы определить следующим образом:

  class book: object
    noun = 'книга' 'текст'
    weight = 1              // Книги обычно не слишком тяжелы
  ;

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


Множественное наследование

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

  multiObj: class1, class2, class3
  ;

При таком определении объект multiObj наследует свойства в первую очередь от класса class1, затем от class2, затем от class3. Если, скажем, все три класса определяют одноименные свойства prop1, то объект multiObj унаследует это свойство от class1, поскольку этот класс указан первым.

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



Чтобы изменить свой язык, надо изменить свою жизнь.
ДЕРЕК ВАЛКОТТ (DEREK WALCOTT), Дополнение (1965)


Глава вторая Содержание Глава четвертая