[В начало] [Содержание] [Предметный указатель] [ ? ]

Справочное руководство Enigma

Данное руководство описывает внутреннюю структуру игры Enigma версии 1.20, в частности процесс создания уровней с использованием языка программирования Lua и особенности взаимодействия с движком игры.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

1. Использование Enigma

После того, как вы установили Enigma и прошли несколько уровней, вам, надеемся, будет интересно узнать, как Enigma устроена, как настроить её в соответствии со своими предпочтениями и для чего служат некоторые опции и атрибуты игры.

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

1.1 Размещение ресурсов

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

Для управления загрузкой игры и хранением данных служат несколько путей. Их список можно вызвать в подменю справки или запустив Enigma из консоли с опцией ‘--log’ (см. раздел Опции запуска).

Путь к настройкам

Это путь к файлу с пользовательским настройками приложения. Этот файл обычно размещается у пользователя в директории HOME. Данные пользователей систем Windows, в которых HOME отсутствует, хранятся в директории ‘Application Data\Enigma’. Поскольку это третья версия игры, файл по умолчанию называется ‘.enigmarc.xml’.

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

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

Для разработчиков игры существует опция ‘--pref’ (см. раздел Опции запуска), с помощью которой можно переименовать файл настроек. Запустив программу с переименованным файлом настроек, разработчик может проверить новую конфигурацию, не рискуя потерять свои собственные настройки. Также разработчик в целях проверки может использовать переименованный файл, чтобы запустить игру со стандартной конфигурацией.

В любом случае файл настроек будет скрыт при помощи знака ‘.’ в начале имени файла.

Папка пользователя

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

Обязательно создайте резервную копию этой папки!

Её стандартное размещение — директория ‘.enigma’ в вашей директории HOME. В системах Windows, в которых отсутствует HOME, этой цели служит директория ‘%APPDATA%\Enigma’, которая разрешается в подпапку ‘Application Data\Enigma’ в Windows XP/2000 или ‘AppData\Roaming\Enigma’ в Vista/Windows 7, расположенную в папке данных пользователя.

В папке пользователя также хранятся файлы журналов событий и ошибок.

Путь к папке с пользовательскими данными можно задать в меню Настройки (см. раздел Пользовательские опции). Так можно сохранить данные игры на внешнем накопителе или общем разделе жёсткого диска и использовать их на различных установках Enigma.

Папка эскизов

Это ещё одна пользовательская папка Enigma. В ней хранятся изображения, в частности снимки экрана и эскизы уровней. Обычно папка пользователя, указанная в строке ‘Папка пользователя’, является одновременно и папкой изображений.

Если вы делаете много снимков экрана, а место в директории, указанной в строке ‘Папка пользователя’ ограничено, можно задать отдельный путь для папки с изображениями в меню Настройки (см. раздел Пользовательские опции).

Системная папка

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

Папки с ресурсами

Это список папок. Каждый ресурс, независимый от версии игры, ищется во всех папках из этого списка и загружается из первой найденной папки.

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

Папка локализации

В этой папке размещаются данные локализаций.

Обратите внимание, что некоторые ресурсы, например уровни, могут храниться в zip-архивах. В таких случаях ресурс, который вы ожидаете найти по адресу ‘имя_директории/имя_файла’, может храниться в zip-файле ‘имя_директории.zip’. Путём к файлу в zip-архиве может выглядеть как ‘имя_директории/имя_файла’ или ‘./имя_файла’. В случае, если ресурс присутствует как в виде zip-файла, так и в другом формате, приоритет отдаётся второму файлу, так как программа воспринимает его как обновление zip-файла.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

1.2 Опции запуска

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

Для многократного использования одинакового набора опций запуска можно создать ярлык на рабочем столе или запись в меню "Пуск" и добавить опции запуска к целевой строке исполняемого файла Enigma.

Ниже следует список поддерживаемых программой пользовательских опций. Если в списке указано как полное имя опции, следующее за двумя знаками "минус", так и однобуквенное сокращение, следующее за одним знаком "минус", используйте один из вариантов (а не два одновременно), например: ‘--data путь’ или ‘-d путь’.

--assert

Опция, предназначенная для разработчиков игры, которая принудительно выполняет все отладочные проверки, даже ресурсоёмкие. Результаты дополнительной проверки выглядят примерно так: ‘ASSERT(noAssert || long_lasting_check(), XLevelRuntime, "remark");’.

--data -d путь

Опция, предназначенная для разработчиков игры; она позволяет добавить к списку путей ресурсов дополнительный путь, который будет располагаться перед путём к системной папке (см. раздел Опции запуска). Разработчик может проверить сборку Enigma, не устанавливая её, посредством её вызова из оболочки с текущей рабочей директорией в качестве главной директории с помощью опции ‘src/Enigma -d ./data’.

--help -h

Просто выводит на экран все опции запуска и завершает свою работу.

--lang -l язык

Опция, позволяющая выбрать язык игры. Язык задаётся стандартным двухбуквенным обозначением, например ‘fr’ для французского или ‘ru’ для русского.

--log

Эта опция добавляет к стандартному выводу внутреннюю информацию. Пользователи Windows могут найти её в файле ‘Output.log’ в стандартной ‘Папке пользователя’. В ещё одном файле ‘Error.log’ перечислены критические ошибки.

В выводимую информацию будут включены, к примеру, пути, описанные в разделе Размещение ресурсов.

--nograb

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

--nomusic

Запускает игру с выключенным музыкальным сопровождением.

--nosound

Запускает игру с выключенным звуковым сопровождением.

--pref -p имя_файла

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

--pref -p путь_к_директории

Путь к альтернативной директории, в которой содержится файл настроек со стандартным именем ‘.enigmarc.xml’. Если файл настроек или папка не существуют, они автоматически создаются. При создании файла настроек здесь по умолчанию располагается пользовательская папка данных. Это позволяет размещать все пользовательские данные игры в отдельной директории, которая может располагаться где угодно, например на USB-накопителе. Чтобы использовать эту новую настройку, всегда нужно запускать Enigma с данной опцией. Помните, что путь, содержащий пробелы, нужно заключать в кавычки.

--redirect

Перенаправляет ‘stdout’ и ‘stderr’ в файлы с названиями ‘Output.log’ и ‘Error.log’ в стандартной папке пользователя (см. раздел Размещение ресурсов). Для Windows данная опция всегда имеет значение true, но она полезна на любых операционных системах, если Enigma запускается через ярлык на рабочем столе или кнопку меню.

--robinson

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

--showfps

Позволяет отображать во время игры количество кадров в секунду (FPS).

--version

Отображает номер версии и завершает свою работу.

--window -w

Позволяет запустить Enigma в оконном режиме (не в полноэкранном).

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

Пользователи Unix могут запустить Enigma при помощи следующей команды:

enigma --log ~/mylevel.xml http://somewhere.com/netlevel.xml

Пользователи Windows могут запустить Enigma из командной строки (не забудьте внести изменения в путь установки, чтобы он соответствовал пути установки Enigma на компьютере):

C:\Programs\Enigma\enigma.exe --log demo_simple.xml

Эти уровни можно найти в пакете уровней ‘Startup Levels’. По умолчанию он является видимым, только если в командной строке указаны какие-либо уровни.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

1.3 Пользовательские опции

Обновление рейтинга

Пожалуйста, для версий ниже 1.00 оставьте следующее значение для этой опции: ‘Не обновлять’.

Имя игрока

Введите ник, под которым Enigma будет сохранять ваши баллы. Пожалуйста, взгляните на список уже используемых ников на домашней странице Enigma и выберите ещё не используемый. Свой ник можно изменить в любой момент, не рискуя потерять свои данные.

Папка пользователя

Как поясняет раздел Размещение ресурсов, здесь можно указать произвольную директорию для хранения ваших пользовательских данных.

Если очистить данную строку, то данные будут размещены в директории по умолчанию.

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

Папка эскизов

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

Если очистить данную строку, то данные будут размещены в директории по умолчанию.

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

1.4 Консольная функция инвентаря

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

Команду можно ввести с клавиатуры. Просто введите команду и активируйте её, нажав клавишу <Enter>. Поддерживаются следующие команды:

help

Выводит список всех доступных пользователю команд.

abort

Завершение уровня и возврат в меню уровней. Аналогично нажатию <Alt+X>

April 1st

Просто шутка.

cheats

Выводит "читы", предназначенные для быстрого тестирования уровня его разработчиком.

collision

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

easy

Перезапуск уровня в простом режиме.

find строка_поиска

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

god

Необходимый разработчикам "чит", который защищает актёров, присвоенных текущему игроку, подобно активированному объекту it_umbrella. При его использовании счёт в случае успешного завершения уровня сохранён не будет.

hunt

Переход в режим охоты за мировыми рекордами. Аналогично выбору иконки мирового рекорда на левой кнопке в меню уровней.

info

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

jumpto пакет,номер

Непосредственный запуск указанного уровня. Для пакета уровней указывается его название. Для уровня указывается его порядковый номер в пакете. Например jumpto Enigma IV,33.

nohunt

Выключает режим охоты за мировыми рекордами. Аналогично выбору иконки шарика на левой кнопке в меню уровней.

regular

Перезапуск уровня в сложном режиме.

restart

Перезапуск уровня в ранее выбранном режиме сложности.

suicide

Убивает актёров, но без завершения уровня, если это возможно. Аналогично нажатию клавиши F3>.

Игра сохраняет историю команд и отображаемых документов. Эту историю можно вывести с помощью стрелок вверх и вниз.

Стрелка вверх, изначально нажатая при отображении предметов в инвентаре, позволяет прокручивать ранее введённые команды. Повторно отправить команду можно нажатием клавиши <Enter>. История будет пересортирована, при этом последняя команда окажется непосредственно над инвентарём. Историю команд можно редактировать в любой момент, в том числе и добавлять туда новые команды. Если после ввода команды не была нажата клавиша <Enter>, строка всё же будет записана и выведена в качестве первой команды над инвентарём. История команд постоянно поддерживается в актуальном состоянии.

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

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

1.5 Информация об уровне

Enigma хранит гораздо больше сведений о каждом уровне, чем можно отобразить в меню уровней. Эти сведения можно просмотреть с помощью инспектора уровней. Его можно вызвать, щёлкнув правой кнопкой мыши (или любой кнопкой мыши + Ctrl) по эскизу уровня в меню уровней.

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

1.5.1 Публичные рейтинги

Большинство уровней игры оцениваются по пяти различным категориям:

Мы используем сокращения, чтобы подчеркнуть отличие наименований категорий рейтинга от повседневных значений этих слов. В каждой из этих категорий уровни оцениваются по пятибалльной шкале от 1 (лёгкий) до 5 (сложный), за исключением категории ‘знан’, где уровням может присваиваться 6 баллов (уникальный механизм).

Пожалуйста, имейте в виду, что строго придерживаться приведённых ниже критериев в почти 750 случаях чрезвычайно затруднительно, поэтому в отдельных уровнях могут быть (и будут) отклонения от них.

Интеллект (инт)

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

Высокие рейтинги обычно присваиваются головоломкам. Инт-рейтинги не накапливаются: рейтинг уровня определяет его самая сложная задача.

Ловкость (ловк)

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

В противоположность инт-рейтингам, данные рейтинги могут накапливаться: так, уровень, на котором имеется множество опасностей, оцениваемых в три балла, в результате может получить 4 или даже 5. Роторы в уровнях также ассоциируются с рейтингами ловкости ‘ловк’ и скорости (‘скор’). Поэтому уровни с комбинацией из ‘ловк-скор’ в основном подвижные и активные, тогда как комбинации с высокими ‘ловк-терп’ характерны для лабиринтов.

Терпение (терп)

Терпение — это субъективный рейтинг; он в большей степени отражает "ощущение времени", много ли времени потребовалось для решения уровня, исходя из субъективных ощущений игрока. Таким образом, два однотипных уровня могут иметь различные терп-рейтинги, если, например, у одного из них более привлекательный дизайн или прогресс игрока на нём более очевиден (например, сразу видно количество открытых оксидов). Необходимость много раз начинать уровень заново оказывает существенное влияние на этот рейтинг; важно не время в нижнем левом углу экрана и не счёт, а субъективное ощущение времени, прошедшего с момента, как вы впервые окинули уровень взглядом, до завершения уровня.

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

Высокие терп-рейтинги обычно присваиваются лабиринтам. В сочетании с "инт 3" высокий терп-рейтинг обычно свидетельствует о наличии на уровне спрятанного предмета или полого камня. Терп-рейтинги относятся ко всему уровню, так что они не могут накапливаться.

Знание игры (знан)

Рейтинг знания игры в основном учитывает функции и способы взаимодействия различных объектов игры, таких как камни, покрытия, предметы и актёры. Однако в некоторых случаях он учитывает и особые способы решения уровней. Основой при выставлении рейтинга является уровень "Advanced Tutorial", которому присвоен рейтинг "знан 3". Рейтинг "знан 4" присваивается уровням, на которых встречаются стандартные объекты, отсутствующие в "Advanced Tutorial". Уровни с рейтингом "знан 5" требуют более глубокого знания внутренних закономерностей игры. И, наконец, рейтинг "знан 6" является показателем уникальных либо редко встречающихся способов решения уровня. Общий знан-рейтинг уровня определяется по наиболее сложным объектам или способам решения, встречающимся на нём, и, таким образом, не накапливается:

  1. Управление одним шариком на обычных покрытиях, обычные стены, оксиды, оксидоподобные камни, смертоносные камни, вода, пропасть, документы, использование инвентаря, неизменная гравитация, видимые склоны.
  2. Перемещение камней, несложные задания а-ля "Сокобан", постройка мостов через воду и пропасть, соединённые камни-мозаики, управление более чем одним шариком, медитация, решётки, роторы и волчки, невидимые склоны, триггеры и переключатели, двери, отверстия (кроме сделанных динамитом), болото, дискеты и камни для дискет (st-floppy), ключи и замки, монеты и отверстия для монет, трещины в полу, камни-таймеры.
  3. Различные типы покрытий с различным трением и силой мыши, космос, лёд, инверсное покрытие, сочетание тонущих и плавучих камней, чёрные решетки, не пропускающие роторы и волчки, динамитные шашки, камни, которые можно взорвать с помощью динамита, лопаты, болдеры, волшебные палочки, изменяющие направление движения болдеров, необходимость загнать болдеров в пропасть, стекло, ложки, актёры и предметы, прячущиеся под подвижными камнями, маленькие белые шарики (не опасные), улицы определённого цвета с односторонним движением, импульсные камни ("бамперы"), роторы, перелетающие через пропасть, выхлопные камни, мечи и рыцари, лазеры, неподвижные и подвижные зеркала, трансформация предметов и монет под воздействием веса и лазера, зонтики, позволяющие перелетать через пропасть, молотки и бьющиеся камни (хотя и отсутствуют в "Advanced Tutorial").
  4. Постройка мостов на болотах, резиновые ленты, ленточные камни, камни-ножницы, разъединённые камни-мозаики, взрывающиеся камни-мозаики, изменение расположения камней-мозаик (волшебной палочкой и без неё), пружины (оба типа — напольные и встроенные, как на уровне "Upstream Journey"), воры, сёгун-камни из трёх частей, камни-невидимки, полые камни, камни-хамелеоны, предметы, спрятанные под камнями-хамелеонами, камни, замаскированные под другие камни (например фальшивые смертоносные камни), телепорты, магниты, необходимость использовать клавишу <F3> для решения уровня, символы Инь-Ян, одноцветные камни, камни Инь-Ян, обратные камни Инь-Ян, камни, которые можно разбить только шариком определённого цвета, шарики-убийцы, обменные камни, камни для кисточки и рисования, изменение направления на улице с односторонним движением при помощи волшебной палочки, превращение камней в стекло при помощи волшебной палочки, импульсные камни (подвижные, статичные и полые), чёрные и белые бомбы, камни-бомбохранилища, огонь, огнетушители, вращающиеся камни, жёлтые антиобменные камни, мины, флажки, семена, гири, необходимость положить объект под однонаправленные и другие полые камни, электрические камни, турникеты, почтовые камни и каналы связи, кольца (для одного и нескольких игроков), вулканы, сумки, генерирование случайного набора объектов (как возможный результат действия переключателя), лошади (актёры) и камни, через которые лошади могут проходить, шипы, бананы, вишни (необходимы, чтобы сделать шарик невидимым), сюрпризы.
  5. Трещины, напольные пружины, телепорты и т. д. — все предметы, семена растут внутри камней, лазер блокируется всеми предметами, шарики-убийцы не тонут в воде, столкновения, подобные тем, которые присутствуют на уровне "Space Meditation", необходимость удерживать кнопку мыши, невидимость как способ пройти сквозь стекло, прыжки через лазеры...
  6. Плюющиеся камни, камни-сюрпризы, уровни типа "Enigris" или "Flood Gates" ...

"Знан 6" не обязательно означает, что уровень сложен для понимания; уникальный объект или механизм может быть и интуитивно понятным, как, например, во "Flood Gates".

Скорость и контроль над скоростью (скор)

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

  1. Никаких ограничений по времени.
  2. Не останавливайтесь надолго. Вас, к примеру, может кто-нибудь преследовать на невысокой скорости.
  3. Присутствует умеренное ограничение по времени либо необходимость контроля над скоростью. Это может быть один не очень быстрый ротор на открытой территории.
  4. Не останавливайтесь! На уровне могут быть, к примеру, задания с ограничением по времени или роторы (один быстрый либо несколько медленных).
  5. Скорее! Если "скор 4" означает, что уровень сложный, но решаемый, то рейтинг "скор 5" применяется для уровней, для которых данное определение было бы очень сильным преуменьшением.

Скор-рейтинг накапливается, так как множество медленных роторов могут в сумме составить задание, по сложности подпадающее под "скор 3" или "скор 4", а несколько медленных таймеров-переключателей, которые нужно нажать в определенном порядке, могут представлять собой задание, находящееся на пределе человеческих возможностей. В отличие от остальных категорий, для которых средний рейтинг приближается к 3 (или находится между 3 и 4 для знан), большинство уровней определённо подпадают под определение "скор 1". Таким образом, скор-рейтинг можно, скорее, рассматривать как приложение к трём основным рейтингам: инт, ловк и терп.

Совокупность рейтингов

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

 
общая сложность  =  7*инт + 6*ловк + 4*терп + 3*знан + 4*скор - 23

Особенностью данного способа вычисления общего рейтинга является то, что он распределяет уровни по относительно широкой и непрерывной шкале от 1 (все рейтинги равны 1) до 100 (все рейтинги 5, знание 6) и основной упор в нём сделан на самые сложные категории — интеллект и ловкость. Однако некоторые очень низкие и очень высокие значения (например 2 и 99) не могут встречаться в этой комбинации. Другие комбинации позволяют получить полную, но узкую, либо широкую, но не являющуюся непрерывной шкалу.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

1.5.2 Баллы

В колонке баллов находятся ваши баллы, а также некоторые сравнительные величины для нормального и лёгкого режимов (если уровень поддерживает лёгкий режим).

Мировой рекорд — это лучшее время прохождения уровня, которое было прислано команде разработчиков Enigma. Мировые рекордсмены перечислены под колонкой с баллами.

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

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

Количество решивших — это количество игроков, которые решили данную версию уровня.

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

1.5.3 Версии

В колонке "Версия" содержится подробная информация об уровне. Обратитесь к разделам <version> и <modes> главы Основы работы с уровнями, где объяснены приведённые в колонке значения.

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

1.5.4 Личные примечания и оценки

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

Примечания хранятся в вашем персональном файле ‘state.xml’. Для каждого уровня, вне зависимости от его версии, можно ввести только одно примечание.

Кроме того, уровням можно выставлять оценки. Просто нажмите на кнопку "Рейтинг". Можно выставлять оценки от 1 до 10, знак ‘-’ означает "воздержался". 0 означает очень плохой уровень, в который, по вашему мнению, не стоит и играть, 5 обозначает средний уровень, 10 — самый лучший. Старайтесь использовать все значения при выставлении оценок.

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

1.5.5 Снимки экрана

Играя на уровне, можно делать снимки экрана при помощи клавиши <F10>. Можно сделать целую серию снимков экрана, чтобы получить документальные свидетельства об игре. Каждый снимок экрана программа сохраняет под уникальным именем файла. При помощи инспектора уровней можно просматривать снимки экрана прямо из игры. Просто нажмите на кнопку <Снимок экрана> для просмотра первого изображения.

Так как кнопки могут помешать просмотру, управление игрой в данном режиме осуществляется исключительно с помощью клавиатуры. Нажмите <F1> для вызова экрана помощи, <ESC>, чтобы вернуться к инспектору уровней, <Page Up> и <Page Down> для просмотра, соответственно, предыдущего и следующего снимка экрана. Если продолжить просмотр после показа последнего снимка экрана, "недостающий" файл снимка экрана получит имя. Это может быть полезной подсказкой, где искать другие файлы снимков экрана в вашей ‘папке эскизов’ (см. раздел Размещение ресурсов).


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

1.6 Гандикап и пар (PAR)

В то время как PAR описывает степень сложности уровня (см. раздел Баллы), гандикап (‘гкп’) описывает способность решать уровни в рамках PAR. Гандикап всегда привязан к пакету уровней либо группе пакетов. Свой гандикап для каждого пакета в меню уровней можно узнать, выбрав режим соответствия (нажимайте на кнопку в нижнем левом углу экрана, пока не появится изображение ускоряющегося чёрного шарика). Гандикап выводится в правом верхнем углу экрана вместе с количеством уровней, которые решены ниже PAR.

Гандикап в Enigma напоминает гандикап, используемый в гольфе. Чем он ниже, тем лучше. Если решить все уровни за время, равное PAR, гандикап будет равен 0. Если их решить за время, лучшее, чем PAR, гандикап будет выражаться отрицательным числом. Игроки могут использовать гандикап для сравнения своего мастерства.

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

Мы просим всех игроков присылать нам свои баллы. Все присланные баллы учитываются при определении мировых рекордов и подсчёте процента и количества игроков, решивших уровень.

Однако при подсчете PAR мы учитываем баллы только тех пользователей, которые решили более определённого количества уровней (на данный момент примерно 10% всех уровней). Для каждого уровня мы подсчитываем гармоническое среднее баллов, набранных ‘профессиональными игроками’. Профессионалы, не решившие уровень, учитываются при подсчете с баллами в 10 раз большими, чем мировой рекорд. Гармоническое среднее вычисляется по формуле:

гарм.средн. = N / (sum_[j=1..N] 1/баллы_j) )

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

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

+ 1.0Для каждого нерешённого уровня
+ log10(время/par)Для каждого решённого уровня, для которого существует PAR, если ваше время >= par
+ 0.7Верхний предел для каждого решённого уровня, для которого существует PAR, если ваше время >= par
+ log2(время/par)Для каждого решённого уровня, для которого существует PAR, если ваше время < par
- 3.0Нижний предел, а также величина для уровней, для которых нет PAR

Обратите внимание, что всем баллам, лучшим, чем PAR, соответствует отрицательная величина, таким образом они уменьшают общую величину гандикапа. Для пакета, состоящего из 100 уровней, гандикап может быть в пределах от +100 до -300. Для пакетов, состоящих из большего либо меньшего количества уровней, Enigma применяет коэффициент "100/размер пакета" для получения сравнимых величин гандикапа. Гандикапы указываются с точностью до одной десятой.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

1.7 Пользовательские наборы звуков

(Следующая информация действительна только для версии Enigma 1.01 и выше.) Звуковые эффекты вызываются так называемыми ‘звуковыми событиями’ (‘sound events’). У этих звуковых событий обычно есть имя (например ‘dooropen’ — открытие двери) и ассоциированное размещение (координаты двери), которые влияют на проигрывание звукового эффекта. Собрание всех аудио файлов, их соотнесение со звуковыми событиями, а также некоторая дополнительная информация по их использованию называется ‘набором звуков’ (‘sound set’).

Можно использовать свои собственные аудиофайлы для создания персональных наборов звуков Enigma и выбирать между наборами звуков в меню настроек (опция ‘Набор звуков’). Эти наборы звуков можно распространять на условиях любой лицензии, а также устанавливать наборы звуков, созданные другими пользователями. В игре не предусмотрено никаких ограничений на количество установленных наборов звуков.

Звуковое событие конвертируется в звуковой эффект с помощью таблиц. Эти таблицы можно найти в файле ‘data/sound-defaults.lua’, а также в пустом файле сэмплов ‘reference/soundset.lua’. Каждый пункт в этих таблицах представляет собой либо строку типа ‘enigma/st-coinslot’, которая понимается программой как файл ‘soundsets/enigma/st-coinslot.wav’ с некими свойствами по умолчанию, либо список звуковых атрибутов в фигурных скобках. Звуковые события, вызываемые с помощью сообщения sound, конвертируются таким же способом. Ниже мы приводим пример подобного пункта таблицы:

 
dooropen = { file="my_soundset/open-door", volume=0.9, priority=4 },

Данные атрибуты имеют следующие значения:

Чтобы создать новый набор звуков, следуйте следующей инструкции:

  1. Создайте новую папку, содержащую копию файла сэмплов ‘soundset.lua’ и wav-файлы, которые вы хотите использовать.
  2. Переместите её в папку "soundsets", находящуюся в вашей пользовательской папке (возможно, придётся её создать). Структура директории должна выглядеть примерно так:
     
    (папка пользователя)/soundsets/my_sounds/
                                   /soundset.lua
                                   /high_pitch.wav
                                   /soundfile_13.wav
                                   ...
    
  3. Запустите игру и выберите ‘My Soundset’ в меню настроек. Так как в данном наборе звуков звуковые эффекты не ассоциированы с wav-файлами, звук должен отсутствовать.
  4. Отредактируйте файл ‘soundset.lua’ по своему усмотрению. Можно использовать звуковые файлы по умолчанию, например:
     
    ...
    coinsloton = { file="enigma/st-coinslot" },
    ...
    

    Если используете свои собственные файлы, не забудьте создать соответствующие подпапки, например:

     
    ...
    coinsloton = { file="my_sounds/soundfile_13" },
    ...
    

    Ни в коем случае не указывайте расширение ".wav"! Оно добавляется автоматически. Проверьте, чтобы расширение было написано строчными буквами.

  5. Замените ‘MY_SOUNDSET’ подходящим именем переменной, а ‘My Soundset’ — именем, которое вы хотите видеть в меню звуковых настроек. Не делайте его слишком длинным, чтобы оно уместилось на кнопке. Три переменных-идентификатора, имя кнопки и имя директории не обязательно должны быть одинаковыми, но похожие имена, уникальные для каждого набора звука, облегчают жизнь другим разработчикам.

Не забудьте выбрать нужный набор звуков в меню настроек всякий раз, когда вы переименовываете его. И всегда выходите из игры, когда вносите изменения в набор звуков: Enigma не определяет новые наборы звуков без перезапуска.

У вас есть право свободно распространять zip-архивы, содержащие ваш набор звуков и файл ‘soundset.lua’. Новый набор звуков можно установить из архива, просто распаковав его в поддиректорию ‘soundsets’ вашей папки пользователя. Убедитесь, что файл ‘soundset.lua’ находится на одну поддиректорию ниже, чем ‘soundsets’. Деинсталлировать набор звуков можно, просто удалив соответствующую папку. Если вы хотите сделать набор звуков невидимым для Enigma, одного только переименования директории недостаточно, следует переименовать и файл ‘soundset.lua’. Это может пригодиться, если вы используете взаимозависимые наборы звуков (звуковые наборы, использующие общие файлы) и хотите сделать недоступным только один из них.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

2. Основы работы с пакетами уровней

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

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

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

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

2.1 Начало работы с пакетами уровней

Одна из наиболее выдающихся черт Enigma — это возможность добавлять в неё новые уровни. А сообщество пользователей обычно присылает нам несколько новых превосходных уровней каждую неделю.

Добавить новый уровень, полученный в виде XML-файла, очень просто. Найдите поддиректорию ‘levels/auto’ в вашей ‘папке пользователя’ (см. раздел Размещение ресурсов), скопируйте файл уровня в эту папку и перезапустите игру. Новый уровень появится в пакете ‘Auto’ и в него можно будет сыграть точно так же, как и в любой другой.

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

Второй способ сыграть в новые уровни — это добавить адреса файлов этих уровней в командную строку (см. раздел Опции запуска). Так можно играть в уровни, которые хранятся где угодно, можно даже вводить URL уровней, размещённых в Интернете. Уровни, добавленные в командную строку, находятся в пакете ‘Startup Levels’.

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

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

Пакеты уровней, распространяемые в виде папок с файлами уровней и файлом-указателем, следует копировать в папку ‘levels’ в вашей папке пользователя (см. раздел Размещение ресурсов).

Пакеты уровней, распространяемые в виде zip-архивов, следует копировать в папку ‘levels’ в вашей папке пользователя. Распаковывать их не обязательно, хотя это и возможно (см. раздел Пакеты в формате zip).

Пакеты уровней, распространяемые в виде отдельных файлов-указателей XML, следует копировать в папку ‘levels/cross’ в вашей папке пользователя.

Все новые пакеты должны быть доступны из меню пакетов уровней после перезапуска Enigma.

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

2.2 Конвертирование пакетов из версии 0.92

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

Если раньше вы хранили пакеты в папке системных файлов Enigma, то из старой версии игры их нужно скопировать в поддиректорию ‘levels’ в папке пользователя (см. раздел Размещение ресурсов). Пользовательская папка существует на всех системах, и, начиная с версии 1.00, Enigma никогда не вносит изменения в системную директорию файлов уровней; обновления и преобразование она выполняет только в папке пользователя. Если пакеты были внесены в файл ‘index.lua’ в системной директории, то нужно скопировать регистрационные строки в файл ‘index_user.lua’, который следует хранить в папке пользователя.

Если у вас было несколько пакетов уровней, в Enigma 0.92 их можно было хранить в различных поддиректориях папки ‘levels’. Однако, поскольку можно было хранить все файлы уровней и различные указатели в самой папке ‘levels’, с автопреобразованием возникнут трудности, так как Enigma 1.00 допускает только один пакет уровней с приложенными файлами уровней в каждой папке. В подобных случаях мы рекомендуем поэтапное преобразование: на каждом этапе конвертируйте только один старый указатель. Enigma сконвертирует этот указатель в новый файл ‘index.xml’. Переместите полученный указатель в соответствующую папку вместе с уровнями и приступайте к преобразованию нового пакета.

Следует отдельно рассмотреть случаи, когда указатель в папке ‘levels’ содержал ссылки на уровни, хранящиеся в различных поддиректориях папки ‘levels’. Поскольку в Enigma 0.92 не применялись перекрёстные указатели, а Enigma 1.00 требует, чтобы все файлы уровней пакета хранились в отдельной папке, алгоритму преобразования приходится самому определять нужную папку. Он просто берёт поддиректорию первого уровня. Если это не подходит, то перед преобразованием нужно очистить пакет из версии 0.92.

Все прочие стандартные пакеты уровней Enigma конвертирует без особых проблем. Преобразование она выполняет только один раз. После создания нового файла ‘index.xml’ используется только этот указатель. Следовательно, после тщательной проверки старый ‘index.txt’ может быть удалён. Мы рекомендуем сохранять резервную копию старого индекса до окончательного перехода на Enigma 1.00.

Если вы использовали персональный пакет уровней в формате zip, то вы найдёте поддиректорию с именем zip-архива в папке ‘levels’. В этой папке Enigma сохраняет конвертированный файл ‘index.xml’. Вам, скорее всего, придётся заменить старый ‘index.txt’ в zip-архиве на новый указатель. После этого поддиректория может быть полностью удалена, так как Enigma будет загружать указатель прямо из zip-архива.

После окончания конверсии пакетов мы настоятельно рекомендуем обновить уровни до нового формата XML (см. раздел Основы работы с уровнями).


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

2.3 Пакеты в формате zip

Помимо классических пакетов уровней, представляющих собой поддиректорию папки ‘levels’ с файлом ‘index.xml’ и несколькими файлами уровней, в Enigma 1.00 предусмотрен совместимый формат zip-архивов. Zip-формат позволяет уменьшить размер ресурсов игры и облегчает распространение пакетов.

Этот формат стопроцентно совместим. Если у вас есть обычный пакет уровней, размещённый в поддиректории, то можно просто заархивировать всю папку и назвать архив её именем с расширением .zip. Теперь эту папку можно полностью удалить: Enigma автоматически определит пакет уровней и позволит играть в него без ограничений. Сохранятся даже перекрёстные ссылки на этот пакет.

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

Если сохранить и архив, и папку, то файлы папки обладают приоритетом перед архивом. Таким образом, Enigma сохраняет обновления в поддиректориях параллельно с существующими zip-архивами.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

2.4 Группировка и сортировка пакетов

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

Тем не менее, своё мнение мы не навязываем. Вы вольны переименовывать группы, добавлять новые и изменять расположение пакетов. Как и в случае с другими аспектами Enigma, управлять пакетами и группами пакетов можно, щёлкнув любой кнопкой мыши + Ctrl либо одной правой кнопкой по кнопке соответствующего пакета или группы.

В меню конфигурации групп можно переименовать группу либо изменить её расположение. Группе можно выбрать любое имя, которое ещё не использовалось, не заключено в квадратные скобки и отличается от ‘Every Group’. Обратите внимание, что, возможно, вам не удастся ввести все желаемые символы. Приносим извинения за это неудобство.

В меню конфигурации пакетов можно поместить пакет в какую-либо группу. Список групп содержит два специальных пункта: ‘[Every Group]’ и ещё одно имя в квадратных скобках. При выборе первой псевдогруппы пакет уровней отображается в каждой группе. Это является размещением по умолчанию группы ‘Startup Levels’. Второе имя в квадратных скобках — это группа, в которой по умолчанию размещается сам пакет уровней. Это подсказка, которая позволит переадресовать пакет уровней в группу по умолчанию, даже если удалить саму группу.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

2.5 Создание новых пакетов уровней

Чтобы создать новый пакет уровней, выберите группу, в которую следует этот пакет добавить. Скорее всего, это будет группа ‘User’. Щёлкните любой кнопкой мыши + Ctrl или одной правой кнопкой по группе, затем нажмите на кнопку ‘Новый пакет’. На экране отобразится меню конфигурации пакетов, где можно ввести все данные, необходимые для создания пакета уровней.

Вначале нужно ввести имя пакета. Здесь также есть ограничение в выборе символов, которые можно использовать для имён файлов. Разрешается использовать алфавитно-цифровые символы A-Z, a-z, 0-9, пробелы, "_" и дефис. Обратите внимание, что позже вы сможете переименовать пакет, если захотите дать ему более удачное или более подходящее имя (см. раздел Изменение и удаление пакетов).

После этого следует определить, хотите ли вы создать пакет, в котором могут находиться источники уровней или только перекрёстные ссылки. Первый вариант удобен для хранения самостоятельно написанных либо загруженных из Интернета уровней. Пакет с перекрёстными ссылками можно использовать для хранения коллекций своих любимых уровней, для которых просто отбираются существующие уровни из других пакетов, исходя из своих собственных критериев. Нужный тип пакета можно выбрать с помощью кнопки ‘Типы уровней’, которая использует символы для ссылок и копий.

Размещение’ — это величина, которая определяет размещение среди групп пакетов, если пакет не был пересортирован вручную (см. раздел Группировка и сортировка пакетов). Эта задаваемая по умолчанию величина имеет значение, только если вы распространяете свой пакет и хотите быть уверенным, что пользователи поместят этот пакет там, где нужно. В большинстве случаев величина, автоматически задаваемая после создания нового пакета, является оптимальной.

Можете указать себя в качестве владельца или создателя пакета. Это просто строка для идентификации.

Наконец, после окончания настройки вы можете создать пакет, нажав кнопку ‘OK’. Enigma создаст новый пакет в папке пользователя (см. раздел Размещение ресурсов).

Если вы решите не создавать новый пакет, нажмите кнопку ‘Отмена’. В этом случае всё останется без изменений.

Если вы хотите сразу же добавить уровни в пакет, нажмите кнопку ‘Составить пакет’. Enigma создаст пакет, и вы сможете использовать составитель пакетов, чтобы наполнить этот пакет уровнями.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

2.6 Изменение и удаление пакетов

Чтобы изменить пакет, щёлкните по нему любой кнопкой мыши + Ctrl либо одной правой кнопкой в меню пакетов уровней. На экран будут выведены метаданные всех пакетов. Однако кнопка ‘Ред. метаданные’ появится только для ваших собственных пакетов, которые хранятся в папке пользователя. Нажав на неё, можно отредактировать метаданные.

Пакет можно переименовать, но нельзя изменить имена файлов. Enigma будет использовать новое имя в качестве логического имени пакета, которое отображается в меню.

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

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

Во избежание случайной потери файлов уровней в игре не предусмотрена функция удаления пакетов. Тем не менее, пакет можно удалить, просто удалив папку с этим пакетом из вашей папки пользователя. Для пакетов, состоящих из перекрёстных ссылок, просто нужно удалить файл-указатель XML в поддиректории ‘levels/cross’ вашей папки пользователя.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

2.7 Составление пакетов уровней

Изменения в состав пакета могут быть внесены при помощи составителя пакетов. Его можно вызвать, нажав любой кнопкой мыши + Ctrl либо одной правой кнопкой мыши на кнопку пакета в меню пакетов уровней, а затем на кнопку ‘Составить пакет’ в меню конфигурации пакета.

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

Буфер обмена позволяет выбрать уровни из одного или нескольких пакетов и вставить их в виде копии либо перекрёстной ссылки в свой пакет. Сначала очистите буфер обмена при помощи клавиш ‘Shift + delete’. Затем выберите нужные уровни из составителя. Добавьте их в буфер обмена с помощью ‘Shift + щелчок’. Они отобразятся в строке вверху экрана. Вернитесь в пакет, в который желаете добавить уровни. Выберите уровень, после которого желаете вставить уровни. Используйте клавишу ‘F8’, чтобы вставить уровни из буфера в виде ссылок. При редактировании пакета, в котором допускаются копии уровней, их можно вставить при помощи клавиши ‘F9’.

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

Помимо добавления уровней, их можно удалять с помощью клавиши ‘delete’. Имейте в виду, что если удалить уровень, который является файлом, а не просто ссылкой, то Enigma удалит и сам файл уровня. Будьте осторожны с уровнями, на эскизе которых имеется изображение документа. Отменить удаление можно с помощью кнопки ‘Отмена’.

Сортировка уровней осуществляется с помощью клавиш ‘alt + стрелка влево’ и ‘alt + стрелка вправо’. Новый порядок следования уровней отображается на экране незамедлительно, и его можно сохранить, нажав кнопку ‘OK’.

Чтобы обновить индекс уровней, можно воспользоваться клавишей ‘F5’. Это может пригодиться во время редактирования уровня. В пакете отразятся изменения названия, версии, появление простого режима для уровней и т. д. Все уровни пакета Enigma обновляет одновременно.

Используя пакет уровней Auto и составитель пакетов, можно создавать пакеты, состоящие из ваших собственных уровней: создайте новый пакет, добавьте файлы уровней в папку ‘auto’, перезапустите игру, добавьте уровни из папки ‘auto’ в буфер обмена, с помощью составителя вставьте уровни из буфера обмена в свой пакет в виде копии и удалите неиспользуемые файлы уровней из папки ‘auto’.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

3. Основы работы с уровнями

Сыграв в несколько уровней Enigma, вы наверняка заметили, что Enigma — довольно динамичная игра с разнообразными уровнями. Поэтому нет ничего удивительного в том, что такие уровни невозможно описать каким-то определённым способом, как простую карту с объектами в Сокобане. Некоторые уровни, такие как лабиринты, каждый раз генерируют свой вид заново и во время каждого сеанса игры выглядят по-разному. Другие уровни предусматривают динамику в процессе игры, например, переключатели могут открывать двери только при определённых условиях. Чтобы обеспечить эти возможности, мы встроили в Enigma мощное и лёгкое расширение языка C — Lua версии 5.1.4.

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

С XML-икацией Enigma после выхода версии 0.92 мы добились полной поддержки XML, интегрировав Apache Xerces и задались целью избавиться от недостатков старого формата уровней и добавить некоторые новые недостижимые ранее возможности:

Позвольте привести пример готового простого уровня ‘Hello World’ в новом формате:

 
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<el:level xsi:schemaLocation="http://enigma-game.org/schema/level/1 level.xsd" xmlns:el="http://enigma-game.org/schema/level/1">
  <el:protected >
    <el:info el:type="level">
      <el:identity el:title="Demo Simple" el:id="20060210ral001"/>
      <el:version el:score="1" el:release="1" el:revision="2" el:status="stable"/>
      <el:author  el:name="Ronald Lamprecht"/>
      <el:copyright>Copyright © 2006,2009 Ronald Lamprecht</el:copyright>
      <el:license el:type="GPL v2.0 or above" el:open="true"/>
      <el:compatibility el:enigma="1.10"/>
      <el:modes el:easy="false" el:single="true" el:network="false"/>
      <el:score el:easy="-" el:difficult="-"/>
    </el:info>
    <el:luamain><![CDATA[
ti[" "] = {"fl_lawn_b"}
ti["#"] = {"st_box"}
ti["o"] = {"st_oxyd"}
ti["@"] = {"#ac_marble"}

wo(ti, " ", {
    "####################",
    "#                  #",
    "#  o      @     o  #",
    "#                  #",
    "####################",
})
    ]]></el:luamain>
    <el:i18n/>
  </el:protected>
</el:level>

Несложно заметить, что XML-часть содержит все привычные метаданные, поставляемые автором уровня. Это, по сути, формула, которую можно скопировать из шаблона и заполнить.

Код Lua вставлен в XML. Единственное ограничение для Lua-части состоит в том, что для обозначения конца она резервирует ‘]]>’ и может понадобиться заменить его на ‘]] >’. Больше никаких ограничений нет.

Поскольку приведённый выше пример включает в себя все обязательные части XML, скорее всего, нам больше не потребуется вносить существенные изменения для авторов уровней на Lua (как мы того и хотели).

Приведённый пример можно найти в пакете уровней ‘Experimental’ из группы ‘Development’. Исходный код находится в поддиректории ‘levels/enigma_experimental’ системного пути (см. раздел Размещение ресурсов).

Если вы пробуете себя в программировании, используя копию этого уровня в качестве шаблона, то добавьте её в папку Auto (см. раздел Начало работы с пакетами уровней) либо используйте как аргумент в командной строке (см. раздел Опции запуска).

В следующих разделах мы остановимся на подробностях форматирования и поясним необязательные части.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

3.1 Общее знакомство с уровнями

Скорее всего, вам хочется понять основные принципы расположения объектов на уровне. Здесь приведено описание очень простого уровня, которое также может служить отправной точкой для новых уровней. (Вообще-то, это первый уровень Enigma, так что можете попробовать сыграть в него прямо сейчас.)

 
 1    ti[" "] = {"fl_gravel"}
 2    ti["#"] = {"st_box"}
 3    ti["O"] = {"st_oxyd"}
 4    if wo["IsDifficult"] then
 5        ti["Q"] = {"st_quake", name="quake"}
 6        ti["T"] = {"st_timer", interval=10.0, target="quake"}
 7    else
 8        ti["Q"] = ti[" "]
 9        ti["T"] = ti[" "]
10    end
11    ti["@"] = {"ac_marble_black", 0.0, 0.5}
11
12    wo(ti, " ", {
13	"####################",
14	"#                  #",
15	"#                  #",
16	"#  O            O  #",
17	"#         @        #",
18	"#                  #",
19	"#        QT        #",
20	"#                  #",
21	"#                  #",
22	"#  O            O  #",
23	"#                  #",
24	"#                  #",
25	"####################"})

Результат выглядит в игре следующим образом:

images/first_level

Давайте теперь проанализируем эту программу строка за строкой:

 
 1    ti[" "] = {"fl_gravel"}
 2    ti["#"] = {"st_box"}
 3    ti["O"] = {"st_oxyd"}

Сначала мы объявляем несколько кодов для объектов, которые мы хотим использовать на нашей карте уровня. Просто добавляем каждый код в наше хранилище секций ti и ставим ему в соответствие описание объекта секции, которая в простейшем случае состоит из вида объекта. Двухсимвольная приставка указывает на тип объекта, такой как покрытие, предмет, камень, актёр и т.п.

 
 4    if wo["IsDifficult"] then
 5        ti["Q"] = {"st_quake", name="quake"}
 6        ti["T"] = {"st_timer", interval=10.0, target="quake"}
 7    else
 8        ti["Q"] = ti[" "]
 9        ti["T"] = ti[" "]
10    end

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

В сложном режиме мы задаем значения двум описаниям камней. Каждое представляет собой описание камня с дополнительными атрибутами. ‘st_quake’ — это камень, который после прикосновения к нему или срабатывания переключателя закрывает все камни-оксиды. Для того, чтобы к нему можно было обратиться впоследствии, просто назначаем ему имя. Второй камень — таймер, который должен запускаться каждые 10 секунд и посылать своей цели, закрывающему оксиды ‘st_quake’, сообщение о том, чтобы он сменил своё состояние. Поскольку мы присвоили этому камню имя, здесь мы можем использовать в качестве цели его имя.

 
11    ti["@"] = {"ac_marble_black", 0.0, 0.5}

Теперь мы просто объявляем нашего актёра. Это чёрный шарик, который следует расположить не в левом верхнем углу решётки, а посредине левой границы решётки секций. В принципе, мы хотим просто поместить его в центре уровня. Поскольку размеры одноэкранного уровня составляют 20 x 13, нам следует использовать смещения, указанные выше.

 
12    wo(ti, " ", {
13        "####################",
14        "#                  #",
15        "#                  #",
16        "#  O            O  #",
17        "#         @        #",
18        "#                  #",
19        "#        QT        #",
20        "#                  #",
21        "#                  #",
22        "#  O            O  #",
23        "#                  #",
24        "#                  #",
25        "####################"})

Теперь можно создать мир, просто указав его карту. Необходимо только вызвать ‘wo’, представление нашего мира, указав для него преобразование секций, код покрытия по умолчанию и карту кодов секций.

Необходимую теоретическую информацию можно найти в разделе Ключевые понятия Enigma, а дополнительные примеры и информацию о синтаксисе в разделе Lua API. Но сперва следует разобраться с метаданными уровня на основе XML.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

3.2 XML-структура уровня

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

 
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<el:level xsi:schemaLocation="http://enigma-game.org/schema/level/1 level.xsd http://enigma-game.org/schema/editor editor.xsd" xmlns:el="http://enigma-game.org/schema/level/1" xmlns:ee="http://enigma-game.org/schema/editor">
  <el:protected>
    <el:info el:type="level">
      <!-- требуемые элементы пропущены -->
    </el:info>
    <el:elements/>
    <el:luamain><![CDATA[
    ]]></el:luamain>
    <ee:editor/>
    <el:i18n/>
  </el:protected>
  <el:public>
    <el:i18n/>
    <el:upgrade/>
  </el:public>
</el:level>

Первая строка — это объявление XML. Не считая задания кодировки, оно фиксировано. На всех платформах Enigma поддерживает по крайней мере ‘US-ASCII’, ‘UTF-8’, ‘UTF-16’, ‘ISO-8859-1’, ‘windows-1252’. Введите вашу кодировку и убедитесь, что ваш редактор сохраняет уровень в этой кодировке. В некоторых редакторах можно начать работу в режиме ASCII, скопировать скелет уровня с другим объявлением кодировки, например UTF-8, сохранить уровень в том же режиме ASCII и заново открыть файл. После этого редактор может автоматически обнаружить объявление XML и автоматически переключиться на указанную кодировку. Имейте в виду, что если не вводить в уровне строки на языке, отличном от английского, о кодировке можно вообще не беспокоиться. В этом случае можно выбрать UTF-8.

Несколько дополнительных замечаний для новичков в XML: теги разметки XML очень похожи на теги HTML. Но для каждого начального тега ‘<element>’ XML нуждается в соответствующем теге завершения ‘</element>’. Для элементов, у которых есть только атрибуты, но нет содержимого, следует использовать альтернативную запись пустого элемента ‘<element/>’. Заметим, что когда мы определяем элемент как пустой или с состоянием, которое лишает его содержимого, между начальным и завершающим тегом не разрешается использовать ни одиночный пробел, ни даже конец строки. Во избежание ошибок используйте запись пустого элемента.

Мы используем приятный глазу формат записи с отступом в 2 символа. Каждый элемент начинается с отдельной строки. У элементов с текстовым содержимым на той же строке есть завершающий тег. Только у элементов с дочерними элементами завершающий тег находится на отдельной строке с таким же отступом.

Такой формат не обязателен. Можно даже вставлять разрывы строки в текстовое содержимое, метки и даже значения атрибутов. Но имейте в виду, что каждый разрыв строки во время парсинга (обработки интерпретатором) XML будет заменяться пробелом. Учитывайте это во избежание ошибок или просто используйте длинные строки.

Идентификатор пространства имён предваряет все названия тегов и атрибутов. В уровнях Enigma в качестве сокращения мы используем ‘el’. Этот префикс используют все названия тегов, которые можно отредактировать вручную.

Наконец, короткий комментарий о зарезервированных символах XML, ‘&’ и ‘<’. Эти два символа зарезервированы как начальные символы тега и объекта. Если они понадобятся в текстовом содержимом или значениях атрибутов, их следует заменить соответственно объектами ‘&amp;’ и ‘&lt;’. К тому же, значения атрибутов следует заключать между ‘"’ или ‘'’. Безусловно, необходимо заменять и кавычки, используемые в значениях атрибутов. Используйте ‘&quot’ и ‘&apos’.

Элементы:

/level, необходим, уникальный

Это основной элемент. В каждом файле он присутствует только в одном экземпляре. Как и первая строка объявления XML, эта вторая строка довольно фиксирована. Существует две её версии. Простая версия с 3 атрибутами, которая использовалась в первом примере, и версия предназначенная только для редакторов уровней, использующая 4 атрибута, как в последнем примере. Для ручного редактирования уровня просто скопируйте простую версию во вторую строку своего файла уровня.

Атрибуты:

xmlns:xsi, необходим, фиксированное содержимое

Определение пространства имён схемы. Содержимое установлено в “http://www.w3.org/2001/XMLSchema-instance”. Тег атрибута ‘xsi’ должен соответствовать префиксу следующего тега атрибута и является стандартным.

xsi:schemaLocation, необходим, фиксированное содержимое

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

xmlns:el, необходим, фиксированное содержимое

Определение пространства имён для “уровня Enigma”. В качестве префикса пространства имён всех тегов элементов и атрибутов уровня мы, как правило, используем ‘el’. Используемый префикс может быть произвольным, но должен соответствовать своему тегу атрибутов. Содержимое атрибута зависит от пространства имён уровня Enigma.

xmlns:ee, необязательный

Последнее определение пространства имён используют только редакторы уровней. Например, мы объявили ‘ee’ как префикс пространства имён для всех тегов элементов и атрибутов редактора. Используемый префикс может быть произвольным, но должен соответствовать своему тегу атрибутов. Содержимым атрибута является пространство имён редактора.

/level/protected, необходим, уникальный

Защищённый раздел содержит все данные уровня, полученные от автора, которые не может изменять никто другой.

/level/protected/info, необходим, уникальный

Информационный раздел содержит все метаданные уровня. Он обязателен и детально описан в разделе Информация о метаданных.

/level/protected/elements, необязательный, уникальный

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

/level/protected/luamain, необязательный, уникальный

Раздел luamain является частью для ручной вставки определений уровня Lua. Он подробно описан в разделе Код LUA.

/level/protected/editor, необязательный, уникальный

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

/level/protected/i18n, необходим, уникальный

Раздел интернационализации содержит строки на английском, переводы на другие языки и комментарии, предоставляемые переводчикам самим автором. Это обязательный раздел, и он подробно описан в разделе Интернационализация (i18n).

/level/public, необязательный, уникальный

Общедоступный раздел — это необязательное расширение защищённого раздела. Он содержит информацию не подтверждённую автором и даже может быть добавлен после последнего пересмотра самим автором.

/level/public/i18n, необязательный, уникальный

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

/level/public/upgrade, необязательный, уникальный

Раздел обновления является частью системы Обновление и дополнение.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

3.3 Информация о метаданных

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

Далее мы рассмотрим все поддерживаемые информационные подразделы с их типичными атрибутами:

 
<el:info el:type="level">
  <el:identity el:title="Demo I18N" el:subtitle="Translate or let it be translated" el:id="20060211ral002"/>
  <el:version el:score="1" el:release="1" el:revision="0" el:status="experimental"/>
  <el:author  el:name="Ronald Lamprecht" el:email="ral@users.berlios.de"/>
  <el:copyright>Copyright © 2006 Ronald Lamprecht</el:copyright>
  <el:license el:type="GPL v2.0 or above" el:open="true"/>
  <el:compatibility el:enigma="0.92"/>
  <el:modes el:easy="false" el:single="true" el:network="false"/>
  <el:comments/>
  <el:update el:url="http://…"/>
  <el:upgrade el:url="http://…" el:release="2"/>
  <el:score el:easy="-" el:difficult="-"/>
</el:info>

Атрибуты:

type, необходим, значения = “level”, “library”, “multilevel”

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

level’ предназначен для всех определений отдельного уровня. Не имеет значения, редактируется ли он вручную или с помощью редактора уровней и какое используется описание элементов.

library’ предназначен для описания частей уровня, которые могут быть включены в другой уровень. Библиотека состоит только из кода Lua в разделе luamain. Библиотеки могут использовать почти все разделы кроме ‘/level/protected/info/score’ и ‘/level/*/i18n’, которые должны предоставляться, но не могут использоваться. Библиотеки включаются в уровни с помощью элемента зависимости (см. раздел <compatibility>).

multilevel’ предназначен для описания сразу нескольких уровней. Основная цель — поддержка форматов уровней сторонних игр, таких как формат уровней Сокобан, которые обычно описывают в одном файле целый набор карт уровней (см. раздел Многоуровневые файлы).

quantity, необязательный

Количество уровней в файле, содержащем несколько уровней (см. раздел Многоуровневые файлы).

Содержимое — элементы:

identity, необходим

Заголовок, подзаголовок и основная строка описания уровня (см. раздел <identity>).

version, необходим

Все аспекты версии уровня (см. раздел <version>).

author, необходим

Вся предоставленная автором информация о себе (см. раздел <author>).

copyright, необходим

Сообщение об авторском праве на уровень (см. раздел <copyright>).

license, необходим

Информация об условиях лицензии (см. раздел <license>).

compatibility, необходим

Вся информация о совместимости с версиями Enigma, зависимостях от библиотек, внешних данных и редакторе, в котором был создан уровень (см. раздел <compatibility>).

modes, необходим

Режимы, которые поддерживает уровень, такие как сложность, сетевой режим и контроль (см. раздел <modes>).

comments, необязательный

Необязательные комментарии, такие как благодарности, посвящения и комментарии по коду (см. раздел <comments>).

update, необязательный

Обновление и дополнение

upgrade, необязательный

Обновление и дополнение

score, необходим

Результат самого автора уровня (см. раздел <score>).


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

3.3.1 <identity>

Элемент ‘identity’ необходим, т.к. он предоставляет информацию для идентификации уровня человеком и системой.

 
<el:identity el:title="Demo I18N" el:subtitle="Translate or let it be translated" el:id="20060211ral002"/>

Атрибуты:

title, необходим

Английское название уровня. Строка может содержать произвольные символы, которые могут быть отображены шрифтом Enigma и поддерживаются XML. В многоуровневых файлах заключительный знак "#" имеет особое значение. Название не должно быть слишком длинным, потому что Enigma использует его в меню выбора уровня. Переводы названия можно предусмотреть в разделе Интернационализация (i18n).

subtitle, необязательный

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

id, необходим

Это основная строка идентификации уровня, которая всегда актуальна, независимо от последующих обновлений выпуска. Данная строка не должна содержать пробелов, скобок и масок (‘*? ()[]{}’. Главное требование Enigma к id в том, чтобы он был уникальным для всех уровней, созданных всеми авторами по всему миру, и не заканчивался закрывающей квадратной скобкой.

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

ГГГГММДДпользовательNNN

Где ‘ГГГГ’,‘ММ’,‘ДД’ — это дата создания первой экспериментальной версии, ‘пользователь’ заменяется уникальным именем автора, а ‘NNN’ случайным числом. Например, у моего уровня под названием ‘Houdini’ такой id: ‘20060816ral719’. Конечно, у всех уровней созданных в один день случайное число должно отличаться. id — это системный идентификаток уровня Enigma, и пользователь никогда в нём не нуждается.

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

Содержимое:

Этот элемент пуст — его нельзя заполнять.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

3.3.2 <version>

Этот элемент предоставляет системе информацию о версии.

 
<el:version el:score="1" el:release="1" el:revision="0" el:status="experimental"/>

Атрибуты:

score, необходим

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

При создании уровня, следует пользоваться атрибутом ‘status’, чтобы пометить уровень как неготовый. Когда автор сменит ‘status’ на ‘released’, он должен проверить сопоставимость результатов и, при необходимости, увеличить номер версии.

Этот атрибут — логический эквивалент атрибута ‘revision’ файла ‘index.txt’ в Enigma 0.92.

release, необходим

Технический выпуск версии задаётся как положительное целое число. Новые уровни начинают с выпуска под номером "1". Если изменения в уровне вызывают техническую несовместимость с предыдущими выпусками Enigma или несовместимость номеров версии, то следует увеличить номер выпуска версии.

Основной причиной технической несовместимости может стать изменение движка Enigma. Поскольку такая коррекция не будет работать на старой версии Enigma, версии уровней должны отличаться различными номерами выпуска.

И в случае технической несовместимости, и в случае несовместимости номеров версий также должно быть изменено имя файла уровня. Это необходимо, потому что на некоторые системы одновременно могут быть установлены различные версии Enigma. Они нуждаются в одновременном доступе к обеим версиям уровня. Также одновременный доступ к различным версиям уровней Enigma должны предоставлять интернет-сервера.

Чтобы дать игрокам возможность пользоваться различными выпусками файлов уровней, мы настоятельно рекомендуем использовать следующий стандарт именования уровней ИдентификаторАвтораНомерУровня_НомерВыпуска.Суффикс, где номер уровня состоит по крайней мере из 2 цифр; например, ‘ral01_2.xml

revision, необходим

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

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

Хотя издание задается числом, атрибут может получить другую строку в виде ключевого слова ‘$Revision: 1.19 $’. Этот формат Subversion позволяет репозиторию Subversion авторов уровней автоматически вставлять номер издания уровня. Они должны просто установить значение ‘svn propset svn:keywords "Revision" level.xml’ для каждого файла уровня в своем репозитории. Поскольку номер издания Subversion — это постоянно увеличивающаяся величина, она удовлетворяет нашим критериям. Заметьте, что Enigma не требует, чтобы номера издания шли по порядку.

status, необходим, значения = “released”, “stable”, “test”, “experimental”

Этот атрибут описывает качество уровня во время разработки. Enigma использует состояние для защиты базы данных результатов от искажений непредусмотренными результатами решения. Она записывает только результаты уровней, помеченных как ‘released’.

К сведению авторов уровней: при изменении уже выпущенного уровня следует снова сменить его статус на ‘experimental’. Затем внесите изменения и протестируйте уровень. Когда будете абсолютно уверены, что не добавили никаких спойлеров, то можете снова выпустить уровень с новым изданием, а может, и новым выпуском или номером версии.

Содержимое:

Этот элемент пуст — его нельзя заполнять.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

3.3.3 <author>

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

Элемент автора может выглядеть следующим образом:

 
<el:author  el:name="Ronald Lamprecht" el:email="ral@users.berlios.de" el:homepage="http://myhomepage.domain"/>

Атрибуты:

name, необязательный, по умолчанию = “anonymous”

Имя автора в том виде, в каком оно будет показываться на странице информации об уровне и в начале уровня. По умолчанию это имя ‘anonymous’.

email, необязательный

Электронная почта автора, новостная лента или форум, которые он просматривает. В основном это указание о том, как связаться с ним/ней. Значение просто показывается в виде строки на странице информации об уровне.

homepage, необязательный

Домашняя страница автора или место, где он размещает дополнительные уровни Enigma. Значение просто показывается в виде строки на странице информации об уровне.

Содержимое:

Этот элемент пуст — его нельзя заполнять.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

3.3.4 <copyright>

Стандартизованное расположение сообщения об авторских правах:

 
<el:copyright>Copyright © 2006 Ronald Lamprecht</el:copyright>

Атрибуты:

отсутствует

Содержимое:

Уведомление об авторских правах.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

3.3.5 <license>

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

 
<el:license el:type="GPL v2.0 or above" el:open="true"/>

Атрибуты:

type, необходим

Короткое определение типа лицензии, с необязательной ссылкой на текст лицензии или строкой ‘special’, если автор приводит в содержимом этого элемента свою собственную лицензию.

open, необходим

Булево значение, показывающее, удовлетворяет ли выбранная лицензия критериям Open Source Initiative (OSI). Пожалуйста, имейте в виду, что значение ‘false’ может помешать распространению вашего уровня с Enigma.

Содержимое:

В качестве содержимого этого элемента допускается использовать полный текст лицензии. Воспользуйтесь атрибутом ‘type’, чтобы определить уровень.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

3.3.6 <compatibility>

 
<el:compatibility el:enigma="0.92" el:engine="enigma">
  <el:dependency el:path="lib/natmaze" el:id="lib/natmaze" el:release="1" el:preload="true" el:url="http://anywhere.xxx/mypage/natmaze.xml"/>
  <el:externaldata el:path="./extfile" el:url="http://anywhere.xxx/mypage/extdata.xml"/>
  <el:editor el:name="none" el:version=""/>
</el:compatibility>

Атрибуты:

enigma, необходим

Минимальный номер выпуска Enigma, с которым совместим уровень.

engine, необязательный, значения = “enigma”, “oxyd1”, “per.oxyd”, “oxyd.extra”, “oxyd.magnum”; по умолчанию = “enigma”

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

Содержимое — элементы:

Сам элемент совместимости в качестве содержимого включает только подэлементы.

dependency, необязательный, несколько экземпляров

Этот элемент используется для указания библиотеки Enigma-Lua, от которой зависит этот уровень. Используя несколько экземпляров данного элемента, можно указать несколько библиотек. Если библиотека настроена на предварительный запуск, движок загрузит её перед загрузкой или исполнением любого кода Lua, используемого в уровне. Порядок загрузки нескольких библиотек основывается непосредственно на порядке элементов зависимостей.

Атрибуты:

path, необходим

Путь к ресурсам библиотеки без её суффикса и любого расширения. Большинство библиотек Enigma хранит в поддиректории ‘lib’ своей директории ‘levels’, в большинстве случаев путь к ресурсам будет примерно таким: ‘lib/ant’. Это действительный путь к файлу библиотеки, который может иметь вид ‘levels/lib/ant.xml’, ‘levels/lib/ant.lua’ или ‘levels/lib/ant_1.xml’.

Однако, библиотеки могут быть и полностью зависимыми от пакета уровней. В этом случае можно выбрать относительный путь, такой как ‘./mylib’, и хранить библиотеку в самой директории пакета уровней.

id, необходим

Это независимый от версии id библиотеки, указанный в её метаданных. Во избежание проблем Enigma проверит данный атрибут при загрузке библиотеки и, вместе с номером выпуска версии, может использовать для определения перемещённых библиотек.

release, необходим

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

preload, необходим

Булево выражение, которое указывает, должна ли библиотека загружаться предварительно. Если библиотека не является предварительно загружаемой, её всё равно можно загрузить с помощью кодовых выражений Lua. Но даже эти библиотеки должны быть определены, т.к. Enigma проверит их на соответствие. При использовании раздела ‘elements’ всегда нужно предварительно загружать библиотеки.

url, необязательный

Этот необязательный атрибут позволяет указать для библиотеки запасной адрес. Это будет полезно при использовании новых библиотек, ещё не поставляемых с системой.

Во время разработки и тестирования новых библиотек разработчик может распространять пробные уровни с пустым атрибутом пути к ресурсам ‘library’. Пробные уровни загрузят новейшую версию библиотеки, которая указана в полученном URL.

Содержимое:

отсутствует

externaldata, необязательный, несколько экземпляров

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

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

Атрибуты:

path, необходим

Путь к ресурсам внешнего файла данных без расширения ‘.txt’. Путь должен иметь формат "./name" для внешнего файла данных, который хранится локально в той же папке, что и файл уровня, или будет сохранён по данному адресу при загрузке, или "externaldata/name" для общих внешних файлов данных, на которые ссылаются различные файлы уровней, хранящиеся в разных папках. Внешний файл данных может храниться локально или сохраняться в папке "levels/externaldata". В любом случае локальное имя внешнего файла данных будет иметь расширение ‘.txt’, чтобы обозначить его как читаемый, но не исполняемый для локальной операционной системы.

url, необязательный

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

Содержимое:

отсутствует

editor, необязательный, уникальный

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

Атрибуты:

name, необходим

Название редактора уровней.

version, необходим

Номер версии редактора, полученный в виде строки .

Содержимое:

отсутствует

Содержимое:

отсутствует


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

3.3.7 <modes>

Элемент ‘modes’ позволяет автору указать поддерживаемый режим и режим уровня по умолчанию. Движок Enigma проверяет, используется ли уровень в поддерживаемом режиме.

 
<el:modes el:easy="false" el:single="true" el:network="false" el:control="force" el:scoreunit="duration" el:scoretarget="time"/>

Атрибуты:

easy, необходим, значения = “true”, “false”

Если уровень поддерживает второй, упрощённый, режим, установите этот атрибут в ‘true’. Если поддерживается только сложный режим, установите атрибут в ‘false’.

single, необходим, значения = “true”, “false”

Если уровень, как и стандартные, предоставляет возможность одиночной игры, установите этот атрибут в ‘true’. Установите этот атрибут в ‘false’, только если уровень представляет собой сетевую игру для 2 игроков.

network, необходим, значения = “true”, “false”

Если уровень предоставляет возможность сетевой игры для 2 игроков, установите этот атрибут в ‘true’. В противном случае установите этот атрибут в ‘false’.

control, необязательный, значения = “force”, “balance”, “key”, “other”; по умолчанию = “force”

Этот атрибут задаёт для уровня стандартный режим управления. На уровне можно играть, пользуясь мышью, движение которой влияет на положение шариков: это стандартный способ, и он был единственным вплоть до Enigma 0.92. Или же можно играть на уровне, используя мышь или другие устройства ввода для балансировки шариками в мире уровня. Кроме того, можно использовать клавиши со стрелками на клавиатуре, чтобы перемещать актёра, как в классических играх Сокобан.

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

scoreunit, необязательный, значения = “duration”, “number”; по умолчанию = “duration”

Этот атрибут задаёт режим подсчёта и отображения результатов. В режиме по умолчанию, ‘duration’ результаты интерпретируются как время решения уровня и отображаются в формате ММ:СС. Режим ‘number’ отображает результаты в виде обычных чисел, а меньшие числа означают лучшие результаты. Этот режим подходит для подсчета толчков и шагов.

scoretarget, необязательный, значения = “time”, “pushes”, “moves”, *; по умолчанию = “time”

Целевое значение задаёт способ подсчёта баллов. При выборе ‘time’ учитывается время решения, при выборе ‘pushes’ — количество толчков камней, при выборе ‘moves’ — шаги актёра. Любое другое значение вызовет функцию Lua для подсчёта результатов. Цель в пользовательском интерфейсе представлена в виде короткого наименования для результатов.

Содержимое:

отсутствует


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

3.3.8 <comments>

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

 
<el:comments>
    <el:credits el:showinfo="true" el:showstart="false">Thanks to the author of my favorite libs</el:credits>
    <el:dedication el:showinfo="true" el:showstart="false">To a honorable or a beloved person</el:dedication>
    <el:code>some important general notes</el:code>
</el:comments>

Атрибуты: отсутствуют

Содержимое — элементы:

Сам элемент комментариев в качестве содержимого включает только подэлементы.

credits, необязательный, уникальный

Место для того, чтобы поблагодарить людей, помогавших вам при создании уровня.

Атрибуты:

showinfo, необязательный, по умолчанию = “false”

Значение ‘true’ покажет сообщение на странице информации об уровне.

showstart, необязательный, по умолчанию = “false”

Значение ‘true’ покажет сообщение при запуске уровня. Пожалуйста, используйте эту возможность только в исключительных случаях.

Содержимое:

Сами благодарности. Он может быть разделён на несколько строк. Перед отображением лишние пробелы убираются.

dedication, необязательный, уникальный

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

Атрибуты:

showinfo, необязательный, по умолчанию = “false”

Значение ‘true’ покажет сообщение на странице информации об уровне.

showstart, необязательный, по умолчанию = “false”

Значение ‘true’ покажет сообщение при запуске уровня. Пожалуйста, используйте эту возможность только в исключительных случаях.

Содержимое:

Само посвящение. Он может быть разделён на несколько строк. Перед отображением лишние пробелы убираются.

code, необязательный, уникальный

Атрибуты:

отсутствуют

Содержимое:

Главный комментарий к коду, который может представлять собой пояснение состояния <version> или список запланированных изменений. Он может быть разделён на несколько строк. Этот комментарий не обрабатывается.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

3.3.9 <score>

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

 
<el:score el:easy="01:07" el:difficult="-"/>

Атрибуты:

easy, необходим

Время решения уровня в упрощённом режиме. Формат: ММ:СС, где ММ означает минуты, а СС секунды, либо - если автор ещё не решил свой уровень. Для уровней с единицей измерения результатов ‘number’ значение должно быть числом шагов шарика или толчков.

difficult, необходим

Время решения уровня в сложном режиме. Формат: ММ:СС, где ММ означает минуты, а СС секунды, либо - если автор ещё не решил свой уровень. Для уровней с единицей измерения результатов ‘number’ значение должно быть числом шагов шарика или толчков.

Содержимое:


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

3.4 Код LUA

Этот элемент включает в себя любой код Lua в виде единого блока почти без ограничений:

 
    <el:luamain><![CDATA[
levelw = 20
levelh = 13

create_world( levelw, levelh)
draw_border("st-wood")
fill_floor("fl-leavesb", 0,0,levelw,levelh)

oxyd( 4,4)
oxyd( 14,4)

document(5,10,"hint1")
document(10,10,"hint2")
document(10,5,"Heureka!")
set_actor("ac-blackball", 4, 11)
    ]]></el:luamain>

Атрибуты:

отсутствуют

Содержимое:

В качестве содержимого этого элемента выступает основной код Lua.

Все остальные библиотеки, объявленные как зависимости, и поставляемые элементами XML фрагменты кодаLua загружаются предварительно, как описано в разделе <compatibility>. Обычно для того, чтобы загрузить библиотеки, нет необходимости в использовании таких функций Lua как ‘Require’. В случае, если нужно контролировать точку входа в процедуру, где должна загружаться библиотека, можно объявить библиотеку с атрибутом ‘el:preload="false"’. Чтобы загрузить библиотеку следует использовать новую функцию @ref{enigma.LoadLib}.

Код Lua — это код, заключённый в разделе XML под названием CDATA. Это накладывает на код Lua ограничения в виде невозможности использования завершающего тега ‘]]>’. Любое его вхождение должно быть заменено на ‘]] >’.

С другой стороны, формат XML расширяет возможности использования кодировок в Lua. В строках и комментариях Lua можно использовать умляуты, но идентификаторы Lua всё же ограничиваются чистым US-ASCII. Преимущество в том, что допускается использовать умляуты и другие символы, отсутствующие в ASCII, в подсказках it-document.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

3.5 Интернационализация (i18n)

Интернационализация уровней стала движущей силой изменений в формате уровней. Можно заметить, что существует два элемента ‘i18n’: один в защищённом авторском разделе и один в общедоступном. Рассмотрим, как использовать их для интернационализации трёх документов нашего уровня ‘demo_i18n.xml’:

 
  <el:protected >
    <!-- elements omitted -->
    <el:i18n>
      <el:string el:key="title">
        <el:english el:translate="false"/>
      </el:string>
      <el:string el:key="subtitle">
        <el:english el:translate="true"/>
        <el:translation el:lang="de">Ьbersetzten oder ьbersetzten lassen</el:translation>
      </el:string>
      <el:string el:key="hint1">
        <el:english el:comment="Let 'right' be ambiguous: correct and opposite of left - if not possible choose correct">Read the right document</el:english>
        <el:translation el:lang="de">Lies das rechte Dokument</el:translation>
      </el:string>
      <el:string el:key="hint2">
        <el:english el:comment="the correct one and not the right positioned one">The right one, not the right one!</el:english>
        <el:translation el:lang="de">Das rechte, nicht das rechte</el:translation>
      </el:string>
      <el:string el:key="Heureka!">
        <el:english el:translate="false">Heureka!</el:english>
      </el:string>
    </el:i18n>
  </el:protected>
  <el:public>
    <el:i18n>
      <el:string el:key="hint1">
        <el:translation el:lang="fr">Lisez la document de droite</el:translation>
      </el:string>
    </el:i18n>
  </el:public>

Два документа для обращения к строке используют ключевые слова. Последний использует в качестве ключа непосредственно английскую строку. Существует ещё два дополнительных зарезервированных ключа: ‘title’ и ‘subtitle’.

Для каждой переведённой строки или той, которая нуждается в переводе, мы определяем подэлемент защищённого раздела — ‘string’ и добавляем к самому элементу подэлемент ‘english’. Элемент ‘string’ получает один обязательный атрибут — код строки. У элемента ‘english’ один обязательный атрибут ‘translate’ (который по умолчанию инициализируется значением ‘true’), отражающий решение автора о необходимости перевода строки. Если автор не желает, чтобы строка была переведена, он может и должен вообще не добавлять в эту строку элемент ‘string’. Таким образом, элементы для строк с ключами ‘title’ и ‘Heureka!’ необязательны и встречаются довольно редко.

title’ и ‘subtitle’ отображают текст на английском в элементе <identity>. Все остальные строки, к которым обращаются, используя их код, в качестве содержимого элемента ‘english’ должны включать в себя английский текст. Примерами служат ‘hint1’ и ‘hint2’.

Ввиду того, что мы выбрали довольно неясные английские тексты, переводчики, которые сами не играли в игру, могут перевести их неправильно. Во избежание ошибок автор уровня может добавить к элементу ‘english’ атрибут ‘comment’. Как мы увидим ниже, переводчик получит в своё распоряжение комментарий вместе со строкой на английском.

Если для автора английский язык не родной, он должен добавить к элементу ‘string’ свой подэлемент ‘translation’. У элемента ‘translation’ есть один обязательный атрибут ‘lang’, который принимает 2-буквенное сокращение языка. Содержимое этого элемента — сам перевод.

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

Необходимо упомянуть, что в общедоступном разделе у нас есть элемент ‘i18n’. В этом элементе содержатся предложения по переводу. Автор может самостоятельно добавить его для других известных ему языков. Они могут быть добавлены на пути уровня к пользователю или даже самим пользователем.

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

Формат аналогичен используемому в защищённом разделе, исключая неиспользуемые элементы ‘english’. Атрибут ‘key’ элемента ‘string’ должен точно совпадать с атрибутом ‘key’ соответствующего элемента ‘string’ из защищённого раздела. Тут есть одно трудноуловимое различие, вызванное причинами технического и практического характера. Атрибуты ‘key’ в общедоступном разделе должны быть идентификаторами XML; поэтому, нельзя предоставить общедоступный перевод строк, использующих фразу на английском в качестве кода. Чтобы избежать подобных проблем, выберите ключевое слово и вынесите английскую строку в общедоступный раздел ‘i18n’.

Элемент ‘string’ из защищённого и общедоступного разделов должен быть уникальным относительно атрибута ‘key’ в соответствующем разделе. Это значит, что переводы для всех известных языков следует добавить для строки в элементе ‘string’ из защищённого и общедоступного разделов. Порядок не имеет значения.

Давайте посмотрим, что получит переводчик для каждой из строк. Давайте начнём с ‘hint2’ для немецкого переводчика:

 
#  level: "Demo Internationalization"
#  author: "Ronald Lamprecht" email "ral@users.berlios.de"
#  comment: "the correct one and not the right positioned one"
#  use: "Das rechte, nicht das rechte"
#: po/level_i18n.cc:17
msgid "The right one, not the right one!"
msgstr ""

msgid’ — это строка на английском. ‘msgstr’ принимает немецкий перевод. Но переводчику не надо ничего переводить, потому что автор предоставил перевод на немецкий в строке ‘# use:’.

Другим примером может послужить ‘hint1’ для французского переводчика:

 
#  level: "Demo Internationalization"
#  author: "Ronald Lamprecht" email "ral@users.berlios.de"
#  comment: "Let 'right' be ambiguous: correct and opposite of left - if not possible choose correct"
#  check: "Lisez la document de droite"
#: po/level_i18n.cc:14
msgid "Read the right document"
msgstr "Lisez le document de droite"

Здесь автор предоставляет общедоступный перевод в строке ‘# check:’. Так как в нём есть по крайней мере одна ошибка, переводчик должен его исправить, как показано в строке ‘msgstr’.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

3.6 Использование

После сухой теории давайте посмотрим, как обращаться с форматом уровней XML на практике. Конечно, не обязательно заново собирать воедино метаданные XML для каждого нового уровня, который будет писаться. Целесообразнее использовать шаблоны. Можно начать с любого существующего уровня, например ‘demo_i18n.xml’, поставляемого с этой документацией. Добавьте в него свои личные данные и храните его в качестве шаблона для всех новых уровней, которые вы напишете.

Некоторые авторы уровней хорошо знакомы с форматом файлов Lua, так как их любимый редактор поддерживает подсветку синтаксиса в этих файлах. Имя файла XML и элементы XML указывают их редактору использовать подсветку синтаксиса XML. Тем не менее, эти авторы привыкли вносить метаданные в заголовки своих уровней Lua в виде нестандартных комментариев Lua; мы решили добавить поддержку подобного формата XML, совместимого с Lua. Мы называем его "XML с комментариями Lua", потому что он просто комментирует все строки XML комментариями Lua — ‘--xml-- ’. Например:

 
--xml-- <?xml version="1.0" encoding="UTF-8" standalone="no" ?>
--xml-- <el:level xsi:schemaLocation="http://enigma-game.org/schema/level/1 level.xsd" xmlns:el="http://enigma-game.org/schema/level/1">
--xml--   <el:protected >
--xml--     <el:info el:type="level">
--xml--       <el:identity el:title="Demo Simple" el:id="20060210ral001"/>
--xml--       <el:version el:score="1" el:release="1" el:revision="0" el:status="stable"/>
--xml--       <el:author el:name="Ronald Lamprecht"/>
--xml--       <el:copyright>Copyright © 2006 Ronald Lamprecht</el:copyright>
--xml--       <el:license el:type="GPL2" el:open="true">GPL v2.0 or above</el:license>
--xml--       <el:compatibility el:enigma="0.92"/>
--xml--       <el:modes el:easy="false" el:single="true" el:network="false"/>
--xml--       <el:score el:easy="-" el:difficult="-"/>
--xml--     </el:info>
--xml--     <el:luamain><![CDATA[
levelw = 20
levelh = 13

create_world( levelw, levelh)
draw_border("st-wood")
fill_floor("fl-leavesb", 0,0,levelw,levelh)

oxyd( 4,4)
oxyd( 14,4)

set_actor("ac-blackball", 4, 11)
--xml--     ]]></el:luamain>
--xml--     <el:i18n/>
--xml--   </el:protected>
--xml-- </el:level>

Пожалуйста, имейте в виду, что каждая строка с метаданными XML начинается именно с ‘--xml-- ’, 8 символов, включая пробел в конце. Дополнительное ограничение формата XML с комментариями Lua возникает из-за способности Lua хранить кодировки символов. Для успешного использования формата XML с комментариями Lua следует ограничиться ‘UTF-8’ или, конечно, ‘US-ASCII’. Пожалуйста, помните, что хотя часть XML комментируется Lua, она всё так же должна обрабатываться, а значит и быть верной.

Каждый уровень, хранящийся в формате XML с комментариями Lua, как файл с расширением ‘.lua’, может использоваться непосредственно в командной строке, равно как и в любом пакете уровней, который хранится в пользовательской домашней директории Enigma. Однако уровни в формате XML с комментариями Lua не могут храниться на серверах в интернете или обновляться в режиме реального времени. Поэтому этот формат хорош для создания уровней, но перед распространением их необходимо преобразовать в чистый формат XML. Пожалуйста, имейте в виду, что сначала Enigma ищет уровни XML, а уровни Lua использует, только если не найдёт уровень XML.

Ещё одним вариантом применения уровней в формате XML с комментариями Lua служит обратная совместимость с Enigma 0.92. Если уровни в этом формате не используют новые возможности Enigma, то их можно включить в пакеты уровней Enigma 0.92.

Так как вам может понадобиться несколько раз преобразовать уровни из формата XML в Lua и обратно, мы предлагаем инструменты преобразования: ‘xml2lua’ и ‘lua2xml’. Оба они — очень простые скрипты Lua 5, которые, если 5-я версия Lua установлена правильно, можно использовать следующим образом: ‘lua xml2lua demo_simple.xml > demo_simple.lua’. В системах Unix можно пометить скрипты как исполняемые и просто вводить ‘xml2lua demo_simple.xml > demo_simple.lua’.

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

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

Конечно, можно использовать и сторонний инструмент проверки корректности XML. Нужно только скопировать файл схемы ‘level.xsd’ в директорию, в которой находится сам уровень. Примером возможных инструментов контроля может быть программа Xerces-C ‘DOMPrint.exe -n -s -f -v=always level.xml’ или редакторы с проверкой корректности, такие как Exchanger XML Lite. Такие редакторы выделят во всех местах все возможные элементы и атрибуты.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

3.7 Обновление и дополнение

Поскольку мы указываем все необходимые атрибуты в элементе <version>, Enigma может загружать новые версии уровней.

Если Enigma загружает новые версии, у которых отличается только номер издания ‘revision’, мы подразумеваем ‘обновление’. Обновление может быть выполнено автоматически с заменой старых версий обновлёнными, поскольку автор гарантирует их совместимость в результатах и зависимостях. Автор должен предоставить адрес для загрузки автоматических обновлений в защищённом информационном элементе:

 
<el:update el:url="http://myLevelServer.org/path/level_1.xml"/>

Атрибуты:

url, необходим

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

Если автор уровня вносит в уровень элементы, приводящие к несовместимости, он увеличивает выпуск версии уровня и сохраняет файл уровня под новым именем. Мы называем загрузку такой версии нового уровня ‘дополнением’.

Чтобы сообщить о доступности выпуска дополнения, автор должен обновить предыдущий выпуск с последним номером издания, просто добавив элемент дополнения, сообщающий о новом выпуске:

 
<el:upgrade el:url="http://myLevelServer.org/path/level_2.xml" el:release="2"/>

Атрибуты:

url, необходим

Действительный в течение длительного времени, полный адрес для загрузки дополнений этого уровня. Путь к новому файлу.

release, необходим

Выпуск версии дополнения.

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

3.8 Использование библиотек

Библиотеки — это набор функций Lua для многократного использования во многих уровнях. Для использования библиотеки её необходимо объявить как зависимость, как описано в разделе <compatibility>. Предварительная загрузка библиотеки — это всё, что необходимо для её использования. Кроме того, для загрузки библиотеки в определенной точке выполнения можно использовать функцию @ref{enigma.LoadLib}.

Enigma предоставляет несколько очень полезных библиотек. Их можно найти по системному пути в поддиректории ‘levels/lib’. Большинство из них хорошо прокомментированы непосредственно в исходном коде. Для ‘ant’ существует отдельный файл с документацией: ‘doc/ant_lua.txt’.

В этом разделе, мы сконцентрируемся на аспектах написания и поддержки библиотек:


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

3.8.1 Написание библиотеки

Файлы библиотеки практически аналогичны файлам уровней. Основное отличие в атрибуте ‘el:type’ элемента ‘info’, который следует установить в ‘library’. Все остальные элементы и атрибуты должны быть такими же, как и для уровней. Конечно, никакие атрибуты, основанные на результатах, не будут учитываться и им следует присвоить значения по умолчанию.

Библиотеки могут зависеть от других библиотек, поэтому необходимо предоставить идентификационный номер и номер выпуска версии. Несколько выпусков библиотеки могут сосуществовать, и их можно обновить и дополнить, если предоставить необходимую информацию. Конечно, библиотеки могут содержать строки документов, которые могут быть локализованы, если вы предоставите элементы ‘i18n’.

Элемент ‘el:luamain’ принимает полный код Lua, как и для уровней. Давайте взглянем на важнейшие XML-части библиотеки:

 
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<el:level xsi:schemaLocation="http://enigma-game.org/schema/level/1 level.xsd" xmlns:el="http://enigma-game.org/schema/level/1">
  <el:protected >
    <el:info el:type="library">
      <el:identity el:title="" el:id="lib/ant"/>
      <el:version el:score="1" el:release="1" el:revision="0" el:status="released"/>
      <el:author  el:name="Petr Machata"/>
      <el:copyright>Copyright © 2002-2003 Petr Machata</el:copyright>
      <el:license el:type="GPL v2.0 or above" el:open="true"/>
      <el:compatibility el:enigma="0.92">
        <el:dependency el:path="lib/natmaze" el:id="lib/natmaze" el:release="1" el:preload="false">
      </el:compatibility>
      <el:modes el:easy="false" el:single="false" el:network="false"/>
      <el:score el:easy="-" el:difficult="-"/>
    </el:info>
    <el:luamain><![CDATA[
    …
    ]]></el:luamain>
    <el:i18n/>
  </el:protected>
</el:level>

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

3.8.2 Поддержка библиотеки

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

Для сосуществования эти различные выпуски библиотек должны следовать жёсткой схеме именования. У каждой библиотеки должно быть своё основное имя. В предыдущем примере это ‘lib/ant’. Имя файла данного конкретного выпуска — это основное имя с добавлением символа подчёркивания и номера версии плюс суффикс ‘xml’. Поэтому выпуск ‘lib/ant’ следует хранить в виде ‘lib/ant_2.xml’.

Если посмотреть в директорию lib, то можно обнаружить, что Enigma хранит большинство библиотек без добавления к основному имени номера выпуска. Это из-за поддержки совместимости форматов уровней Lua для версии 0.92. Допускается хранить один, и только один, выпуск каждой библиотеки без добавления к основному имени номера выпуска. Enigma загрузит эту версию из обычных уровней Lua, которые не предоставляют никакой информации о необходимом выпуске библиотеки.

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

3.9 Многоуровневые файлы

Ещё одним способом повторного использования кода, помимо файлов библиотек, являются многоуровневые файлы. Код, содержащийся в одном файле, может генерировать несколько уровней, называемых подуровнями, которые присутствуют в пакете в качестве автономных уровней. Конечно, данный способ является менее гибким, чем использование библиотек, поскольку другие файли не могут повторно использовать код. Но вы можете использовать данный способ, если вы написали большой объём специфического кода для сложного уровня, предусматривающего более двух вариантов (в противном случае эти варианты указывались бы как ‘difficult’ и ‘easy’ в элементе <modes>).

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

Но у многоуровневых файлов есть свои ограничения. Они используют единый набор метаданных XML. Поэтому эти метаданные должны подходить для всех уровней. Элемент <version> будет идентичным, поскольку он отражает версию кода оригинального уровня или версию файла, импортируемого из другой игры. Но другие данные, например <author>, <compatibility> и <modes>, также должны совпадать.. В противном случае использовать многоуровневый файл нельзя.

Для всех уровней, описанных в многоуровневом файле, могут (и должны) отличаться только значения ‘title’ и ‘id’. В игре предусмотрены специальные средства работы с этими атрибутами для многоуровневых файлов.

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

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

В элементе <identity> необходимо указать один уникальный идентификатор уровня. Enigma автоматически добавить строку "[1]" для первого подуровня, "[2]" — для второго и т. д. Таким образом, каждый подуровень будет иметь уникальный идентификатор.

Кроме того, в элементе <identity> необходимо предусмотреть общий заголовок для уровней. Если заголовок заканчивается знаком ‘#’, Enigma будет автоматически генерировать заголовки для подуровней, добавляя к общей строке заголовка номер подуровня.

Для отдельных подуровней заголовки необходимо предусмотреть в коде LUA. Заголовок, указываемый в элементе <identity>, не может оканчиваться знаком ‘#’ и будет использоваться только в качестве заголовка по умолчанию, если в коде LUA заголовок подуровня будет отсутствовать. Перед выполнением кода Lua осуществляется инициализация глобального атрибута SublevelNumber. Загрузка подходящего подуровня в коде Lua может осуществляться на основе данного номера любым из способов. Кроме того, код Lua в таком случае также должен задать значение ещё одного глобального атрибута многоуровневых файлов — SublevelTitle.

<compatibility> externaldata


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4. Ключевые понятия Enigma

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

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4.1 Структура мира

В данном разделе мы рассмотрим уровень как отдельное произведение и как единое целое и опишем первоначальное размещение компонентов игрового мира и его динамическое поведение во время игры. Давайте подробно рассмотрим задействованные в нём объекты:


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4.1.1 Очертания и координаты мира

Сыграв в несколько уровней, вы наверняка заметили, что каждый экран в игре разделён на квадратные секции — 20 по горизонтали и 13 по вертикали. Хотя игроку и бывает трудно определить очертания некоторых больших уровней, мир каждого уровня имеет форму прямоугольника. Тем не менее, некоторые части уровня игрок может никогда и не увидеть, потому что путь к ним ему преграждают каменные стены или водные барьеры.

При создании мира автор должен задать его размер в секциях. Заданные ширина и высота мира являются фиксированными и не могут быть изменены впоследствии. Типичный размер — 20x13 для одноэкранного уровня. Но здесь нет никаких ограничений. Можно создавать уровни даже меньшие, чем размер экрана. Обратите внимание, что при создании больших уровней необходимо учитывать тот факт, что один ряд или колонка клеток при прокрутке обычно делится между двумя экранами. Таким образом, уровень размером 2x2 экрана имеет размер 39x25 секций, 3x4 экрана — 58x49 секций и т. д.

Края секций образуют решётку, охватывающую мир уровня. Координаты левого верхнего угла мира уровня мы определяем как {0, 0}. Первая координата показывает смещение по горизонтальной оси вправо от начала координат, вторая — смещение по вертикальной оси вниз. В одноэкранном уровне координаты секции в нижнем правом углу — {19, 12}, в то время как координаты самого угла — {20, 13} (обратите внимание, что данная точка уже не относится к уровню).

Позицию актёра, например чёрного шарика, следует задавать двумя числами с плавающей запятой, например {1.5, 2.5} для актёра, расположенного в центре секции, находящейся на пересечении третьего ряда и второй колонки, считая от левого верхнего угла.

Но большинство объектов, таких как камни, можно разместить только в соответствии с фиксированными координатами, выраженными целыми числами. Даже если попытаться поместить камень в точку {1.5, 2.5}, он окажется в точке {1, 2}. Таким образом, если при задании координат учитываются только целые числа, мы говорим об участке решётки. Легко заметить, что позиция секции определяется по её верхнему левому углу. Верхняя и левая стороны и являются принадлежащими непосредственно данной секции, правая же и нижняя сторона принадлежат соседним секциям.

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4.1.2 Слои объектов

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

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

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

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

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

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4.1.3 Мир как объект

Трение, ломкость, режимы и прочее, режимы прокрутки.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4.1.4 Неразмещаемые объекты

Похоже, упущен по крайней мере один объект, который нельзя связать ни с конкретным местом на уровне, ни с одним из лежащих сверху слоев — резиновые ленты! На самом деле, кроме покрытий, предметов, камней и актеров в большом количестве существуют Остальные объекты, которые не требуется размещать на карте уровня. Кроме видимых резиновых лент и проволоки в мир можно добавить полезные гаджеты (gadgets), которые помогают составлять уровни простым добавлением новых элементов.

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4.1.5 Игрок и инвентарь

По своей концепции, Enigma — это игра для двух игроков. Тем не менее, в неё можно играть одному на единственном компьютере, переключая управление между двумя виртуальными игроками. Мы называем этих игроков Инь и Ян, поскольку первый игрок, как правило, управляет чёрным шариком, в то время как второй обычно управляет белым.

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

Инвентари игроков существуют вне прямоугольного мира. Поэтому у любого предмета из инвентаря игрока позиция относительно мира недействительна, что выражается в возвращаемом на запрос ‘exists()’ значения ‘false’. С помощью дополнительного метода мира add можно добавлять предметы непосредственно в инвентари.

Хотя актёры закреплены за игроками, они достаточно самостоятельны и живут в одном из слоёв объектов (см. раздел Слои объектов). С игроками они связаны следующим образом:

каждый виртуальный игрок может управлять одним или несколькими принадлежащими ему актёрами любого типа. Это значит, что игрок Инь не обязан всегда управлять чёрным шариком ac_marble, а с тем же успехом может управлять и белым шариком ac_pearl, лошадью ac_horse или любым другим произвольным набором объектов.

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

Назначение игрокам актёров однозначно регулируется атрибутами актёров (см. раздел Атрибуты актёров).

Если в Enigma играет один пользователь, то он начинает игру, управляя игроком Инь. Используя объекты Инь-Ян, он может переключиться на управление игроком Ян и обратно. При игре одним игроком на сетевых уровнях автоматически добавляются предметы it_yinyang. Они позволяют произвольно переключаться между игроками. Камни st_yinyang ограничивают способность пользователей переключаться между управлением различными игроками.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4.1.6 Подконтрольные объекты

Помимо объектов, принадлежащих игроку и входящих в его инвентарь, объекты могут временно входить в состав другого объекта. Наиболее очевидный пример — предмет, хранящийся в сумке it_bag. В качестве других примеров можно привести два камня сёгун st_shogun, расположенные на одном участке решётки, или, в течении короткого промежутка времени, камень, меняющийся местами с st_swap или st_pull.

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

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4.2 Описание объектов

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4.2.1 Тип объекта

До настоящего времени мы говорили о таких типах объектов как покрытие ‘fl’, предмет ‘it’, камень ‘st’ и актёр ‘ac’. Все эти типы являются абстрактными. Можно проверить, принадлежит ли данный объект к какому-либо типу, но нельзя создать экземпляр абстрактного типа.

Чтобы создать объект, для него необходимо задать определённое имя типа, например ‘st_switch’. Описания всех типов объектов приведены в соответствующих главах, начиная с главы Покрытия. Для всех имён типов с по крайней мере одним подчеркиванием можно создать экземпляры.

Большинство типов подразделяются на подтипы, такие как ‘st_switch_black’ и ‘st_switch_white’. В случае с переключателями, если не добавить суффикс, то будет получен переключатель, независимый от цвета шарика. В других случаях, например в случае со ‘st_chess’, при указании одного только общего типа будет получен заданный по умолчанию ‘st_chess_black’, так как не существует бесцветных шахматных камней.

Если запросить объект по его типу, то возвращаемое значение всегда будет самого подходящего типа. Таким образом, ‘st_chess’ вернет тип ‘st_chess_black’, в то время как ‘st_switch’ не изменяет своё имя.

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

Существует несколько специальных объектов, предназначенных исключительно для создания новых объектов. Обычно в их названиях присутствует суффикс ‘_new’. Такие объекты никогда не возвращают имя своего первоначального типа, а немедленно преобразуются к стандартному типу.

Если конкретный подтип не важен, то можно проверить объект на соответствие любому общему типу. Например, если проверить переключатель любого цвета на соответствие ‘st_switch’, он вернёт значение "true".


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4.2.2 Ссылка на объект

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

С помощью ссылки на объект особого типа Lua ‘object’ можно узнать текущее состояние и атрибуты объекта, модифицировать объект, посылать сообщения либо пользоваться любыми поддерживаемыми методами объекта.

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

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

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

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4.2.3 Именование объектов

Чтобы длительное время иметь возможность для обращений к объекту, каждому отдельному объекту можно назначить имя. Для этого необходимо только установить для объекта атрибут ‘name’ с уникальной строкой. Естественно, имя объекта можно запросить посредством чтения атрибута ‘name’.

Имя — это строка, которая должна состоять из символов ‘a..z’, ‘A..Z’, цифр ‘0..9’ и символа подчёркивания ‘_’. Прочие символы разрешены только в случаях, рассмотренных ниже.

Необходимо следить за тем, чтобы каждому объекту было дано уникальное имя. Если повторно использовать имя, уже ассоциированное с каким-либо объектом, то этот объект лишится имени, а само имя будет ассоциировано с новым объектом. Чтобы упростить именование больших групп однотипных объектов, можно добавить знак ‘#’ в качестве последнего символа имени, например ‘mydoor#’. Это даёт программе команду добавить к строке уникальный случайный номер. Поэтому автоматически именованные объекты никогда не будут разыменованы другим объектом, именованным позднее. Но если удалить автоматически именованный объект, такой как, например ‘mydoor#103284’, то такое же имя может быть присвоено другому объекту, созданному впоследствии.

Все именованные объекты регистрируются в хранилище именованных объектов. API предлагает переменную ‘no’ , которая позволяет получить ссылку на любой именованный объект, например ‘no["mylaser_a"]’. В таких случаях выдаётся Ссылка на объект или ‘nil’, если объект с данным именем отсутствует.

Поскольку существует возможность автоматически именовать группы объектов, допускается использование символов ‘?’ и ‘*’, заменяющих прочие символы. Вопросительный знак заменяет один произвольный символ, звёздочка — любое количество символов. Например, ‘no["mydoor#*"]’ выдаёт все автоматически именованные объекты ‘mydoor’ в отдельной группе объектов.

Многие атрибуты объектов, такие как ‘target’, ‘destination’, предполагают, требуют ссылок на другие объекты. Помимо временных ссылок на объекты (см. раздел Ссылка на объект), всегда можно создать строку имени как долгосрочную ссылку на объект. Если значением атрибута могут быть несколько объектов, можно ввести группу ссылок на объекты, список имён объектов или шаблон имени объекта с символами ‘?’ и ‘*’. Таким образом, строка ‘"mydoor#*"’ является допустимой целью.

Переключатели часто располагаются рядом с подконтрольными им объектами. Чтобы значительно упростить себе жизнь, на ближайший объект группы можно сослаться, добавив в начало его имени символ ‘@’.

 
ti["F"] = {"st_floppy", target="@door#*"}
ti["D"] = {"st_blocker", name="door#"}

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

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

Другая уникальная особенность имён объектов — это их предварительное объявление. Это даёт возможность ссылаться на ещё не существующий объект. Так, если требуется поместить на уровне два телепорта, каждый из которых будет точкой назначения для другого, использование имён объектов будет идеальным решением:

 
wo[{3,4}]  = {"it_vortex", name="vortex1", destination="vortex2"}
wo[{15,9}] = {"it_vortex", name="vortex2", destination="vortex1"}

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

Со временем объекты могут изменяться. Двери открываются, шахматы меняют цвет, блокирующий камень может сжаться до блокирующего предмета. Это означает, что меняется и тип объекта. Но во многих случаях это также означает, что временные ссылки на объект также станут недействительными. Для удобства авторов сущность объекта будет перенесена на новый объект, даже если ссылка станет недействительной. И подобно пользовательским атрибутам, имя — это часть сущности объекта. Таким образом, если присвоить имя камню st_blocker, а затем он сожмётся в it_blocker, то этот предмет можно запросить в хранилище именованных объектов, использовав имя объекта.

Когда объект полностью уничтожается, например, дверь разрушается с помощью it_seed, он больше не может быть подконтролен активным объектам наподобие переключателей. Всё ещё существующая ссылка на уже не существующий объект не вызовет проблем для пересылки Сообщения. А как насчёт ссылок на ближайший объект? Во избежание проблем, вызванных уничтожением объектов, стандартная ссылка на ближайший объект, предваряемая приставкой ‘@’, окончательно формируется на этапе инициализации уровня (см. раздел Инициализация уровня). Это означает, что она заменяется уникальным именем ближайшего объекта, который существовал после создания всех объектов и до того момента, когда пользователь получает возможность действовать и может разрушить объект.

Но иногда предпочтительнее работать с подконтрольными объектами динамически. При этом объекты обрабатываются во время доступа к ним. Если добавить к имени приставку ‘@@’, то ссылка при инициализации будет сформирована не до конца, а останется динамической.

 
ti["c"] = {"it_coin_s", "magic#"}
ti["v"] = {"it_vortex", destination="@@magic#*"}

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

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4.2.4 Атрибуты объектов

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

Атрибут — это имя, строка с приписанным ей значением. Например, ‘obj["inverse"]=true’ задаёт булево значение отдельному атрибуту объекта, а ‘{"it_magnet", range=6.5}’ описывает магнит с первоначально заданным атрибутом с плавающей запятой.

Набор этих значений многообразен. Могут быть присвоены большинство типов Lua и ряд типов, характерных только для Enigma :

Понятие "булево значение" мы употребляем в том смысле, в котором оно употребляется в Lua 5, то есть оно может иметь значения ‘true’ и ‘false’.

Многие значения перечисляемого типа, такие как направления или цвета, выражаются целыми числами.

Особый интерес представляет значение ‘nil’. Лишь немногие атрибуты непосредственно используют значение ‘nil’, например "color" (цвет) для некоторых объектов. Задание атрибуту значения ‘nil’ изменит его значение на значение по умолчанию. Например, если установить для атрибута "orientation" (направление) объекта st_boulder значение ‘nil’, оно будет изменено на значение по умолчанию, каковым является ‘NORTH’ (север), значение направления перечисляемого типа. Последующее чтение атрибута вернёт это значение. Только атрибуты, которые допускают нулевое значение, выдадут ‘nil’ при доступе. Отсюда напрямую следует, что значения этих атрибутов по умолчанию всегда равны ‘nil’.

Разработчики Lua решили прекратить использование ‘nil’ в качестве значений в списках Lua. Так как в качестве определений объектов мы часто пользуемся анонимными списками, таким атрибутам нельзя будет присвоить значение ‘nil’. Такие атрибуты придётся устанавливать явно. Может пригодиться и добавленное нами значение ‘DEFAULT’, которое можно использовать для установки атрибутов где угодно, даже в списках Lua.

 
mySwitch["color"] = nil
mySwitch["color"] = DEFAULT
wo[{3,6}] = {"ac_marble_black", player=DEFAULT}

Заметим, что ‘DEFAULT’ — это не ‘nil’. В контексте Lua это разные значения. Просто оба они вызывают сброс атрибутов к значениям по умолчанию. Если запросить атрибут с нулевым значением, то всегда будет получено значение Lua ‘nil’. Движок никогда не вернёт ‘DEFAULT’.

Группа — это упорядоченный набор ссылок на объект (см. раздел Ссылка на объект). Так как к моменту их использования все содержащиеся в ней объекты уже должны существовать, то это значение редко используется для атрибутов при объявлении объектов. Но она очень полезна для постобработки объектов и использования в функциях обратного вызова (см. раздел Функция обратного вызова).

Самый сложный тип значений атрибута — это метки. Их назначение — определение одного или нескольких объектов. Так как в Enigma предусмотрено несколько способов это сделать, в этом типе значений объединены и перемешаны все возможности. Метка может быть строкой с именем объекта, ссылкой на объект, а также группой либо списком, в которых перечислены любые их этих типов в любом количестве и порядке. Так, справа в приведённой ниже колонке находятся допустимые метки атрибута ‘target’:

 
obj1["target"] = "mydoor"
obj2["target"] = myobject
obj3["target"] = grp(ojb1, obj2, obj3)
obj4["target"] = {"mydoor", myobject}
obj5["target"] = {grp(ojb1, obj2, obj3), "mydoor", myobject, "anotherdoor"}

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

В главе Общие атрибуты и сообщения и последующих главах приведены подробные описания существующих атрибутов объектов.

Помимо этих предварительно заданных атрибутов, автор уровня может хранить свою собственную информацию об объектах для дальнейшего использования. Любое имя, начинающееся со знака подчеркивания ‘_’, может быть использовано для целей, специфичных для данного уровня. Данная приставка была выбрана в силу того, что получающиеся в результате её добавления имена все ещё являются действительными именами Lua. Типичная сфера их использования — это переключатели и триггеры с функциями обратного вызова. Эти функции ожидают в качестве аргумента отправителя (переключатель или триггер). Если планируется ассоциировать одни и те же функции с несколькими отправителями, то можно хранить необходимую контекстную информацию в отправителе.

Внутренний движок также использует атрибуты объектов. Имена таких недоступных атрибутов начинаются со знака ‘$’. Они могут указываться в документации для сведения разработчиков, использующих язык C++. Авторам уровней следует игнорировать эти атрибуты.

В некоторых случаях можно наблюдать различное поведение при установке атрибутов во время определения объекта и установке того же атрибута, когда объект уже находится на решётке. Например, дверь ‘{"st_door_h", state = OPEN}’ открыта с самого начала, в то время как ‘mydoor["state"] = OPEN’ по отношению к закрытой двери открывает эту дверь. Открытие двери в таком случае занимает некоторое время. Подробности смотрите в разделе Жизненный цикл уровня.

После просмотра кода на C++ могут возникнуть вопросы по поводу задействования атрибутов. Не все атрибуты находятся непосредственно на карте. Некоторые из них хранятся в переменных экземпляров объектов, другие вообще не существуют. Атрибуты объектов — это абстрактное понятие, которое объединяет несколько внутренних характеристик в рамках простого общего API кода уровня. В движке C++ малозаметные причины, такие как оптимизация работы программы, требуют значительно более сложного манипулирования.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4.3 Способы взаимодействия

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4.3.1 Сообщения

Открытую дверь можно сгенерировать, установив её атрибуты. Но как же сделать так, чтобы переключатель открывал дверь, когда до него дотронешься шариком? Он просто посылает двери сообщение ‘open’. Другие переключатели могут посылать сообщение ‘on’ ("включиться") лазеру, или ‘ignite’ ("загореться") объекту it_dynamite. При взрыве динамит в свою очередь пошлёт сообщение ‘ignite’ соседним участкам решётки.

Сообщение — это простая универсальная функция или, с точки зрения объекта-получателя и автора уровня, "метод". Он может принимать два аргумента — имя сообщения, строка и необязательное значение, Например:

 
mydoor:message("open")
myboulder:message("orientate", NORTH)

mydoor:open()
myboulder:orientate(NORTH)

Последние два примера — это стандартное сокращение первых двух.

Сообщения могут возвращать значения. Но большинство сообщений возвращают просто ‘nil’.

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

Не посылайте сообщений при инициализации уровня. Переключатель можно запрограммировать на отправление двери сообщения ‘open’ при помощи цели-действия (см. раздел Цель-действие). С помощью функции обратного вызова Lua (см. раздел Функция обратного вызова) можно послать сообщение любому объекту во время его функционирования.

В главе Общие сообщения и последующих главах перечислены и описаны все сообщения.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4.3.2 Цель-действие

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

Задать "цель-действие" можно с помощью атрибутов ‘target’ и ‘action’, применённых к первому объекту. Например, чтобы переключатель открыл дверь, которой присвоено имя ‘mydoor’, можно написать:

 
{st_switch, target="mydoor", action="open"}

Объекты, такие как переключатели, можно включать и выключать. Каждый раз они будут совершать какое-либо действие. Если требуется, чтобы дверь открывалась и закрывалась по команде переключателя, понадобится не ‘open’, а другое действие. Универсальным сообщением, чередующим состояния целевых объектов, является ‘toggle’.

 
{st_switch, target="mydoor", action="toggle"}
{st_switch, target="mydoor"}

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

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

Как вы помните, сообщения могут принимать значения. Сообщения о действиях не исключение. Каждый объект отправляет свое действие с некоторым значением, обычно булевым. Переключатель посылает значение ‘true’, если его включить, и ‘false’, если его выключить. Подходящим сообщением для двери будет универсальное сообщение ‘signal’:

 
{st_switch, target="mydoor", action="signal"}

Теперь дверь будет открываться при включении переключателя и закрываться при его выключении.

Сообщение signal принимает целочисленное значение ‘0’ или ‘1’. Действительно, значение действия не совпадает. Но, как и во многих других случаях, сообщения и значения сконструированы таким образом, что они автоматически преобразуются к необходимому типу. Эта совместимость является основой для лёгкого объединения объектов.

Во многих случаях перед авторами встаёт необходимость активировать одним объектом несколько других объектов. Как ‘target’, так и ‘action’ могут принимать несколько значений. ‘target’ относится к значению типа "метка" (см. раздел Атрибуты объектов), в то время как ‘action’ может быть строкой или списком строк.

 
{st_switch, target={grp(ojb1, obj2, obj3), "mydoor", myobject, "anotherdoor"},
            action={"toggle",              "open",   "turn",   "close"}}

Все объекты, описанные при помощи метки, получают сообщение в списке действий. Если перечислено недостаточно сообщений, то будет послано действие по умолчанию ‘toggle’.

Обычно действия совершаются сразу же. Это очень важно, поскольку порядок действий часто играет большую роль. К примеру, ящик st_box сдвигается с одного триггера it_trigger на соседний или просто шарик ac_marble перемещается с первого триггера на соседний. В обоих случаях важно, чтобы первый триггер был отпущен до того, как будет нажат второй. Если эта последовательность изменяется так, что оба триггера одновременно могут быть нажаты одним объектом, то это может стать огромной дырой в балансе уровня. Поэтому все действия производятся в логичной и стабильно повторяющейся последовательности без намёка на случайность.

Действия представляют собой простые, а иногда и очень сложные изменения в мире. Но в любом случае даже не думайтеуничтожать’ (‘kill’) объект-отправитель. Уничтожение отправителя может обрушить приложение! Следите за тем, чтобы даже объединённые в цепочку действия не могли уничтожить своего объекта-отправителя. Поэтому триггер (it_trigger), который переключает выключатель st_switch, который в свою очередь уничтожает первый триггер, так же опасен, как и триггер уничтожающий сам себя. Мы не советуем уничтожать какой-либо объект, используя его собственное действие, поскольку его уничтожение не будет анимироваться, а кроме того, будет нарушен принцип WYSIWYG ("Что Ты Видишь, То Ты И Получишь" — прим. перев.). Но если это просто необходимо для соблюдения игровой логики, такое действие можно осуществить в безопасном, отсроченном режиме. Просто добавьте к уничтожаемому объекту атрибут safeaction со значением ‘true’. С этого момента действия будут производиться не мгновенно, а с минимальной задержкой, таким образом, чтобы никогда не обрушить приложение. Но помните, что даже минимальная задержка, не выходящая за пределы соответствующего отрезка времени, может нарушить последовательность действий, приведя к неожиданным результатам.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4.3.3 Функция обратного вызова

Функции обратного вызова — самое мощное расширение парадигмы Цель-действие, которое только можно себе представить. Вместо целевого объекта в качестве получателя сообщения о действии можно использовать встроенную в Lua функцию, которая вызывается всякий раз, когда запрашивается действие.

 
{"st_switch", target="my_magic", action="callback"}
{"st_switch", target="my_magic"}

target’ — это строка, состоящая из имени функции. Для уточнения можно присвоить параметру ‘action’ строку ‘"callback"’, но, как видно из второго примера, это необязательно. Движок определяет, что типом цели является функция Lua, а значит действие должно быть возвращаемым. Однако вы должны следить, чтобы имена всех объектов и функций обратного вызова были уникальны.

Давайте посмотрим на синтаксис типичной функции обратного вызова:

 
function my_magic(value, sender)
    if value == true then
        wo[sender + {1,0}] = {"it_coin_s"}
    end
end

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

В приведённом примере мы проверяем, был ли st_switch только что переведён в состояние "включен" (ON). Если так оно и есть, берём за точку отсчёта переключатель, который представляет собой отправителя, и размещаем к востоку от него монету it_coin — небольшой банкомат для снятия денег со счёта.

В разделе Примеры сложных уровней на языке Lua будут приведены примеры по-настоящему мощных функций обратного вызова с комментариями к каждой строке.

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4.3.4 Состояние объекта

Ключевым понятием в объединении объектов, таких как переключатели и двери, являются простейшие механизмы состояния этих объектов. Большинство объектов описываются при помощи простых механизмов состояния, включающих только два состояния, такие как ‘ON’,‘OFF’ или ‘OPEN’, ‘CLOSED’. Эти объекты можно связать несколькими общими сообщениями. Кроме того, эти простейшие механизмы состояния подстраиваются под игроков, которые хотят не читать руководства, а исследовать объекты в самой игре.

Хотя состояниям обычно присваиваются имена, состоящие из прописных букв (см. примеры выше), состояния — это целые числа, начиная с ‘0’, который обычно соответствует состоянию по умолчанию. Но некоторые объекты в силу исторических причин используют другие привязки. Так, для состояний, связанных с направлением, состоянием по умолчанию обычно является ‘3’, соответствующее направлению ‘NORTH’ ("север"), а прочие состояния нумеруются по часовой стрелке вплоть до ‘0’, соответствующего направлению ‘WEST’ ("запад").

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

Возьмём, к примеру, st_fourswitch, у которого есть четыре состояния, и два объекта st_laser, каждый из которых может быть как включен, так и выключен. Каждый из лазеров светится в трёх из четырёх состояний четырёхпозиционного переключателя. Но один из них выключается, когда переключатель находится в положении ‘EAST’ ("восток"), а другой — в положении ‘WEST’ ("запад"). Это можно сделать при помощи следующих зависимых от состояния целей и действий:

 
{st_fourswitch, target_3="laser#2", action_3="on",
                target_2="laser#1", action_2="off",
                target_1="laser#1", action_1="on",
                target_0="laser#2", action_0="off"}

Добавив число в качестве суффикса к ‘target_’ и ‘action_’, можно получить специальные атрибуты цели и действия, которые будут обладать приоритетом перед общими атрибутами ‘target’ и ‘action’, если значение состояния равно указанному в суффиксе номеру. Альтернативным объявлением будет следующее:

 
{st_fourswitch, target={"laser#1", "laser#2"},
              action_3={"nop",     "on"},
              action_2={"off",     "nop"},
              action_1={"on",      "nop"},
              action_0={"nop",     "off"}}

Здесь мы обращаемся к обоим лазерам во всех состояниях. Но один из них получает сообщение nop, которое означает "операция отсутствует". Фактически данное сообщение никогда не посылается. Это просто формальное сообщение, которое в приведённом выше случае нам необходимо использовать из-за требований синтаксиса.

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

 
{it_trigger, name="on_trigger",  target="laser#1", action_1="on", action_0="nop"}
{it_trigger, name="off_trigger", target="laser#1", action_1="off", action_0="nop"}

Блокировка ‘action_0’ очень важна, и её нельзя пропускать, так как в этом случае будет осуществлено действие по умолчанию. Это будет сообщение ‘toggle’, которое включит лазер.

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

 
{it_trigger, name="on_trigger",  target="laser#1", action_1="on", nopaction=true}
{it_trigger, name="off_trigger", target="laser#1", action_1="off", nopaction=true}

Когда объект удаляется с триггера, будет выполнено действие, соответствующее состоянию ‘0’. Так как ни ‘action_0’, ни ‘action’ не определены, будет выполнено действие по умолчанию, в данном случае ‘nop’.

Заглянув в код C++, можно заметить, что у многих объектов значительно более сложные механизмы состояния, чем могут предполагать авторы уровней и игроки. Это происходит из-за использования анимации, таймеров и т. д. На набор внутренних состояний объектов C++ накладывается куда более простой набор внешних состояний. Это главная причина того, почему некоторые характеристики, предлагаемые авторами уровней, не могут быть осуществлены в Lua API.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4.4 Жизненный цикл уровня

Загрузка уровня по принципу снимка, инициализация, обратные вызовы во время выполнения, условия завершения — загадка оксидов и медитации


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4.4.1 Предварительная загрузка библиотек


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4.4.2 Принцип снимка

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

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

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

 
{"st_switch", target="mydoor", state=ON}
{"st_door", name="mydoor", state=OPEN}

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

 
{"st_laser", state=ON}
{"st_laserswitch", target="mydoor"}

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

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

Некоторые объекты всё же осуществляют трансформации внутреннего состояния, которые невозможно задать атрибутами. Но некоторые из этих состояний могут представлять интерес при описании снимка уровня. Везде, где возможно, существуют особые подтипы объектов с суффиксом ‘_new’. Эти объекты могут быть использованы при первоначальном описании уровня, чтобы разместить объекты с особыми начальными состояниями. Такой подтип существует, например, для it_blocker. Обратите внимание, что такие объекты никогда не возвратят свой первоначальный подтип при запросе типа, поскольку они начинают своё существование как стандартные объекты.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4.4.3 Инициализация уровня

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

Перед выполнением первой строки кода мир существует просто как абстрактное вместилище, а не как массив-решётка, способный принимать объекты. Поэтому первые строки кода должны настроить все Глобальные атрибуты, необходимые значения которых отличаются от значений по умолчанию. Не смотря на то, что многие атрибуты впоследствии можно настроить и изменить даже во время выполнения, существуют такие атрибуты как ProvideExtralifes, который имеет смысл устанавливать, только перед созданием мира, или MaxOxydColor, который следует устанавливать перед его первым использованием. Мы рекомендуем выносить настройку всех глобальных атрибутов в самое начало уровня.

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

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

Главное выражение в любом уровне — это Создание мира. Оно задаёт размеры мира и располагает предметы в пределах решётки мира, согласно заданным описаниям секций. Хотя позже можно свободно добавить или изменить любой из объектов, размеры мира фиксированы и неизменны.

Поэтому следующие строки кода должны добавлять (add) другие объекты, рисовать дополнительные карты объектов или дополнять некоторые объекты. Общеупотребительным выражением подобного дополнения является метод shuffleOxyd. Для того, чтобы перекрасить и перемешать оксиды (st_oxyd), ему необходимо знать о всех оксидах на уровне. Ещё одним примером дополнения можно считать генерирование требуемого лабиринта, которое придаёт карте уровня форму лабиринта (см. раздел res.maze).

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

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

Однако существует одно особое исключение. Функция обратного вызова postinit() вызывается после того, как выполнился код инициализации уровня и завершились все последующие инициализации внутри движка. Если эта функция присутствует в уровне, то она выполняется непосредственно перед обработкой первого события перемещения мыши. А значит, при использовании этой функции можно быть уверенным, что все объекты находятся в своём окончательном состоянии. Перед использованием функции обратного вызова postinit() её следует поместить после всего кода инициализации уровня, но перед остальными функциями обратного вызова, которые будут исполняться при последующих событиях.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4.4.4 Преобразование объекта

Во время игры некоторые объекты Enigma преобразуются в другие объекты-потомки, такие как st_blocker/it_blocker, st_brake/it_brake, it_rubberband/ot_rubberband, it_hammer/it_sword и т. д.

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4.4.5 Именованные позиции

Многие камни можно перемещать и, даже если пользователь не может оттолкнуть их, большинство может поменяться местами с рядом стоящим обменным камнем. Актёры могут поднимать предметы, которые могут быть уничтожены во всепоглощающем пламени. Поэтому в большинстве случаев, предпочтительно помечать на полу якоря или формы. В каждой ячейке решётки существует объект покрытия, который намного стабильнее всех остальных объектов. Тем не менее, пользователь может сбросить в воду (fl_water) или бездну (fl_abyss) ящик (st_box), камень-головоломку (st_puzzle) или другой строительный камень. Более того, пользователь может оставить зажжённую бомбу (it_bomb), которая разрушит покрытие и оставит на его месте бездну (fl_abyss). В любом из этих случаев можно лишиться именованного якоря или важной части именованной области решётки, доступной в виде группы объектов.

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

 
ti["~"] = {"fl_water", "water#"}
...
function sweet()
    wo[po["water#*"]] = {"it_cherry"}
end

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4.4.6 Обратные вызовы и таймеры

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

Цели-действия убивают предупреждающий ot_timer.

Глобальные функции и функция res.*.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4.4.7 Перезапуск уровня


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

4.4.8 Условия завершения

Теперь нам необходимо ответить на два важных вопроса: когда и при каких условиях уровень будет являться успешно завершённым? Как можно объявить медитативный уровень в противоположность стандартному уровню, основанному на открытии пар оксидов.

Флаг "медитации" по сути отсутствует как в информации о метаданных , так и среди глобальных атрибутов. Это означает, что формальное разграничение между двумя видами уровней отсутствует. Но есть два вида условий завершения. Оба проверяются постоянно и первое выполненное условие приводит к завершению уровня. Таким образом, Enigma даёт возможность писать действительно гибридные уровни, в которых присутствуют как объекты st_oxyd, так и it_meditation, позволяя пользователю решить уровень двумя абсолютно различными способами.

Главный способ закончить игру — это выполнить условие открытия пар оксидов:

игра считается завершённой после того, как пользователь попарно откроет все стандартные окрашенные пары камней st_oxyd.

Соответственно, подразумевается, что на уровне имеется по крайней мере одна пара стандартных окрашенных оксидов и что для всех оксидов одного цвета имеется чётное количество экземпляров. В то время как в стандартном уровне всегда будет хотя бы одна пара оксидов, за количеством экземпляров бывает трудно уследить. Поэтому движок постоянно проверяет во время выполнения, является ли количество экземпляров оксидов каждого цвета чётным. Нарушение данного условия приведёт к ошибке. Если вы добавляете или удаляете оксиды на уровне, эту проверку необходимо отключить, установив для атрибута AllowSingleOxyds значение true. Теперь ответственность за возможность решения уровня несёте вы и добавлять или удалять оксиды вы должны только попарно.

Вторым способом завершения игры является выполнение условия медитации:

все объекты ac_pearl должны находиться в течение не менее чем одной секунды в неровностях it_meditation, все it_meditation, помеченные как essential, должны быть заняты, и количество ac_pearl должно быть равно количеству занятых it_meditation.

Опять же, это подразумевает, что на уровне существует по крайней мере один объект ac_pearl и что в одном и том же объекте it_meditation не могут одновременно находиться две жемчужины. На уровне должно находиться не меньше объектов it_meditation, чем на нём есть ac_pearl; объектов it_meditation может быть и больше, если они не отмечены как essential. Избыток объектов it_meditation может легко получиться в результате взрывов it_dynamite.

На уровне, который необходимо решить, выполнив условие медитации, могут находиться объекты st_oxyd; установив для атрибута AllowSingleOxyds значение true, можно добавить на уровень нечётное количество оксидов одного цвета. С другой стороны, на уровень, который должен быть решён путём выполнения условия открытых пар оксидов, можно добавить ac_pearl. Но необходимо внимательно следить, чтобы пользователь не мог поместить жемчужины в имеющиеся объекты it_meditation и чтобы он не мог создать новые объекты it_meditation при помощи it_dynamite. Непредусмотренных решений можно избежать, отметив в качестве essential большее количество объектов it_meditation, чем на уровне имеется ac_pearl.

Уровень, который изначально позволяет пользователю выполнить оба условия завершения, называется гибридным. Конечно же, предусмотреть равноценные решения для обоих подходов — нелёгкая задача.

Вне зависимости от типа условий завершения все актёры, отмеченные как essential, должны оставаться в живых в момент выполнения этих условий. Автор также может позволить пользователю пожертвовать актёром, чтобы выполнить условия завершения, установив для атрибута SurviveFinish значение false (подобная опция в основном необходима для поддержания совместимости с уровнями, написанными для предыдущих версий, но её также можно использовать и для новых уровней при условии их тщательного планирования). В этом случае шарик может разбиться, но условие всё равно будет выполнено. Конечно же, актёры, указанные как essential, не могут разбиться заранее, поскольку после того, как такой актёр будет уничтожен, в случае, если его нельзя воскресить, уровень будет перезапущен.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5. Lua API

Теперь, после изучения основных принципов мира уровней Enigma, осталось лишь разобраться языком программирования, чтобы приступить к написанию своего первого уровня. Уровни Enigma написаны на языке Lua 5.1.4. Этот мощный язык позволяет вам писать самые сложные, динамические уровни и в то же время он достаточно понятен для того, чтобы написать простые стандартные уровни. Действительно, зачем в самом начале копаться в тонкостях языка?

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

Чтобы вам было удобно, код Lua мы будем выделять цветом. Предопределённые (или встроенные) переменные и функции Lua мы будем выделять зелёным цветом. Встроенные строковые константы Enigma, такие как типы объектов или названия атрибутов и сообщений, выделяются синим. Названия переменных и их значения, относящиеся только к какому-то конкретному уровню, окрашены в пурпурный цвет.

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.1 Примеры простых уровней на языке Lua

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.1.1 Простой пример

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

 
 1    <?xml version="1.0" encoding="UTF-8" standalone="no" ?>
 2    <el:level xsi:schemaLocation="http://enigma-game.org/schema/level/1 level.xsd" xmlns:el="http://enigma-game.org/schema/level/1">
 3     <el:protected>
 4       <el:info el:type="level">
 5         <el:identity el:title="Basic Level" el:subtitle="" el:id="20080721ral513"/>
 6         <el:version el:score="1" el:release="1" el:revision="$Revision: 1.19 $" el:status="experimental"/>
 7         <el:author el:name="Ronald Lamprecht" el:email="ral@users.berlios.de"/>
 8         <el:copyright>Copyright © 2008 Ronald Lamprecht</el:copyright>
 9         <el:license el:type="GPL v2.0 or above" el:open="true"/>
10         <el:compatibility el:enigma="1.10"/>
11         <el:modes el:easy="true" el:single="true" el:network="false"/>
12         <el:score el:easy="-" el:difficult="-"/>
13       </el:info>
14       <el:luamain><![CDATA[
15
16    wo["ConserveLevel"] = true
17
18    ti[" "] = {"fl_samba"}
19    ti["."] = {"fl_abyss"}
20    ti["~"] = {"fl_water"}
21    ti["#"] = {"st_granite"}
22    ti["X"] = {"st_oxyd"}
23
24    ti["L"] = {"st_laser", orientation=EAST, state=ON}
25    ti["M"] = {"st_lightpassenger", interval=0.04}
26
27    ti["P"] = {"st_polarswitch", name="polar"}
28    ti["T"] = {"it_trigger", target="polar"}
29
30    ti["^"] = {"st_boulder", "boulder", orientation=NORTH}
31    ti["F"] = {"st_fourswitch", target="boulder", action="orientate"}
32
33    ti["D"] = {"st_door_d", "door", faces="ew"}
34    ti["B"] = {"it_blocker", "wall#"}
35    ti["S"] = {"st_switch", target={"door", "wall#*"}}
36
37    ti["v"] = {"it_vortex", "left", destination="right"}
38    ti["V"] = {"it_vortex", "right", destination="left"}
39
40    ti["O"] = {"st_turnstile", flavor="red"}
41    ti["E"] = {"st_turnstilearm", orientation=EAST}
42    ti["N"] = ti["."] .. {"st_turnstilearm_n"}
43
44    ti["+"] = {"fl_samba", checkerboard=0} .. ti({"fl_wood", checkerboard=1})
45
46    ti["1"] = {"#ac_marble"}
47
48    if wo["IsDifficult"] then
49        ti["="] = ti["~"]
50    else
51        ti["="] = ti["~"] .. {"it_strip_ew"}
52    end
53
54    w, h = wo(ti, " ", {
55        "####################",
56        "#      ....++++++~ #",
57        "L   PM ..N.++~~~~OE#",
58        "#######  T~++++++. #",
59        "#     ^   ~++++++# #",
60        "#         =++++++X X",
61        "#         ~++++++# #",
62        "#~~~~~~~~~~~~~+++X X",
63        "#    ~   B   ~+++###",
64        "F    ~   B   ~+++++#",
65        "# 1  ~   B   #+++++#",
66        "S   v~V  B   D+++++#",
67        "####################"
68    })
69
70    wo:shuffleOxyd()
71
72     ]]></el:luamain>
73        <el:i18n>
74          <el:string el:key="title">
75            <el:english el:translate="false"/>
76          </el:string>
77        </el:i18n>
78      </el:protected>
79    </el:level>

Получившийся уровень выглядит в игре следующим образом:

images/ralD006_1

Теперь давайте рассмотрим код строка за строкой.

В строках с 1 по 14 содержатся метаданные уровня в формате XML, описанные в главе Основы работы с уровнями. Единственная строка, на которую стоит обратить внимание, это:

 
10         <el:compatibility el:enigma="1.10"/>

Чтобы использовать API 2, так как это описано в данном руководстве, следует объявить, что уровень совместим с Enigma версии 1.10 или выше. Значение меньшее 1.10 означает совместимость с предыдущим выпуском Enigma, который использует старый API 1 и который не следует смешивать с новым API 2.

Код Lua начинается со строки 15:

 
16    wo["ConserveLevel"] = true

Как и большинство уровней, этот уровень начинается установкой глобальных атрибутов (см. раздел Глобальные атрибуты). Представление нашего мира — это ‘wo’. Это предварительно заданная объектная ссылка (см. раздел Мир как объект). С точки зрения Lua, это ‘пользовательские данные’, но большая часть синтаксиса их использования, идентична синтаксису списков Lua. Так, например, мы можем получить доступ к атрибуту, заключив имя нужного атрибута в квадратные скобки. Чтобы задать символьное имя атрибута, мы должны заключить его в двойные кавычки ‘"’. Смысл строки в указании миру воскрешать убитого актёра, пока хватает дополнительных жизней, чтобы текущий уровень всё ещё можно было пройти (см. раздел ConserveLevel). В сущности ‘true’ — это значение по умолчанию, поэтому мы могли бы опустить эту строку. Мы оставляем ее в целях полноты изложения.

Вторая часть уровня — это определение секций способом, описанным в разделе Очертания и координаты мира. Давайте начнём с самого простого:

 
18    ti[" "] = {"fl_samba"}
19    ti["."] = {"fl_abyss"}
20    ti["~"] = {"fl_water"}
21    ti["#"] = {"st_granite"}
22    ti["X"] = {"st_oxyd"}

Мы снова используем указатель, ‘ti’, который представляет собой объектную ссылку на хранилище определений секций. Подобно миру, это ‘пользовательские данные’ Lua, и мы можем обратиться к нему, задавая нужный индекс в квадратных скобках. Эти индексы можно выбрать на свой вкус. Если они используются далее в карте мира, то их длина должна быть одинаковой. Для маленьких уровней подойдут односимвольные коды. Разрешается использовать любые понятные Lua символы ASCII. То есть, символы ‘A-Z,a-z’ в верхнем и нижнем регистрах, цифры и специальные символы, исключая обратный слэш ‘\’ и двойные кавычки ‘"’.

Определение, присваиваемое объекту, представляет собой анонимные таблицы Lua — фигурные скобки, в простейшем случае содержащие лишь желаемый Тип объекта. Так как это опять строка символов, то её следует заключать в двойные кавычки. Если не указано иное, объекты берутся в их конфигурации по умолчанию, которая описана в разделе Покрытия и следующих разделах.

 
24    ti["L"] = {"st_laser", orientation=EAST, state=ON}
25    ti["M"] = {"st_lightpassenger", interval=0.04}

Эти две строки описывают объекты с конфигурацией, отличающейся от конфигурации по умолчанию. st_laser должен послать свой луч в восточном направлении и изначально должен быть включен. st_lightpassenger должен перемещаться немного быстрее, чем обычно. В обоих случаях мы должны просто добавить дополнительные атрибуты, разделённые запятой. Названия атрибутов не заключаются в кавычки, потому что за ними следует знак равенства ‘=’.

 
27    ti["P"] = {"st_polarswitch", name="polar"}
28    ti["T"] = {"it_trigger", target="polar"}

st_polarswitch переименован, чтобы на него было удобнее ссылаться (см. раздел Именование объектов). Для it_trigger настраивается Цель-действие, целью становится наша переключаемая преграда для лазера. Атрибут действия опущен. По умолчанию им становится сообщение ‘toggle’. Таким образом, любой актёр или камень, ставший на триггере, делает прозрачной переключаемую преграду для лазера, но, покидая триггер, снова делает её непрозрачной.

 
30    ti["^"] = {"st_boulder", "boulder", orientation=NORTH}
31    ti["F"] = {"st_fourswitch", target="boulder", action="orientate"}

Другая пара объектов объединена понятием Цель-действие. st_boulder изначально пытается двигаться на север. На этот раз мы даём объекту имя, просто передавая его в качестве второго из строковых параметров, разделяемых запятыми. Мы опустили идентификатор атрибута ‘name =’. Это упрощение для наиболее часто используемого атрибута, которое требует, чтобы сразу после типа объекта вторым параметром было указано имя.

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

 
33    ti["D"] = {"st_door_d", "door", faces="ew"}
34    ti["B"] = {"it_blocker", "wall#"}
35    ti["S"] = {"st_switch", target={"door", "wall#*"}}

И ещё одна Цель-действие, немного посложнее. Мы хотим с помощью единственного переключателя (st_switch) открывать/закрывать дверь (st_door) и, в то же время, изменять состояние it_blocker. Игровой момент здесь состоит в том, что ни при включенном, ни при выключенном переключателе шарик не может преодолеть оба препятствия. Чтобы преодолеть их, игроку нужно провести болдер сквозь блокирующую стену.

Настройка двери проста. Для того, чтобы в будущем к ней обращаться, нам нужно просто дать ей имя. Мы хотим использовать несколько блокирующих объектов и дать имя каждому, чтобы ссылаться на них. Мы делаем это, добавляя к его имени знак решётки ‘#’ так, как это описано в разделе Именование объектов. Каждому блокирующему объекту присваивается уникальное имя. Каждый из этих объектов должен быть перечислен в списке целей переключателя. Это сделано с использованием встроенного анонимного списка, формируемого фигурными скобками, в которых через запятую указываются значения. Первое — имя двери, второе — шаблон строки, описывающей все наши блокирующие объекты. Звёздочка представляет собой любой суффикс, который в процессе автоматического именования наших блокирующих объектов может быть добавлен после решётки.

 
37    ti["v"] = {"it_vortex", "left", destination="right"}
38    ti["V"] = {"it_vortex", "right", destination="left"}

Мы хотим использовать две воронки (it_vortex), которые связаны друг с другом, давая таким образом шарику возможность переноситься в обоих направлениях. Каждой воронке мы присваиваем уникальное имя и добавляем атрибут ‘destination’, который принимает значение имени другой воронки.

Заметим, что мы можем без проблем обратиться к правой воронке в строке 37, в то время как описана она будет только в строке 38. Мы пока только описываем секции, но ещё не создаём вообще никаких объектов.

 
40    ti["O"] = {"st_turnstile", flavor="red"}
41    ti["E"] = {"st_turnstilearm", orientation=EAST}
42    ti["N"] = ti["."] .. {"st_turnstilearm_n"}

Другая группа объектов — это блок из st_turnstile с одной отсоединённой рукояткой. Первые два определения предельно понятны. Но в строке 42 мы предваряем определение рукоятки ссылкой на другой объект. Это секция бездны, определённая в строке 19. Объединяя двумя точками (..) описание секции и объекта, мы можем определить новый объект, состоящий из обоих объектов. В данном случае мы определяем рукоятку турникета на покрытии бездны.

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

 
44    ti["+"] = {"fl_samba", checkerboard=0} .. ti({"fl_wood", checkerboard=1})

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

Пожалуйста, обратите внимание, что мы преобразовали одно из определений объекта покрытия в определение секции, вызвав функцию ‘ti()’. Это необходимо, потому что Lua не знает, как объединить два анонимных списка. Один из объединяемых параметров должен быть секцией.

 
46    ti["1"] = {"#ac_marble"}

Наконец, нам понадобится наш шарик. В отличие от остальных объектов он может располагаться в любом месте решётки. Самое распространённое место — это центр решётки. Для этого нам просто нужно предварить тип актёра знаком решётки ‘#’.

 
48    if wo["IsDifficult"] then
49        ti["="] = ti["~"]
50    else
51        ti["="] = ti["~"] .. {"it_strip_ew"}
52    end

Мы рекомендуем нашим создателям уровней предоставить для уровней упрощённый режим. В данном примере приводятся определения разных секций для разных режимов. Как и в строке 16, мы обращаемся к атрибуту мира. Но в этот раз это чтение атрибута IsDifficult. В упрощённом режиме мы хотим, чтобы на поверхности воды была полоска суши (it_strip), которая позволит шарику пересечь её и нажать на триггер. В сложном режиме прохода быть не должно. Таким образом, специальная секция идентична секции воды, определённой в строке 20.

 
54    w, h = wo(ti, " ", {
55        "####################",
56        "#      ....++++++~ #",
57        "L   PM ..N.++~~~~OE#",
58        "#######  T~++++++. #",
59        "#     ^   ~++++++# #",
60        "#         =++++++X X",
61        "#         ~++++++# #",
62        "#~~~~~~~~~~~~~+++X X",
63        "#    ~   B   ~+++###",
64        "F    ~   B   ~+++++#",
65        "# 1  ~   B   #+++++#",
66        "S   v~V  B   D+++++#",
67        "####################"
68    })

После того, как все секции будут заданы, мы можем создать наш мир, просто нанеся его на карту, используя наши коды секций. Первый аргумент — это наше представление ‘ti’, которое определяет, как следует преобразовывать коды. Второй аргумент — код нашего покрытия по умолчанию. Третий аргумент — карта в виде списка строк, по одной строке списка в каждой строке карты.

Создание мира возвращает ширину и высоту нашего мира, которые рассчитаны по размеру карты.

 
70    wo:shuffleOxyd()

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.1.2 Colored Turnstiles

Так как этот уровень входит в состав пакетов уровней Enigma, мы советуем сначала поиграть на нём, чтобы познакомиться с используемыми объектами и их поведением.

Теперь давайте взглянем на важную часть Lua исходного кода уровня, чтобы понять, как подобная идея может быть реализована с использованием нового API:

 
ti[" "] = {"fl_sahara"}
ti["#"] = {"st_purplegray"}
ti["@"] = {"#ac_marble_black", "marble_black"}

ti["N"] = {"st_turnstilearm_n"}
ti["S"] = {"st_turnstilearm_s"}
ti["E"] = {"st_turnstilearm_e"}
ti["W"] = {"st_turnstilearm_w"}
ti["R"] = {"st_turnstile", action = {"open", "close"}, target = {"red#*", "green#*"}}
ti["G"] = {"st_turnstile", action = {"close", "open"}, target = {"red#*", "green#*"},
                           flavor = "green"}
ti["r"] = {"it_blocker", "red#"} .. ti({"fl_red"})
ti["g"] = {"it_blocker", "green#"} .. ti({"fl_lawn"})

ti["O"] = {"st_oxyd", flavor = "d", oxydcolor = OXYD_GREEN}
ti["o"] = {"st_oxyd", flavor = "d", oxydcolor = OXYD_RED}

w, h = wo(ti, " ", {
 -- 01234567890123456789
   "#O#####O############",
   "#   r N g N rO##O#O#",
   "#WRE#WGE# R ####g#r#",
   "#   r N r S  r N   #",
   "#g#g#WG #g##r# REr##",
   "# # N S r    g S  gO",
   "#@g RE#g#gWGE###g###",
   "# # S   g    r N  ro",
   "#r#r#WGE#r##g#WGEg##",
   "# N r S g N  r     #",
   "#WGE# RE# RE####r#g#",
   "#   g S r S go##o#o#",
   "#o#####o############"
})

wo:shuffleOxyd()

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

 
ti["r"] = {"it_blocker", "red#"} .. ti({"fl-red"})
ti["g"] = {"it_blocker", "green#"} .. ti({"fl-leaves"})

Все блокирующие предметы на красных покрытиях получают имена автоматически, объединением приставки ‘red#’ и уникального случайного номера, добавляемого движком так, как описано в разделе Именование объектов. Это даёт нам возможность позже обращаться ко всем этим блокирующим предметам.

 
ti["R"] = {"st_turnstile", action = {"open", "close"}, target = {"red#*", "green#*"}}
ti["G"] = {"st_turnstile", action = {"close", "open"}, target = {"red#*", "green#*"},
                           flavor = "green"}

Всякий раз, когда шарик проходит через турникет (st_turnstile), он (турникет) осуществляет действия над своими целями. В данном случае автор мудро использовал несколько целей и действий так, как описано в разделе Цель-действие. При каждом повороте красного турникета все объекты с именем вида ‘red#*’, то есть всем нашим блокирующим предметам на красном покрытии, будет отправлено сообщение ‘open’, тогда как все блокирующие камни на зелёном покрытии, вторая группа целей, получат второе сообщение действия ‘close’. Важно выбрать сообщения ‘open’ и ‘close’ вместо ‘toggle’, так как последовательно могут повернуться несколько красных турникетов, однако только первый поворот красного турникета должен "переключить" все блокирующие камни. Соответственно, следующее переключение должно произойти при первом повороте зелёного турникета.

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.2 Обзор API 2

После того, как мы проанализировали первый уровень, настало время рассмотреть возможности API 2. Сделаем это, перечислив различные возможности и их использование на примерах.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.2.1 Обзор типов

Для начала мы должны рассказать о специальных типах значений Enigma, отличных от стандартных типов Lua ‘nil’, ‘boolean’, ‘string’, ‘function’ и ‘table’:

Типы:
position:  см. раздел Позиция

Позиция в мире, которую можно задать координатами x и y.

positions: стандартная переменная: po; см. раздел Хранилище позиций

Единый тип хранилища всех именованных позиций.

object:  см. раздел Объект

Объект Enigma, такой как камень, предмет, покрытие и т.п. Каждому объекту также соответствует позиция.

group:  см. раздел Группа

Список объектов.

namedobjects: стандартная переменная: no;   см. раздел NamedObjects

Единый тип хранилища всех именованных объектов.

default: стандартная переменная: DEFAULT;   см. раздел Атрибуты объектов

Единый тип значений по умолчанию, который может использоваться вместо типа Lua ‘nil’ в определениях анонимных списков секций.

tile:  см. раздел Описание секций и объектов

Описание для одной секции одного или нескольких объектов (покрытие, предмет, камень, актёр).

tiles: стандартная переменная: ti;   см. раздел Хранилище секций

Единый тип хранилища всех экземпляров секций.

world: стандартная переменная: wo;   см. раздел Мир

Единый тип мира, содержащий все объекты.

position list:  см. раздел Именованные позиции

Список позиций.

Пожалуйста, обратите внимание на четыре представления: ‘po’, ‘no’, ‘ti’ и ‘wo’. Два из них уже можно было заметить в предыдущем разделе Примеры простых уровней на языке Lua. Это четыре переменные, которые должны быть настроены перед тем, как будет выполнен код уровня.

Обычно, для часто используемых переменных и функций API 2 использует двухсимволные имена, чтобы сократить код уровня и сделать его более читаемым. Авторам рекомендуется использовать либо одиночные символы, либо имена из трёх и более символов для имён локальных переменных.

В оставшейся части этого раздела мы предполагаем, что ‘obj’ — это объектная ссылка на камень, предмет или покрытие; это значит, что его тип — ‘object’. И предположим, что ‘pos’ — допустимая переменная типа ‘position’.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.2.2 Работа с позициями

Подробности можно узнать в разделе Позиция.

Создание позиций:
 
pos = po(7, 3)         — использование функции "po()" для создания объекта позиции
pos = po({7, 3})        — использование в качестве аргумента списка констант позиции
pos = obj              — каждый объект представляет собой допустимую позицию
pos = po(12.3, 3.7)    — позиция в сетке (для актёра)

Абсолютные позиции создаются функцией ‘po()’. Но самым распространённым способом должно быть представление объекта в качестве позиции. Это позволяет размещать новые объекты относительно заданных объектов.

Константы позиции:
 
{7,3}     — позиция, допустимая для всех параметров и операций (см. Предупреждение)

Анонимные списки всего с двумя числовыми значениями во многих случаях могут использоваться непосредственно как константы позиции. В случае ошибок, например, когда операторы заданы не самым лучшим образом, вроде сложения двух констант, что приводит к попытке объединения двух списков Lua, для преобразования констант используют функцию ‘po()’.

Доступ к координатам:
 
x, y = pos.x, pos.y
x, y = pos["x"], pos["y"]
x, y = pos:xy()
x, y = obj.x, obj.y
x, y = obj:xy()

Координаты позиции или объекта (x и y), как и любой атрибут объекта, можно прочитать. Вызов метода позиции или объекта под названием ‘xy()’ возвращает значения сразу обеих координат. Изменить значение позиции, используя доступ на запись, нельзя. Объекты должны быть помещены на новую позицию в мире. Для задания новой позиции можно использовать арифметические операции.

Расчёт позиции:
 
pos = obj + {2,7}
dpos = obj1 - obj2
dpos2 = 2 * dpos
dpos3 = dpos / 2

Чтобы получить вектор расстояния, позиции можно складывать и вычитать. Их можно умножать и делить на любое число.

Центрирование позиции для размещения актёров
 
pos_centered1 = pos + {0.5, 0.5}
pos_centered2 = #pos
pos_centered3 = #obj

По большей части для размещения актёров, иногда необходимо знать позицию центра ячейки решётки. Конечно, её можно получить добавлением константы позиции. Однако то же самое можно сделать и проще, применив к позиции или актёру оператор ‘#’.

Округление позиции до границ ячейки
 
grid_pos = pos:grid()
grid_pos = ((pos1 - pos2)/2):grid()

Иногда результат расчёта позиции необходимо округлить до целых координат решётки. Это делается методом ‘grid()’.

Сравнение позиций
 
pos_centered1 == pos_centered2
pos_centered1 ~= pos_centered2    — Оператор неравенства Lua

Позицию можно легко проверить на соответствие.

Существование позиций
 
pos:exists()

Возвращает ‘true’, если позиция находится в пределах мира. Все позиции за пределами мира возвращают ‘false’.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.2.3 Работа с атрибутами

Подробности можно узнать в разделе Объект.

Установка одного атрибута:
 
obj["destination"] = po(7,3)
wo["Brittleness"] = 7

Как и атрибуты мира, атрибуты объекта могут устанавливаться как список значений Lua. Они могут получать значения специальных типов Enigma, таких как позиция, объект или группа.

Установка нескольких атрибутов:
 
obj:set({target=mydoor, action="open"})

С помощью объектного метода ‘set()’ для любого объекта можно установить несколько атрибутов. Аргумент представляет собой анонимный список Lua с именами атрибутов в качестве ключевых полей и соответствующими выбранными вами значениями.

Запрос атрибутов:
 
value = obj["attr_name"]
value = wo["Brittleness"]
if wo["IsDifficult"] then ... end

Атрибуты объектов и мира можно прочесть в виде списка ключевых полей и значений Lua.

Сброс атрибутов:
 
obj["length"] = nil       — длина по умолчанию, например ‘1obj["color"]  = nil       — удаляет атрибут цвета — убирает цвет
obj["length"] = DEFAULT   — длина по умолчанию, например ‘1

Любой атрибут объекта может быть сброшен к своему значению по умолчанию, что представляет собой операцию "удаления" атрибутов, присвоением ему значения ‘nil’ из Lua или значения ‘DEFAULT’ из Enigma.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.2.4 Работа с объектами

Подробности можно узнать в разделе Объект.

Создание объектов:
 
wo[pos]  = {"st_chess", color=WHITE, name="Atrax"}   — на секции решётки
wo[#pos] = {"ac_bug"}              — актёр, помещённый в центре секции решётки
wo[pos]  = {"#ac_bug"}             — актёр, помещённый в центре секции решётки
wo[pos]  = {"ac_bug", 0.3, 0.7}    — актёр, смещённый от границ секции решётки
wo[my_floor] = {"it_magicwand"}    — размещение на заданном объекте покрытия волшебной палочки
wo[pos]  = ti["x"]                 — определение объекта на основе секции

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

Именование объектов:
 
no["Atrax"] = obj
wo[pos] = {"st_chess", name="Atrax"}
wo[pos] = {"st_chess", "Atrax", color=WHITE}

Как сказано в разделе Именование объектов, имена — это единственные ссылки на объект, которые существуют длительное время. Чтобы назвать объект, как ключевое поле, можно явно дать объекту имя присвоив его в хранилище именованных объектов ‘no’. Но, в основном, просто давайте объектам имена через атрибут объекта. При передаче атрибута с именем через второе значение в анонимном списке атрибутов, чтобы немного сократить код, можно опустить ключевое поле ‘name =’.

Автоматическое именование объектов:
 
wo[pos] = {"st_chess", name="Atrax#"}

Как описано в разделе Именование объектов, к имени можно добавить знак решётки ‘#’ и использовать полученную строку для произвольного количества подобных объектов. Это особенно полезно при создании групп.

Обращение к объектам:
 
obj = no["Atrax"]       — получение именованного объекта из хранилища
obj = it(pos)
obj = it(x,y)
obj = st(pos)
obj = wo:it(pos)
my_item = it(my_floor)  — получить предмет, который находится на заданном покрытии

Как правило, сначала объектам присваиваются имена, после чего к ним можно обратиться посредством ссылки на этот объект в хранилище ‘no’. Если известна позиция нужного объекта, то можно воспользоваться одной из функций или методов мира ‘fl’, ‘it’, ‘st’, которые принимают в качестве параметра позицию, объект, позицию которого можно использовать, или просто две координаты. Особенно полезным может быть запрос объектов одного типа, которые размещены в той же ячейке, что и другой объект (камень на поверхности и т.п.).

Уничтожение объектов:
 
wo[pos] = {"it_nil"}
obj:kill()

Объект удаляется, размещением заменяющего его объекта на том же месте и в том же слое. Если нет нужды размещать новый объект, то можно использовать объекты "пустышки" ‘fl_nil’, ‘it_nil’, ‘st_nil’. Другой способ состоит в вызове метода объекта ‘kill()’ или отправке сообщения ‘kill’. Удалять можно только объекты, которые размещены на решётке. Ни актёры, ни связанные с ними объекты, как например предметы в инвентаре игрока, не могут быть уничтожены — они просто будут игнорировать подобную попытку.

Сравнение объектов
 
obj1 == obj2
obj1 ~= obj2

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

Существование объекта
 
obj:exists()
-obj                — унарный оператор отрицания перед объектом
if -obj then ...

Ссылки на объекты могут стать недействительными после уничтожения объектов. Так как обращения к недоступным объектам просто будут игнорироваться, то в большинстве случаев это не проблема. Но если логика уровня зависит от существования объекта, можно вызвать метод ‘exists()’ или просто предварить ссылку унарным оператором отрицания ‘-’. Оба способа возвращают простое значение булевого типа, сообщающее, действительна ли ещё ссылка на объект.

Сообщения:
 
my_boulder:message("orientate", WEST)
my_boulder:orientate(EAST)
my_door:open()

Сообщения — основная возможность Enigma. Их можно отправить напрямую каждому объекту методом ‘message()’ или используя любое сообщение как сам вызов метода.

Классификация объектов:
 
obj:is("st_chess")
obj:is("st")
obj:is("st_chess_black")

Новые объекты создаются с помощью задания типа объекта (см. раздел Тип объекта). Позже заданный объект можно проверить на соответствие указанному классу или типу. Не смотря на то, что нельзя создавать объекты абстрактного типа, например ‘st’, таким образом можно проверить, является ли объект камнем. Проверка особых подтипов может даже определить текущее состояние или другие атрибуты объекта, чтобы сообщить о его текущей классификации.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.2.5 Работа с группами

Подробности можно узнать в разделе Группа.

Создание групп:
 
group = no["Atrax#*"]           — группа всех объектов с похожим названием
			        —   можно использовать маски "*","?"
group = grp(obj1, obj2, obj3)
group = grp({obj1, obj2, obj3})  — группа объектов собрана в список

При правильном использовании масок, запрос объектов из хранилища именованных объектов вернёт группу объектов. Добавление звёздочки ‘*’ к символу автоименования, решётке, вызовет все объекты, у которых в имени есть такой суффикс. Кроме того, группу можно создать и с помощью функции ‘grp’. Просто добавьте в качестве аргументов требуемые объектные ссылки, или по одной, или в виде списка.

Использование групп:
 
floor_group["friction"] = 3.2      — установить атрибут для всех покрытий в группе
door_group:message("open")
door_group:open()
stone_group:kill()
wo[floor_group] = {"it_coin_m"}   — положить немного денег на все участки покрытия

wo[pos] = {"st_switch", target=door_group, action="open"}
wo[pos] = {"st_switch", target="door#*", action="close"}

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

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

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

Операции над группами:
 
doors_lasers = doorgrp + lasergrp       — объединение двух групп
lasergrp     = doors_lasers - doorgrp   — разность двух групп
common_doors = doorgrp1 * doorgrp2      — пересечение двух групп

Группы предлагают несколько стандартных операций, знакомых по работе с наборами.

Члены группы:
 
count = #mygroup        — количество объектов в группе
obj   = mygroup[5]     — 5-й объект группы
obj   = mygroup[-1]    — последний объект группы
for i = 1, #mygroup do obj = mygroup[i] ... end
for obj in mygroup do ... end

К членам группы можно обратиться по порядковым индексам. Размер группы сообщается стандартным оператором Lua, решёткой ‘#’. Если требуется последовательно перебирать все объекты группы, то можно просто написать циклы на Lua. Можно как использовать счётчик, так и напрямую перебирать содержимое объектов.

Перемешанная группа:
 
shuffled_group = sorted_group:shuffle()
shuffled_group = no["Atrax#*"]:shuffle()

После получения сообщения "shuffle", любая группа вернёт перемешанную группу, состоящую из её элементов.

Отсортированная группа:
 
sorted_group = group:sort("linear", po(2, 1))
sorted_group = group:sort("linear")
sorted_group = group:sort("circular")
sorted_group = group:sort()

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

Отделение подгруппы:
 
sub_group = group:sub(2)   — первые два объекта
sub_group = group:sub(-2)  — последние два объекта
sub_group = group:sub(2, 4) — объекты со 2 по 4
sub_group = group:sub(2, -2) — два объекта, начиная со 2

По заданным индексам и числам формирует подгруппу.

Ближайший объект:
 
object = group:nearest(reference)

Ищет в группе объект, ближайший к указанному.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.2.6 Работа с секциями и миром

Подробности можно узнать в разделах Описание секций и объектов, Хранилище секций и Мир.

Секции:
 
ti["_"] = {"fl_sahara"}
ti["__"] = {"fl_sahara"}
ti[".."] = {"fl_sand"}
ti["##"] = {"st_blocker"}
ti["switch_template"] = {"st_switch"}
ti[".."] = {"fl_abyss"}   — переопределение приводит к ошибке, чтобы исключить работу с неверными данными
ti[".w"] = ti[".."] .. {"it_magicwand"}
ti[" w"] = {"fl_abyss"} .. ti({"it_magicwand"})

Хранилище секций ‘ti’ напоминает список, но ориентированный на хранение определений секций. В качестве ключевого поля разрешается использовать любую строку. Одно и то же определение можно хранить в двух разных ключевых полях. Но уже заполненное ключевое поле переопределить нельзя. Это сделано исключительно с целью защиты от часто возникающих ошибочных ситуаций. Находящееся в хранилище определение может использоваться в последующих определениях. Обращение к записи в хранилище секций по заданному ключевому полю в виде ‘ti[".."]’ возвращает значение секции. Такие значения секций могут быть объединены оператором ‘..’ с другими значениями секций и анонимными списками, содержащими определения объектов. Последний пример — это объединение двух ранее не объявленных определений объектов. Нельзя объединить два анонимных списка. В Lua это запрещено. Объединение становится возможным после преобразования любого из двух списков в значение секции с помощью оператора ‘ti()’.

Заполнение мира:
 
  width, height = wo(ti, "__", { — второй аргумент: ключевое поле секции (по умолчанию), которое 
  "##__......",                  —   также задаёт основу — в этом примере
  "##..__.w__",                  —   для каждой секции/ячейки отводится 2 символа
  "##.. w__.."
  })

Мир заполняется вызовом ‘wo()’, который подробно рассматривается в разделе Создание мира. В простой форме в качестве первого аргумента передаётся ‘ti’. Второй аргумент — код определения секции по умолчанию, который задаёт покрытие по умолчанию, которое будет установлено, если в секции нет другого объекта покрытия. Кроме того, своим размером этот код задаёт стандартную длину кода, используемого в последующей карте. Третий аргумент — карта, задаваемая анонимными списками строк. Размер мира задаётся максимальной длиной передаваемых строк и их количеством. Эти значения возвращаются вызовом.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.2.7 Работа с именованными позициями

Подробности можно узнать в разделе PositionList.

Использование именованных позиций:
 
obj["name"] = "anchor1"
obj:kill()
pos = po["anchor1"]
po["anchor2"] = pos

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

Создание списков позиций:
 
polist = po["deepwater#*"]
polist = po(grp)

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

Использование списка позиций:
 
wo[polist] = ti["x"]
grp = fl(polist)

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

Операции со списком позиций:
 
wo[polist .. po["beach#*"]] = {"it_banana"}

Два списка позиций можно объединить. Также к списку позиций можно добавлять отдельные позиции.

Элементы списка позиций:
 
for i = 1, #polist do
    wo[polist[i]] = {"it_cherry"}
end

К отдельным элемента списка позиций можно обратиться по индексу. Обработать весь список можно с помощью простого цикла (for). Оператор "решётка" (‘#’) возвращает количество позиций в списке.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.3 Примеры сложных уровней на языке Lua

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.3.1 Color Maze

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

 
01    wo["ConserveLevel"] = false
02    wo["FollowGrid"] = false
03    wo["FollowMethod"] = FOLLOW_SCROLL
04
05    ti[" "] = {"fl_fake_abyss"} .. ti({"st_lightglass"})
06
07    ti["!"] = {"fl_blueslab", "blue#", _color="blue"}
08    ti["@"] = {"fl_pinkbumps", "orange#", _color="orange"}
09    ti["#"] = {"fl_redslab", "red#", _color="red"}
10    ti["$"] = {"fl_lawn_b", "green#", _color="green"}
11
12    ti["b"] = ti["!"] .. {"st_door", flavor="d", faces="ns", state=OPEN}
13    ti["B"] = ti["!"] .. {"st_door", flavor="d", faces="ew", state=OPEN}
14    ti["o"] = ti["@"] .. {"st_door", flavor="d", faces="ns", state=OPEN}
15    ti["O"] = ti["@"] .. {"st_door", flavor="d", faces="ew", state=OPEN}
16    ti["r"] = ti["#"] .. {"st_door", flavor="d", faces="ns", state=OPEN}
17    ti["R"] = ti["#"] .. {"st_door", flavor="d", faces="ew", state=OPEN}
18    ti["g"] = ti["$"] .. {"st_door", flavor="d", faces="ns", state=OPEN}
19    ti["G"] = ti["$"] .. {"st_door", flavor="d", faces="ew", state=OPEN}
20
21    ti["d"] = {"it_document", text="text1"}
22    ti["5"] = ti["b"] .. ti["d"]
23    ti["6"] = ti["O"] .. ti["d"]
24    ti["7"] = ti["r"] .. ti["d"]
25    ti["8"] = ti["G"] .. ti["d"]
26
27    ti["x"] = {"it_sensor", invisible=true, target="gates"}
28    ti["*"] = ti["x"] .. {"#ac_marble_black", "me"}
29
30    ti["?"] = {"st_oxyd_a"}
31
32    wo(ti, " ", {
33    --      |         1    1   |2    2
34    --      |1   5    0    5   |0    5
35           "                           ",
36           " xO@OxR#RxO@OxB!BxR#RxB!Bx ", --01
37           " b   r   g   g   b   g   r ",
38           " !   #   $   $   !   $   # ",
39           " b   r   g   g   b   g   r ",
40           " xR#RxB!BxO@OxG$GxO@OxO@Ox ", --05
41           " g   g   r   g   g   b   b ",
42           " $   $   #   $   $   !   ! ",
43           " g   g   r   g   g   b   b ",
44           " xR#RxO@OxG$GxR#RxG$GxR#Rx ",
45           " g   b   b   o       b   r ", --10
46           " $   !   !   @       !   # ",
47           " g   b   5   o   ?   b   r ", --
48           " xO@OxO@6*8$Gx   xG$GxR#Rx ",
49           " r   b   7   b   ?   o   o ",
50           " #   !   #   !       @   @ ", --15
51           " r   b   r   b       o   o ",
52           " xG$GxB!BxR#RxO@OxR#RxG$Gx ",
53           " g   o   o   g   g   o   b ",
54           " $   @   @   $   $   @   ! ",
55           " g   o   o   g   g   o   b ", --20
56           " xB!BxO@OxR#RxR#RxO@OxB!Bx ",
57           " o   r   g   g   b   b   g ",
58           " @   #   $   $   !   !   $ ",
59           " o   r   g   g   b   b   g ", --
60           " xR#RxB!BxB!BxR#RxO@OxR#Rx ", --25
61           "                           "} --
62    --      |         1    1   |2    2
63    --      |1   5    0    5   |0    5
64    )
65
66    last = it(no["me"])   -- the last visited sensor
67    move = 0              -- the count of link moves
68    sequence = {}         -- the sequence of the 4 colors that the user did choose
69
70    function gates(value, sender)
71        if last ~= sender then
72            local middle = last + (sender - last)/2
73            local color = fl(middle)["_color"]
74            if color == nil then return end  -- someone cheated, avoid throwing an exception
75            st(no[color.."#*"]):close()
76            sequence[move%4] = color
77            if move >= 3 then
78                st(no[sequence[(move+1)%4].."#*"]):open()
79            end
80            move = move + 1
81            last = sender
82        end
83    end

Давайте сосредоточимся на новых аспектах, не обсуждавшихся ранее в разделе Примеры простых уровней на языке Lua.

 
01    wo["ConserveLevel"] = false
02    wo["FollowGrid"] = false
03    wo["FollowMethod"] = FOLLOW_SCROLL

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

 
05    ti[" "] = {"fl_fake_abyss"} .. ti({"st_lightglass"})
32    wo(ti, " ", {

Недоступные места были заполнены прозрачным стеклом на чёрном покрытии, как указано в строке 5. Заполнение мира использует это определение секции в качестве секции по умолчанию. Если оно содержит определение покрытия, то всё в порядке. Дополнительные объекты, такие как стеклянные камни, при использовании по умолчанию никогда не будут размещены.

 
07    ti["!"] = {"fl_blueslab", "blue#", _color="blue"}
08    ti["@"] = {"fl_pinkbumps", "orange#", _color="orange"}
09    ti["#"] = {"fl_redslab", "red#", _color="red"}
10    ti["$"] = {"fl_lawn_b", "green#", _color="green"}

Каждому объекту покрытия присваивается автоматически сгенерированное имя, чтобы позже можно было обратиться к группе. К тому же каждый объект покрытия устанавливает пользовательский атрибут, добавляемый после его имени и предваряемый символом подчёркивания ‘_’. Этот атрибут хранит строку, которая позже понадобится нам в функции обратного вызова.

 
12    ti["b"] = ti["!"] .. {"st_door", flavor="d", faces="ns", state=OPEN}
13    ti["B"] = ti["!"] .. {"st_door", flavor="d", faces="ew", state=OPEN}

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

 
27    ti["x"] = {"it_sensor", invisible=true, target="gates"}

Шаги актёров обнаруживаются невидимыми it_sensorами, которые размещаются на каждом пересечении. Их целью является Функция обратного вызоваgates’. Можно опустить действие, так как имя функции представляет собой уникальную цель.

 
66    last = it(no["me"])   — последний пройденный датчик

Переменная Lua, в которой хранится последний пройденный шариком датчик. Изначально он указывает начальную позицию шарика. Мы обращаемся к шарику по имени, а под ним сохраняем предмет датчика, который представляет собой безымянный объект.

 
67    move = 0              — количество переходов через сенсоры
68    sequence = {}         — последовательность из 4 цветов, которую выбрал пользователь

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

 
70    function gates(value, sender)
71        if last ~= sender then

Функция обратного вызова даёт нам возможность работать с отправителем, it_sensor, который вызывает действие. Это текущий датчик. Так как шарик может вернуться к последнему датчику, то перед тем как предпринимать какие-либо действия, мы должны убедиться, что это новый датчик. Достаточно простого сравнения объектов.

 
72            local middle = last + (sender - last)/2
73            local color = fl(middle)["_color"]

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

 
74            if color == nil then return end  — если кто-то жульничает, то не производим никаких действий

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

 
75            st(no[color.."#*"]):close()

Зная цвет, мы хотим закрыть все двери, расположенные на полосках покрытия того же цвета. Мы автоматически дали имена покрытиям, добавив соответствующие приставки к их именам. Таким образом мы можем получить все покрытия заданного цвета, объединив строку с названием цвета и суффикс ‘#*’ и обратившись к хранилищу именованных объектов. Так как нас интересуют двери, мы обращаемся к камням на каждом покрытии. Мы передаём группу покрытий и получаем группу камней. Не на каждом покрытии есть дверь. Это не имеет значения, потому что в результирующую группу камней добавляются только существующие объекты. Определив камни, мы просто посылаем всем им сообщение ‘close’.

 
76            sequence[move%4] = color

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

 
77            if move >= 3 then
78                st(no[sequence[(move+1)%4].."#*"]):open()
79            end

На первых 3 шагах мы просто закрываем двери и запоминаем последовательность цветов. Но, начиная с 4-го шага, нам нужно открыть следующий цвет в последовательности. Мы получаем строку со следующим цветом из списка последовательностей с помощью простого взятия остатка. Получив название цвета, мы делаем тот же трюк, что и в строке 75. Но на этот раз мы посылаем всем связанным дверям сообщения ‘open’.

 
80            move = move + 1
81            last = sender

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

Вот и всё, что нужно запрограммировать для реализации такой сложной идеи уровня, как "Color Maze".


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.3.2 Weirdly Wired

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

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

 
01      <el:compatibility el:enigma="1.10">
02         <el:dependency el:path="lib/libmath" el:id="lib/libmath" el:release="1" el:preload="true"/>
03      </el:compatibility>
...
04    ti[" "] = {"fl_sahara", friction = 3.5, adhesion = 4.0}
05    ti["a"] = {"fl_ivory", friction = 3.5, adhesion = 4.0}
06    ti["b"] = {"fl_bright", friction = 3.5, adhesion = 4.0}
07    ti["c"] = {"fl_platinum", friction = 3.5, adhesion = 4.0}
08    ti["_"] = {"fl_water"}
09    ti["@"] = {"#ac_marble_black"}
10    ti["w"] = {"st_flat_movable", "wood#"}
11    ti["t"] = {"it_trigger", "trigger#"}
12    ti["d"] = {"st_blocker", "door#"}
13    ti["o"] = {"st_oxyd", oxydcolor = OXYD_YELLOW, flavor = "a"}
14    ti["O"] = {"st_oxyd", oxydcolor = OXYD_WHITE, flavor = "a"}
15    ti["1"] = {"st_panel", cluster = 1}
16    ti["2"] = {"st_panel", cluster = 2}
17    ti["S"] = {"st_switch", target = "easy_mode_call"}
18
19    floors  = {ti[" "], ti["a"], ti["b"], ti["c"]}
20    polynom = lib.math.random_vector(10, 4)
21
22    function myresolver(key, x, y)
23      if key == " " then
24        return floors[lib.math.cubic_polynomial(polynom, x, y) % (#floors) + 1]
25      elseif    (key == "#")
26            or ((key == "_") and (random(4) == 1))
27            or ((key == "S") and wo["IsDifficult"]) then
28        return ti[""..random(2)]
29      else
30        return ti[key]
31      end
32    end
33
34    w, h = wo(myresolver, " ", {
35     -- 01234567890123456789
36       "####################___________________",
37       "#                  #_____###o###_______",
38       "#   w   w t   t    #_____#d   d#_______",
39       "#     w   w t   t  #___### ### ###_____",
40       "#  w     t         #___#d d#_#d d#_____",
41       "#                  ##### ###_### ###___",
42       "S    w   w t @ t        d#___#_#d d#___",
43       "#                  #######_####### #___",
44       "#  w     t         #_______O  d# # o___",
45       "#     w   w t   t  #_______### ### #___",
46       "#   w   w t   t    #_________#d   d#___",
47       "#                  #_________###O###___",
48       "####################___________________"
49    })
50
51    door_p = lib.math.permutation(12)
52    wire_p = lib.math.permutation(12)
53    woods = no["wood#*"]
54    triggers = no["trigger#*"]
55    doors = no["door#*"]
56
57    for j = 1, 12 do
58      triggers[j].target = doors[door_p[j]]
59    end
60
61    for j = 1, 9 do
62      wo:add({"ot_wire",
63              anchor1 = woods[wire_p[j + 3]],
64              anchor2 = woods[wire_p[j%3 + 1]]})
65      wo:add({"ot_wire", name = "obsolete_wire#",
66              anchor1 = woods[wire_p[j + 3]],
67              anchor2 = woods[wire_p[j%9 + 4]]})
68    end
69
70    function easy_mode_call(is_on, sender)
71      if is_on then
72        no["obsolete_wire#*"]:kill()
73      else
74        for j = 1, 9 do
75          wo:add({"ot_wire", name = "obsolete_wire#",
76             	  anchor1 = woods[wire_p[j + 3]],
77                anchor2 = woods[wire_p[j%9 + 4]]})
78        end
79      end
80    end

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

 
01       <el:compatibility el:enigma="1.10">
02         <el:dependency el:path="lib/libmath" el:id="lib/libmath" el:release="1" el:preload="true"/>
03       </el:compatibility>

Мы пользуемся некоторыми функциями из библиотеки libmath. Значит, кроме объявления совместимости с Enigma 1.10, нам нужно предварительно загрузить их.

 
04    ti[" "] = {"fl_sahara", friction = 3.5, adhesion = 4.0}
05    ti["a"] = {"fl_ivory", friction = 3.5, adhesion = 4.0}
06    ti["b"] = {"fl_bright", friction = 3.5, adhesion = 4.0}
07    ti["c"] = {"fl_platinum", friction = 3.5, adhesion = 4.0}

Четыре типа покрытий, из которых составлено динамически изменяемое покрытие. Чтобы обеспечить одинаковые ощущения от перемещения по различным типам покрытий, для них устанавливаются одинаковые значения ‘friction’ и ‘adhesion’.

 
10    ti["w"] = {"st_flat_movable", "wood#"}
11    ti["t"] = {"it_trigger", "trigger#"}
12    ti["d"] = {"st_blocker", "door#"}

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

 
13    ti["o"] = {"st_oxyd", oxydcolor = OXYD_YELLOW, flavor = "a"}
14    ti["O"] = {"st_oxyd", oxydcolor = OXYD_WHITE, flavor = "a"}

Небольшая деталь оформления: выбор двух уникальных цветов для st_oxydов.

 
15    ti["1"] = {"st_panel", cluster = 1}
16    ti["2"] = {"st_panel", cluster = 2}

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

 
17    ti["S"] = {"st_switch", target = "easy_mode_call"}

Переключатель в левой части игрового поля, который можно использовать только в упрощённом режиме. В строке 27 он блокируется, чтобы не появляться в обычном режиме. Цель — функция обратного вызова в строках с 71 по 81.

 
19    floors  = {ti[" "], ti["a"], ti["b"], ti["c"]}
20    polynom = lib.math.random_vector(10, 4)

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

 
22    function myresolver(key, x, y)
34    w, h = wo(myresolver, " ", {

До этого момента мы искали коды, используемые в карте, в нашем хранилище секций ‘ti’, которое было первым аргументом в вызове заполнения мира. Но теперь мы используем Настраиваемое преобразование. Функция, начинающаяся со строки 22, вызывается при каждом выборе секции. Её задача — предоставить соответствующую секцию.

 
23      if key == " " then
24        return floors[lib.math.cubic_polynomial(polynom, x, y) % (#floors) + 1]

Эти две строки формируют постоянно меняющееся оформление покрытия. Для каждого кода карты ‘ ’ мы рассчитываем кубический многочлен, который благодаря коэффициентам непредсказуем. Полученное число ограничивается числом наших четырёх покрытий. Это число берётся по индексу в нашем списке покрытий ‘floors’ и возвращает определение секции.

 
25      elseif    (key == "#")
26            or ((key == "_") and (random(4) == 1))
27            or ((key == "S") and wo["IsDifficult"]) then
28        return ti[""..random(2)]

А теперь мы объединяем границы уровня. Сначала нам нужно решить, где вообще их разместить. Безусловно, это будут позиции, отмеченные на карте решёткой ‘#’. Вдобавок мы случайным образом выбираем каждую 4-ую позицию ‘_’, которая вместо покрытия воды будет границей. Наконец, в сложном режиме мы заменяем переключатель, помеченный как ‘S’, на камень границы. Теперь мы должны привязать к этой позиции одну из двух секций групп границы игрового поля. Мы просто случайным образом выбираем число 1 или 2. Но в качестве кода секций нам нужна строка. Мы заставляем Lua преобразовать число в строку, объединив пустую строку ‘""’ со случайным числом. Выбор правильных вариантов границ для формирования замкнутых групп завершается движком.

 
29      else
30        return ti[key]

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

 
34    w, h = wo(myresolver, " ", {
35     -- 01234567890123456789
36       "####################___________________",
37       "#                  #_____###o###_______",
38       "#   w   w t   t    #_____#d   d#_______",
39       "#     w   w t   t  #___### ### ###_____",
...

Карта использует коды в том виде, в каком они были представлены настраиваемым преобразованием. Таким образом, все обязательные камни границы обозначены решёткой ‘#’, а все, которые могут быть преобразованы из воды, обозначены ‘_’. Все пробелы ‘ ’ не заменяются определением песчаного покрытия из хранилища секций, а представляют собой позиции для нашей настройки оформления покрытия в настраиваемом преобразовании. Заметим также, что даже для секций, обозначенных ‘w’, будет установлено оформление покрытия, потому что покрытием по умолчанию является ‘ ’.

 
51    door_p = lib.math.permutation(12)
52    wire_p = lib.math.permutation(12)

Теперь давайте перемешаем привязки триггеров/дверей и распределение проволоки. Сделаем это перестановкой 12 номеров индексов, используемых для доступа к дверям и проволоке.

 
53    woods = no["wood#*"]
54    triggers = no["trigger#*"]
55    doors = no["door#*"]

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

 
57    for j = 1, 12 do
58      triggers[j].target = doors[door_p[j]]
59    end

Произвольная привязка триггеров к дверям. Каждый триггер получает в качестве цели случайный пронумерованный член группы дверей. Отметим альтернативный доступ к члену атрибута триггера. Вместо обрамления имени атрибутов квадратными скобками и заключения в кавычки константной строки в виде ‘["target"]’, автор предпочёл записать ‘.target’. Это допустимое альтернативное выражение Lua, при условии, что имя атрибута — допустимое имя Lua (см. Предупреждение).

 
61    for j = 1, 9 do
62      wo:add({"ot_wire",
63              anchor1 = woods[wire_p[j + 3]],
64              anchor2 = woods[wire_p[j%3 + 1]]})

Наконец, нам нужно добавить ot_wire между нашими перемещаемыми камнями. Это нельзя сделать средствами самой карты. Нам нужно использовать дполнительный метод мираwo:add()’, который принимает в качестве двух атрибутов-якорей два соединяемых камня. Из нашей группы деревянных ящиков мы выбрали первые 3 камня, чтобы соединить их с 3-мя другими камнями с номерами с 4 по 12. Поэтому в каждом цикле в качестве первого "якоря" один из камней с 4 по 12 соединяем с одним из первых 3-х камней, воспользовавшись простой операцией взятия остатка. Теперь у первых трёх камней есть три проволоки и с ними покончено. У оставшихся 9 камней всего по одному проводу.

 
65      wo:add({"ot_wire", name = "obsolete_wire#",
66              anchor1 = woods[wire_p[j + 3]],
67              anchor2 = woods[wire_p[j%9 + 4]]})
68    end

Теперь мы последовательно связываем эти оставшиеся 9 камней в замкнутый круг. Это даёт каждому камню 2 дополнительные проволоки. Мы делаем это, соединяя каждый из камней с 4 по 11 со следующим и, наконец, соединяя камень 12 с камнем 4, что делается операцией взятия остатка от деления. Это завершает составление уровня для обычного режима. Готовясь к упрощённому режиму, мы автоматически именуем эти дополнительные проволоки.

 
71    function easy_mode_call(is_on, sender)
72      if is_on then
73        no["obsolete_wire#*"]:kill()

Специально для упрощённого режима мы добавляем переключатель, чтобы убрать и создать заново дополнительные проволоки. Так как мы присвоили этим более не нужным проволокам имена, то мы можем просто удалить их все одним вызовом, применяя метод ‘kill()’ к группе этих проволок.

 
73      else
74        for j = 1, 9 do
75          wo:add({"ot_wire", name = "obsolete_wire#",
76             	  anchor1 = woods[wire_p[j + 3]],
77                anchor2 = woods[wire_p[j%9 + 4]]})
78        end
79      end

Когда пользователь снова щёлкает переключателем, проволоки должны быть созданы заново. Это делается всё тем же кодом со строки 65 по 68. Заметим, что важно, чтобы мы сохраняли используемые перестановки проволок в переменной ‘wire_p’.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.4 Введение в типы данных

Перед подробным описанием типов данных, давайте познакомимся с основными принципами и соглашениями.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.4.1 Синтаксис и соглашения

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

Следующие сокращения и полученные из них добавлением числа всегда представляют собой значение определённого типа:

Для описания синтаксиса операторов и методов для типов данных необходимо перечислить допустимые типы данных аргументов. Зачастую можно использовать любой из списка нескольких допустимых типов. В таких случаях эти типы будут заключены в угловые скобки (‘<’, ‘>’) и отделены друг от друга чертой (‘|’). Эти символы не входят в состав самих операторов или методов и их не следует помещать в код уровня. В то же время квадратные (‘[’, ‘]’) и фигурные (‘{’, ‘}’) скобки сохраняют своё символьное значение в Lua. Когда эти скобки появляются в синтаксисе, их следует включать в код. Например, следующая синтаксическая конструкция:

 
result = pos + <pos | obj | cpos | polist>

позволяет записать в уровне любую из следующих строк:

 
result = pos + pos
result = pos + obj
result = pos + cpos
result = pos + polist

Однако такая синтаксическая конструкция как:

 
x = pos["x"]

требует, чтобы в код включались квадратные скобки Lua. Конечно, имя переменной позиции и её значение могут быть какими угодно.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.4.2 Значение и ссылка

Одной из важнейших сторон типов данных Lua является различия между фактическим значением и ссылкой на него. Значения — это числа, булевы значения ‘true’ и ‘false’, строки и ‘nil’. Единственный ссылочный тип данных — список Lua.

Значения всегда постоянны. Их нельзя изменять. Значения назначаются переменным. Во время вычислений можно назначить переменной другое значение. Но первоначальное значение никогда не изменяется. Это должно быть понятно, если представить себе значение ‘true’ или, например, число ‘7’. Это справедливо и для строк, например "hello". Результатом объединения двух строк становится новая строка. Однако сами компоненты не меняются. Все методы, "изменяющие строку", в качестве возвращаемого значения выдают новую строку. Поэтому переменная, содержащая значение изначальной строки, всё ещё будет хранить неизменённое значение.

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

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.4.3 Полиморфизм и перегрузка

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

Lua ограничивает набор используемых операторов. Поэтому оператор сложения двух типов данных ‘+’ в зависимости от используемых данных предпримет различные действия. Результатом сложения двух позиций станет векторная сумма, а результатом сложения двух групп будет объединение групп. Такое различное использование одного и того же оператора называется ‘перегрузка’.

Перегрузка в сочетании с полиморфизмом может привести к неоднозначным ситуациям. Например, мы решили разрешить складывать позицию с объектом, результатом чего станет векторное добавление позиции объекта к указанной позиции. В то же время мы хотим, используя оператор ‘+’, объединить объект с существующей группой. Но, что получится в результате сложения двух объектов? Векторная сумма их позиций или объединение объектов в новую группу? Оба варианта имеют смысл и могли бы пригодиться. В таком случае мы отдали предпочтение первой возможности, поскольку операция вычитания, возвращающая векторное расстояние между объектами — очень полезная возможность. В любом случае, всегда можно принудительно использовать нужную операцию, преобразовав объект в позицию или группу. Чтобы чётко понять, что получится, внимательно прочтите приведённые правила синтаксиса.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.4.4 Мнимые типы данных

Хотя мы добавили всего десять основных типов данных, которые описаны в последующих подразделах, API по разному обращается с одними и теми же типами данных в зависимости от их использования. Например, стандартное число Lua используется для описания состояния (state) объекта. В очень редких случаях состояние может отражать реальное число, как например состояние ot_counter. Для большинства объектов состояние — это одно значение из допустимого диапазона, которое в API представлено числом. Поэтому, говоря о значениях состояния (state), мы будем подразумевать мнимые типы данных.

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

Стоит упомянуть один абстрактный тип данных, поскольку он может одновременно использовать два различных мнимых типа данных. Этот особый тип данных используется для описания направления (‘direction’) или ориентации (‘orientation’). Они мало отличаются друг от друга, но мы будем говорить об ориентации, если нас интересует просто список основных направлений в виде числовых значений, чтобы определиться с их выбором. Эти постоянные значения выдаются на запрос ориентаций (orientations).

Иногда значения нужны для вычисления смещений позиции. В этом случае мы подразумеваем направление (‘direction’) и используем значения позиции (Позиция) в качестве векторов смещения. Самые общеупотребительные значения в виде констант можно получить так, как это описано в подразделе Смещения направлений. Заметьте, что нет нужды приводить значения наших направлений к 1.

Заданная ориентация может быть приведена к значению направления с помощью списка преобразования ORI2DIR.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.5 Позиция

Позиция — это пользовательский тип данных Lua, добавленный в Enigma, чтобы работать с позициями мира, как описано в разделе Форма и координаты мира. Позиция представляет собой значение, а значит это константа. После того, как позиция была создана, её нельзя изменить, но с позициями можно работать, используя операторы. Если сложить значения двух позиций, то в результате будет получена позиция с новым значением.

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

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

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

Примеры работы с позициями можно посмотреть в разделе Работа с позициями.

Ниже мы рассмотрим поддерживаемые операторы:


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.5.1 Сложение и вычитание позиций

Синтаксис:

result = pos <+|-> <pos | obj | cpos | polist>

result = <pos | obj | cpos | polist> <+|-> pos

Подробности:

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

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

Примеры синтаксиса:
 
newpos = po(3, 4) + {1, 2}              -- = po(4, 6)
newpos = myobject - po(1, 5)
newpolist = po(2, 3) + NEIGHBORS_4      -- po(1, 3) .. po(2, 4) .. po(3, 3) .. po(2, 2)
newpolist = po["myfloor#*"] - po(3, 0)

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.5.2 Умножение и деление позиций

Синтаксис:

result = pos <*|/> number

result = number * pos

Подробности:

Скалярное произведение или частное вектора позиций. Возвращается значение позиции с обоими координатами, умноженными или поделенными на указанное число.

Примеры синтаксиса:
 
newpos = 3 * po(3, 4)   -- = po(9, 12)
newpos = po(2, 3) / 2   -- = po(1, 1.5)

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.5.3 Смена знака позиции

Синтаксис:

result = -pos

Подробности:

Унарное скалярное умножение вектора позиции на ‘-1’. Возвращается новое значение позиции, обе координаты которого умножены на ‘-1’.

Примеры синтаксиса:
 
newpos = -po(3, 4)   -- = po(-3, -4)

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.5.4 Округление позиции до центра решётки

Синтаксис:

result = #pos

Подробности:

Округление вектора позиции до координат центра участка решётки. Возвращается новое значение позиции с координатами центра участка решётки с данной позицией.

Примеры синтаксиса:
 
newpos = #po(3, 4)   -- = po(3.5, 4.5)

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.5.5 Сравнение позиций

Равенство и неравенство.

Синтаксис:

result = pos1 <==|~=> pos2

Подробности:

Сравнение значений двух позиций. Два значения позиции равны, если обе их координаты равны. В противном случае они не эквивалентны. Если необходимо узнать, указывают ли обе позиции на один и тот же участок решётки, то можно предварительно округлить обе позиции. Округлить можно либо к центру, либо к участку решётки, воспользовавшись, соответственно, оператором для позиций ‘#’ или методом ‘grid()’.

Примеры синтаксиса:
 
bool = po(3, 4) == po({3, 4})  -- = true
bool = po(3, 4) == po(4, 3)    -- = false
bool = po(3, 4) ~= po(4, 3)    -- = true

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.5.6 Объединение позиций

Синтаксис:

result = pos1 .. <pos2 | polist>

result = <pos1 | polist> .. pos2

Подробности:

Объединяет две позиции или позицию с существующим списком позиций (PositionList) в новый список позиций, содержащий все позиции в указанном порядке. Следует отметить, что эта операция ассоциативна, то есть не имеет значения, используются ли при множественном объединении скобки.

Примеры синтаксиса:
 
newpolist = po(3, 4) .. po(4, 4)

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.5.7 Доступ к координатам позиций

Синтаксис:

result = pos["x"]

result = pos["y"]

result1, result2 = pos:xy()

Подробности:

Отдельные координаты позиции можно получить в любой момент, воспользовавшись доступом Lua по индексу, заключённому в квадратные скобки. Конечно, можно использовать и альтернативный синтаксис Lua, предоставляющий доступ к индексу через точку (см. примеры). Чтобы узнать обе координаты, можно воспользоваться методом ‘xy()’, который вернёт одновременно два числа в виде перечисления Lua.

Примеры синтаксиса:
 
number = po(3, 4)["x"]            -- = 3
number = po(3, 4).x               -- = 3
number = po(3, 4)["y"]            -- = 4
number = po(3, 4).y               -- = 4
number1, number2 = po(3, 4):xy()  -- = 3, 4

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.5.8 Округление позиции до границ решётки

Синтаксис:

result = pos:grid()

Подробности:

Возвращает новое значение позиции, которое указывает на координаты верхнего левого угла участка решётки, включающего позицию.

Примеры синтаксиса:
 
newpos = po(3.2, 4.7):grid()    -- = 3, 4
newpos = po(-2.4, -5.0):grid()  -- = -3, -5

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.5.9 Существование позиции

Синтаксис:

result = pos:exists()

Подробности:

Проверяет, входит ли позиция в состав мира, и возвращает ‘true’, если это так. В противном случае возвращается ‘false’.

Следует отметить, что метод ‘exists’ для Объекта сообщает о существовании объекта. Результат выполнения ‘po(obj):exists()’ для существующих объектов может быть и ‘false’. Например, это может произойти с предметами (см. раздел Предметы), в данный момент находящимися в инвентаре игрока. Предмет существует, но он не входит в состав мира. В то же время предметы, хранящиеся в сумке, которая лежит в пределах мира, вернут в качестве своих позиций позицию сумки.

Примеры синтаксиса:
 
boolean = po(3.2, 4.7):exists()

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.6 Объект

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

С другой стороны, из-за действий игрока объект может быть уничтожен, хотя ссылка на него всё ещё будет доступна через переменную Lua. Конечно, эта ссылка становится недействительной как только объект, на который она ссылается, будет удалён. Но в новом API такая недействительная ссылка, которая называется ‘нулевой’ (‘NULL’ reference), больше не становится фатальной. Любые попытки что-то записать по этой ссылке просто игнорируются. Поэтому ссылкам на объекты можно посылать сообщения, независимо от их действительности. Только перед записью, возможно, придётся предварительно проверить, существует ли объект, потому что при обращении к нулевым ссылкам будет получено значение ‘nil’.

У объектов есть атрибуты, к которым можно обратиться с помощью индексных методов Lua. В дополнение к отличительным атрибутам объекта можно добавлять свои собственные. Собственные атрибуты начинаются с приставки ‘_’ перед их именем.

Объекты мира создаются назначением позиции мира (см. раздел Мир) описания секции. Получить ссылку на соответствующий объект можно либо с помощью хранилища именованных объектов (см. раздел NamedObjects), функций (см. раздел Функции) либо другими методами, которые возвращают ссылку на отдельный объект.

Объекты поддерживают большую часть методов позиции (см. раздел Позиция) и в большинстве случаев могут напрямую использоваться в качестве позиций без явного преобразования. Для обоих типов отличаются только специализированные методы, такие как проверка на существование. Конечно расположение всех объектов мира ограничено пределами мира. Но помните, что переносные Предметы могут находиться в инвентаре игрока и возвращать позицию за пределами мира. Актёры всегда возвращают позиции, округлённые до координат содержащей их ячейки решётки. Это унаследованная возможность. Поскольку код Lua в любом случае не очень подходит для работы с перемещениями актёров, мы сохранили эту возможность округления.

Объекты поддерживают и стандартный набор операторов групп (см. раздел Группа), если одним из аргументов является группа, а другим объект.

Примеры работы можно посмотреть в разделах Работа с объектами и Работа с атрибутами.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.6.1 Доступ к атрибутам объекта

Синтаксис:

result = obj["attributename"]

obj["attributename"] = value

obj:set({attributename1=value1, attributename2=value2,...})

Подробности:

Читает или записывает атрибуты объекта, как описано в следующих главах, или пользовательские атрибуты. Метод ‘set’ позволяет изменить одновременно несколько атрибутов. Если ссылка на объект недействительна, то попытка записи игнорируется. Чтобы прочитать значение атрибута необходима действительная ссылка на объект. В противном случае возвращается ‘nil’.

Примеры синтаксиса:
 
value = obj["color"]
value = obj.color
obj["color"] = BLACK
obj.color = BLACK
obj:set({target=mydoor, action="open"})

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.6.2 Отправка сообщений объектам

Синтаксис:

result = obj:message("msg", value)

result = obj:msg(value)

Подробности:

Посылает объекту сообщение с указанным значением или значением равным ‘nil’. Любое сообщение может быть послано напрямую, через вызов метода с указанием имени сообщения. Если ссылка на объект недействительна, то сообщение просто игнорируется.

Примеры синтаксиса:
 
value = obj:message("open")
value = obj:open()
value = obj:message("signal", 1)
value = obj:signal(1)

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.6.3 Сравнение объектов

Синтаксис:

result = obj1 <==|~=> obj2

Подробности:

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

Примеры синтаксиса:
 
bool = obj1 == obj1  -- = true
bool = obj1 == obj2  -- = false, if two different objects
bool = obj1 ~= obj2  -- = true, if two different objects

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.6.4 Существование объекта

Синтаксис:

result = -obj

result = obj:exists()

Подробности:

Проверяет действительность ссылки на объект. Возвращает истину, если объект всё ещё существует, в противном случае, для нулевых ссылок на объект (‘NULL’), возвращает ложь.

Примеры синтаксиса:
 
bool = -obj
bool = obj:exists()

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.6.5 Уничтожение объекта

Синтаксис:

obj:kill()

Подробности:

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

Примеры синтаксиса:
 
obj:kill()

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.6.6 Определение типа объекта

Синтаксис:

result = obj:is("kind")

result = obj:kind()

Подробности:

Эти методы позволяют определить и получить Тип объекта.

Примеры синтаксиса:
 
bool = obj:is("st_chess")
string = obj:kind()

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.6.7 Доступ к координатам объекта

Синтаксис:

result = obj["x"]

result = obj["y"]

result1, result2 = obj:xy()

Подробности:

Отдельные координаты позиции объекта можно прочитать в любой момент, воспользовавшись доступом Lua по индексу, заключённому в квадратные скобки. Конечно, можно использовать и альтернативный синтаксис Lua, предоставляющий доступ к индексу через точку (см. примеры). Чтобы узнать обе координаты, можно воспользоваться методом ‘xy()’, который вернёт одновременно два числа в виде перечисления Lua. В любом случае координаты объекта доступны только для чтения. Нельзя переместить объект, изменив его координаты.

Примеры синтаксиса:
 
number = obj["x"]
number = obj.x
number = obj["y"]
number = obj.y
number1, number2 = obj:xy()

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.6.8 Сложение и вычитание объектов

Синтаксис:

result = obj <+|-> <pos | obj | cpos | polist>

result = <pos | obj | cpos | polist> <+|-> obj

Подробности:

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

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

Примеры синтаксиса:
 
newpos = obj + {1, 2}
newpos = myobject - obj
newpolist = obj + NEIGHBORS_4
newpolist = po["myfloor#*"] - obj

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.6.9 Округление объекта до центра решётки

Синтаксис:

result = #obj

Подробности:

Округление вектора позиции объектов до координат центра участка решётки. Возвращается новое значение позиции, с координатами центра участка решётки с данной позицией.

Примеры синтаксиса:
 
newpos = #obj   -- e.g. po(3.5, 4.5)

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.6.10 Объединение объектов

Синтаксис:

result = obj + group

result = group + obj

Подробности:

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

Примеры синтаксиса:
 
newgroup = obj1 + grp(obj2, obj3, obj1)   -- = grp(obj1, obj2, obj3)
newgroup = grp(obj2, obj3) + obj1         -- = grp(obj2, obj3, obj1)

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.6.11 Пересечение объектов

Синтаксис:

result = obj * group

result = group * obj

Подробности:

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

Примеры синтаксиса:
 
newgroup = obj1 * grp(obj1, obj2)  -- = grp(obj1)
newgroup = grp(obj2) * obj1         -- = grp()

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.6.12 Разность объектов

Синтаксис:

result = obj - group

result = group - obj

Подробности:

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

Примеры синтаксиса:
 
newgroup = obj1 - grp(obj2, obj1)  -- = grp()
newgroup = grp(obj1, obj2) - obj1  -- = grp(obj2)

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.6.13 Звук объекта

Синтаксис:

result = obj:sound("name", volume)

Подробности:

С позиции объекта проигрывается звук с указанным именем. По умолчанию громкость устанавливается в значение ‘1’.

Примеры синтаксиса:
 
obj:sound("quake")
obj:sound("quake", 2)

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.7 Группа

Группа — это сортированные набор Объектов. Каждый объект может содержаться в группе только в одном экземпляре. В отличие от списка Lua, группа — тип данных с постоянным значением. Группу нельзя изменить, после того как она была получена. Но группы могут участвовать в вычислениях и к ним могут применяться такие операторы, как объединение, пересечение и разность, в результате которых будут получены другие значения групп.

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

Группа создаётся перечислением входящих в неё объектов в качестве аргументов Функцииgrp()’, получением объектов из хранилища NamedObjects или в результате других методов и вычислений.

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

Все операции с группами, которые возвращают новые группы, убирают из своих результатов недействительные на момент выполнения операции нулевые ссылки на объект. Эту возможность можно использовать, чтобы очистить группу при помощи функции ‘grp()’.

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

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

Примеры работы можно посмотреть в разделе Работа с группами.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.7.1 Отправка сообщений группам

Синтаксис:

result = group:message("msg", value)

result = group:msg(value)

Подробности:

Отправляет сообщение с заданным значением или с ‘nil’ вместо сообщения всем объектам группы. Любое сообщение может быть послано напрямую, через вызов метода с указанием имени сообщения. Если ссылка на объект недействительна, то сообщение просто игнорируется. В качестве возвращаемого значения выступает значение, возвращённое сообщением последнему объекту группы, или, для пустой группы, ‘nil’.

Можно даже послать всем объектам группы сообщение ‘kill()’. Все объекты группы будут уничтожены, но группа сама останется и будет содержать недействительные нулевые ссылки на объекты.

Примеры синтаксиса:
 
value = group:message("open")
value = group:open()
value = group:message("signal", 1)
value = group:signal(1)
value = group:kill()

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.7.2 Запись атрибутов в группу

Синтаксис:

group["attributename"] = value

group:set({attributename1=value1, attributename2=value2,...})

Подробности:

Задаёт значения атрибутов, описанных в следующих разделах, или пользовательских атрибутов для всех объектов группы. Метод ‘set’ позволяет изменить одновременно несколько атрибутов. Если ссылка на объект недействительна, то запись атрибутов не производится. Групповое чтение атрибута невозможно — доступ на чтение к элементу с указанным индексом — это перегруженная операция, доступная только для элементов группы.

Примеры синтаксиса:
 
group["color"] = BLACK
group.color = BLACK
group:set({target=mydoor, action="open"})

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.7.3 Сравнение групп

Синтаксис:

result = group1 <==|~=> group2

Подробности:

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

Примеры синтаксиса:
 
bool = grp(obj1, obj2) == grp(obj2, obj1)  -- = true
bool = grp(obj1, obj2) == grp(obj1, obj3)  -- = false, если содержимое объектов отличается
bool = grp(obj1) ~= grp(obj2, obj1)        -- = true, если содержимое объектов отличается

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.7.4 Размер группы

Синтаксис:

result = #group

Подробности:

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

Примеры синтаксиса:
 
number = #grp(obj1, obj2)         -- = 2
for i = 1, #group do obj = group[i] ... end

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.7.5 Доступ к элементам группы

Синтаксис:

result = group[index]

result = group[obj]

Подробности:

Для индексов в диапазоне от 1 до #group этот основанный на индексе доступ на чтение возвращает объект, находящийся в группе на соответствующем месте. Отрицательные индексы из диапазона от -#groupдо -1 возвращают те же объекты. Следовательно, к последнему объекту всегда можно обратиться по индексу -1. Обращение ко всем остальным индексам возвращает недействительную нулевую ссылку на объект (‘NULL’), а не ‘nil’, как делают списки! Поэтому всегда можно послать сообщения по полученным ссылкам на объект.

Основанный на индексе доступ на чтение возвращает порядковый номер указанного объекта в группе, его индекс, если объект присутствует в группе, или ‘nil’, если отсутствует.

Примеры синтаксиса:
 
object = grp(obj1, obj2)[2]     -- = obj2
object = grp(obj1, obj2)[-1]    -- = obj2
object = grp(obj1, obj2)[0]     -- = NULL object
for i = 1, #group do obj = group[i] ... end
number = grp(obj1, obj2)[obj2]  -- = 2
number = grp(obj1, obj2)[obj3]  -- = nil

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.7.6 Обход элементов группы в цикле

Синтаксис:

for obj in group do ... end

Подробности:

Обход всех объектов группы в цикле. Тело цикла выполняется по порядку для всех объектов, включая недействительные нулевые ссылки на объект.

Примеры синтаксиса:
 
for obj in group do obj:toggle() end

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.7.7 Объединение групп

Синтаксис:

result = group + <obj|group>

result = <obj|group> + group

Подробности:

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

Примеры синтаксиса:
 
newgroup = obj1 + grp(obj2, obj3, obj1)   -- = grp(obj1, obj2, obj3)
newgroup = grp(obj2, obj3) + grp(obj1, obj3)   -- = grp(obj2, obj3, obj1)

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.7.8 Пересечение групп

Синтаксис:

result = <obj|group> * group

result = group * <obj|group>

Подробности:

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

Примеры синтаксиса:
 
newgroup = obj1 * grp(obj2, obj1)  -- = grp(obj1)
newgroup = grp(obj1, obj2) * grp(obj2, obj1, obj3)  -- = grp(obj1, obj2)

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.7.9 Разность групп

Синтаксис:

result = <obj|group> - group

result = group - <obj|group>

Подробности:

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

Примеры синтаксиса:
 
newgroup = obj1 - grp(obj2, obj1)  -- = grp()
newgroup = grp(obj1, obj2, obj3) - grp(obj2, obj4)  -- = grp(obj1, obj3)

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.7.10 Перемешивание группы

Синтаксис:

result = group:shuffle()

Подробности:

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

Примеры синтаксиса:
 
newgroup = grp(obj1, obj2)

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.7.11 Сортировка группы

Синтаксис:

result = group:sort("circular")

result = group:sort("linear" <, direction>)

result = group:sort()

Подробности:

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

Если в качестве параметра выступает строка "circular", то объекты располагаются вокруг их центра на определённом угловом расстоянии друг от друга. Расстояние до центра не имеет значения.

Если в качестве параметра выступает строка "linear", то объекты располагаются линейно. В качестве вектора направления сортировки можно задать позицию (см. раздел Позиция) или, по умолчанию, первые два объекта группы.

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

Примеры синтаксиса:
 
newgroup = grp(obj1, obj2, obj3):sort("linear", po(2,1))
newgroup = grp(obj1, obj2, obj3):sort("circular")
newgroup = grp(obj1, obj2, obj3):sort()

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.7.12 Выделение подгруппы

Синтаксис:

result = group:sub(number)

result = group:sub(start, end)

result = group:sub(start, -number)

Подробности:

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

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

Два положительных числа-параметра задают первый и последний индексы последовательности запрашиваемых объектов.

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

Примеры синтаксиса:
 
newgroup = grp(obj1, obj2, obj3, obj4):sub(2)     -- = grp(obj1, obj2)
newgroup = grp(obj1, obj2, obj3, obj4):sub(-2)    -- = grp(obj3, obj4)
newgroup = grp(obj1, obj2, obj3, obj4):sub(2, 4)  -- = grp(obj2, obj3, obj4)
newgroup = grp(obj1, obj2, obj3, obj4):sub(2, -2) -- = grp(obj2, obj3)

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.7.13 Ближайший объект из группы

Синтаксис:

result = group:nearest(obj)

Подробности:

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

Примеры синтаксиса:
 
newobject = grp(obj1, obj2, obj3):nearest(obj4)

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.8 NamedObjects

Тип данных NamedObjects используется только одним объектом, единым хранилищем именованных объектов. При именовании объектов (см. раздел Именование объектов), это хранилище записывает его имя и позволяет позже обратиться к нему по имени.

Поскольку NamedObjects един, то нельзя создавать другие его экземпляры. При загрузке уровня экземпляр данного объекта хранится в глобальной переменной ‘no’.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.8.1 Запрос к хранилищу NamedObjects

Синтаксис:

result = no["name"]

Подробности:

Запрос одного или нескольких объектов из хранилища. Если в имени не задан шаблон, то возвращается значение типа Объект. Это либо уникальный объект с указанным именем, либо, если объекта с заданным именем нет, — недействительный нулевой (‘NULL) объект.

Если в запрашиваемом имени есть символы шаблона, такие как звёздочка ‘*’ или знак вопроса ‘?’, то возвращается Группа, содержащая все объекты с подходящими именами. Звёздочке соответствует ни одного, один или несколько произвольных символов. Знаку вопроса соответствует один произвольный символ. Оба символа шаблона могут использоваться в любом месте строки и в любом количестве. В любом случае возвращаемым значением всегда будет Группа. В группе может содержаться несколько объектов, один объект или вообще не быть ни одного объекта, если не существует объектов, удовлетворяющих шаблону.

Примеры синтаксиса:
 
obj = no["mydoor"]       — точное совпадение имени
group = no["mydoors#*"]  — любой суффикс
group = no["mydoor?"]    — суффикс из одного символа
group = no["mydoors?#*"] — соответствует, например, "mydoorsA#123435", "mydoorsB#1213"

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.8.2 Именование объектов NamedObjects

Синтаксис:

no["name"] = obj

Подробности:

Доступ по индексу для записи в хранилище позволяет назначить объекту имя или переименовать его. Заметьте, что назначить объекту имя или переименовать его также можно с помощью записи атрибута Объекта. Имя объекта хранится в атрибуте "name". Оба способа именования объекта абсолютно эквивалентны.

Примеры синтаксиса:
 
no["myobject"] = obj

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.9 PositionList

PositionList — это упорядоченный набор, или список, позиций (см. раздел Позиция). Как и Группа, это вместилище — постоянный тип данных, который нельзя изменить. Но, объединяя существующие списки и отдельные позиции, можно легко создавать новые списки позиций.

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

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

Группу можно легко преобразовать в список позиций, воспользовавшись единым хранилищем позиций, с помощью простого выражения ‘po(group)’. А все объекты (см. раздел Объект) общего типа, расположенные по контуру, описываемому списком позиций, можно получить с помощью таких функций (см. раздел Функции), как ‘st(polist)’, ‘it(polist)’ и ‘fl(polist)’.

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

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

Следует отметить, что в отличии от группы (см. раздел Группа), этот тип данных не может храниться прямо в атрибуте объекта (см. раздел Объект). Однако всегда можно хранить в атрибутах группу покрытий. Если есть вероятность, что покрытия будут уничтожены, то, возможно, им следует присвоить имена, как описано в разделе Именованные позиции.

Примеры работы можно посмотреть в разделе Работа с именованными позициями.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.9.1 Сравнение PositionList

Синтаксис:

result = polist1 <==|~=> polist2

Подробности:

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

Примеры синтаксиса:
 
bool = (po(2,3).. po(5,7)) == (po(2,3) .. po(5,7))  -- = true
bool = (po(2,3).. po(5,7)) == (po(4,0) .. po(5,7))  -- = false, различные позиции
bool = (po(2,3).. po(5,7)) == (po(5,7) .. po(2,3))  -- = false, различный порядок

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.9.2 Размер PositionList

Синтаксис:

result = #polist

Подробности:

Количество позиций в списке.

Примеры синтаксиса:
 
number = #(po(2,3) .. po(5,7)) -- = 2
for i = 1, #polist do pos = polist[i] ... end

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.9.3 Доступ к элементам PositionList

Синтаксис:

result = group[index]

Подробности:

Для индексов в диапазоне от 1 до #polist этот основанный на индексе доступ на чтение возвращает позицию, находящуюся в группе на соответствующем месте. Отрицательные индексы из диапазона от -#polist до -1 возвращают те же позиции. Следовательно, к последней позиции всегда можно обратиться по индексу -1. Обращение ко всем остальным индексам, как и ко всем спискам Lua, возвращает ‘nil’.

Примеры синтаксиса:
 
pos = (po(2,3) .. po(5,7))[2]     -- = po(5,7)
pos = (po(2,3) .. po(5,7))[-1]    -- = po(5,7)
pos = (po(2,3) .. po(5,7))[0]     -- = nil
for i = 1, #polist do pos = polist[i] ... end

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.9.4 Объединение PositionList

Синтаксис:

result = polist1 .. <pos | polist2>

result = <pos | polist1> .. polist2

Подробности:

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

Примеры синтаксиса:
 
newpolist = po(po(2,3), po(5,7)) .. po(4, 4) -- = (2,3),(5,7),(4,4)
Предупреждение:

Обратите внимание, что в силу числовой природы списков позиций их объединение создаёт новое значение. Эта операция требует значительного объема ресурсов компьютера. При сборе потенциально большого количества позиций в цикле не следует объединять каждого нового кандидата с существующим списком позиций. Не создавайте большого количества значений списка позиций и объединяйте позиции в стандартном списке Lua. Преобразуем данную таблицу в список позиций (см. раздел Преобразование PositionList):

 
result = {}
for x = 1, 200 do
    table.insert(result, po(x, 17))
end
return po(result)

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.9.5 Смещение PositionList

Синтаксис:

result = polist <+|-> <pos | obj | cpos>

result = <pos | obj | cpos> <+|-> polist

Подробности:

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

Примеры синтаксиса:
 
newpolist = po(2, 3) + NEIGHBORS_4      -- po(1, 3) .. po(2, 4) .. po(3, 3) .. po(2, 2)
newpolist = po["myfloor#*"] - po(3, 0)

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.9.6 Масштабирование PositionList

Синтаксис:

result = polist * number

result = number * polist

Подробности:

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

Примеры синтаксиса:
 
newpolist = 2 * NEIGHBORS_4              -- = po(9, 12)
newpolist = (po(2,4) .. po(6,7)) * 1/2   -- = (1, 2), (3, 3.5)

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.10 Хранилище позиций

Тип данных ‘позиция’ используется только в одном случае — единым хранилищем именованных позиций. Кроме работы с именованными позициями он предоставляет полезные преобразования других типов данных к типам, основанным на позиции.

Поскольку хранилище позиций едино, то нельзя создать его новый экземпляр. Хранилище становится доступным при загрузке уровня, а его элементы хранятся в глобальной переменной ‘po’.

Хранилище позиций — это расширение хранилища именованных объектов (см. раздел NamedObjects). При любом присвоении объекту имени (см. раздел Именование объектов) в этом хранилище сохраняется имя объекта и позже к текущей позиции объекта можно обратиться по имени. Но даже если объект покрытия будет уничтожен, его позиция будет храниться как именованная позиция (см. раздел Именованные позиции). Конечно, можно самостоятельно присваивать позициям имена, но у позиций существующих именованных объектов при определении конфликтов имён всегда будет преимущество.

Примеры работы можно посмотреть в разделе Работа с именованными позициями.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.10.1 Запрос к хранилищу позиций

Синтаксис:

result = po["name"]

Подробности:

Запрос из хранилища одной или нескольких позиций. Если в имени не используются символы шаблона, будет возвращено значение позиции (см. раздел Позиция) конкретного объекта с заданным именем, если тот существует. Если объект с таким именем не существует, то будет возвращена последняя позиция, хранившаяся под этим именем. Если позиция не существует, то будет возвращено значение ‘nil’.

Если в запрашиваемом имени есть символы шаблона, такие как звёздочка ‘*’ или знак вопроса ‘?’, то возвращается PositionList, содержащий все позиции с подходящими именами. Звёздочке соответствует ни одного, один или несколько произвольных символов. Знаку вопроса соответствует один произвольный символ. Оба символа шаблона могут использоваться в любом месте строки и в любом количестве. В любом случае возвращаемым значением всегда будет PositionList. В списке может содержаться несколько позиций, одна позиция или вообще не быть ни одной позиции, если не существует позиций, удовлетворяющих шаблону.

Примеры синтаксиса:
 
pos = po["mydoor"]        — точное совпадение имени
polist = po["mydoors#*"]  — любой суффикс
polist = po["mydoor?"]    — суффикс из одного символа
polist = po["mydoors?#*"] — соответствует, например, "mydoorsA#123435", "mydoorsB#1213"

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.10.2 Доступ к элементам хранилища позиций

Синтаксис:

po["name"] = obj

Подробности:

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

Примеры синтаксиса:
 
po["mypos"] = pos

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.10.3 Преобразование позиций

Синтаксис:

result = po(<obj | pos | {x, y} | x, y >)

Подробности:

Преобразовывает свой параметр в новое значение позиции.

Примеры синтаксиса:
 
pos = po(pos2)
pos = po(obj)
pos = po({2, 4})
pos = po(3, 7)

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.10.4 Преобразование PositionList

Синтаксис:

result = po(group | {pos1, pos2, pos3})

Подробности:

Преобразовывает указанную группу или список в новый PositionList, который содержит позиции всех действительных Объектов группы или списка в той же последовательности.

Примеры синтаксиса:
 
polist = po(group)
polist = po({po(3, 7), po(2, 6)})
polist = po({})  — пустой список позиций

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.11 Описание секций и объектов

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

 
{"st_chess", name="jumper", color=WHITE}
{"st_chess_white", "jumper", _myattr=5}
{"ac_marble", 0.2, 0.6, name="blacky"}

Первое поле, хранящееся в списке на первом месте, всегда должно быть названием поддерживаемого Enigma типа объекта. В первом примере все остальные поля списка представляют собой пары ‘ключ=значение’, где ключом выступает название атрибута. Во втором примере используется сокращение, позволяющее указывать вторым параметром просто имя, которое будет храниться в списке во втором поле. Это должна быть строка. Третья разновидность, полезная для описания актёров, во втором и третьем полях списка хранит координаты смещения по сетке. Конечно, в этом описании нельзя использовать ещё и сокращение, позволяющее указать вторым параметром просто имя.

Такие табличные описания объектов всегда полезны для быстрого описания отдельного объекта. Но в секциях довольно часто помимо покрытия используются предмет или камень. Поэтому требуется тип данных Enigma, который может работать с несколькими описаниями. Таким типом данных является ‘секция’ (‘tile’). Она может принимать как описание одного объекта, так и произвольный список описаний. Табличное описание объекта можно преобразовать в секцию посредством хранилища секций (см. раздел Хранилище секций). После того, как будет получена секция, её можно объединять с другими секциями или табличными описаниями объектов, чтобы получить новые секции.

Enigma гарантирует, что объекты появятся в мире в порядке описаний в секции.

Хотя в большинстве случаев для создания объектов используются объявления объектов и секции, в некоторых сложных случаях может потребоваться использование такого типа данных, когда не требуется ничего добавлять на уровень или даже требуется уничтожить объект, который может существовать на секции. В таких случаях можно использовать наименования псевдотипов объектов — "fl_nil", "it_nil", "st_nil" или "nil". В то время как первые три псевдотипа уничтожат имеющиеся в соответствующем слое объекты, последний псевдотип ничего не будет делать. Использование данного типа аналогично использованию в качестве объявления объекта пустой таблицы Lua, но более выразительно:

 
ti["D"] = cond(wo["IsDifficult"], {"st_death"}, {"nil"})
ti["S"] = {"st_surprise", selection={"st_box", "st_nil"}}
function customresolver(key, x, y)
    if key == "a" then
        return {"nil"}
    elseif key == "b" then
        return {}
    else
        return ti[key]
    end
end

В первом примере псевдотип используется в качестве допустимого третьего аргумента функции cond, что позволяет избежать ошибок синтаксиса при передаче миру в лёгком режиме.

Во втором примере псевдотип используется, чтобы уничтожить объект st_surprise без создания камня-замены.

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

Примеры работы можно увидеть в разделе Работа с секциями и миром.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.11.1 Объединение секций

Синтаксис:

result = tile .. <tile | odecl>

result = <tile | odecl> .. tile

Подробности:

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

Следует отметить, что оператор ‘..’ выполняется в Lua справа налево! Поэтому нужно правильно располагать скобки или быть уверенным, что хотя бы одна из двух крайних справа меток — секция.

Примеры синтаксиса:
 
newtile = ti{"st_chess"} .. {"fl_sahara"}
newtile = ti{"st_chess"} .. {"fl_sahara"} .. {"it_cherry"}   — ошибка Lua из-за выполнения справа налево
newtile = (ti{"st_chess"} .. {"fl_sahara"}) .. {"it_cherry"} — явное указание порядка выполнения
newtile = ti{"st_chess"} .. {"fl_sahara"} .. ti{"it_cherry"} — одно из двух значимых описаний преобразовано к типу секции

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.12 Хранилище секций

Тип данных ‘секция’ используется только в одном случае — единым хранилищем описаний секций и объектов (см. раздел Описание секций и объектов). Кроме работы с секциями он предоставляет полезные преобразования основанных на списках описаний объектов к секциям.

Поскольку хранилище секций едино, то нельзя создать его новый экземпляр. Хранилище становится доступным при загрузке уровня, а его элементы хранятся в глобальной переменной ‘ti’.

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

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

Примеры работы можно увидеть в разделе Работа с секциями и миром.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.12.1 Запись секций

Синтаксис:

ti["key"] = <tile|odecl>

Подробности:

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

Примеры синтаксиса:
 
ti["#"] = tile
ti["$"] = {"st_chess"}
ti["$"] = {"st_switch"}   — ошибка переназначения ключа
ti["anykey"] = {"st_chess"}

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.12.2 Запрос секции

Синтаксис:

result = ti["key"]

Подробности:

Запрос секции, которая была назначена указанному ключу. Если в ключе ещё не хранится секция, то будет возвращено значение Lua ‘nil’. Следует отметить, что хранилище секций, в отличие от хранилищ именованных объектов и позиций, не работает с шаблонами. Звёздочка ‘*’ и знак вопроса ‘?’ — такие же ключи, как и остальные символы.

Примеры синтаксиса:
 
tile = ti["#"]

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.12.3 Преобразование секции

Синтаксис:

result = ti(odecl)

Подробности:

Преобразовывает основанное на списке описание объекта к новому значению секции.

Примеры синтаксиса:
 
tile = ti({"st_chess"})
tile = ti{"st_chess"}   — эквивалент в синтаксисе Lua

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.13 Мир

Тип данных ‘мир’ существует только в одном экземпляре, это ещё один единый объект. После загрузки уровня ссылка на этот единый объект хранится в глобальной переменной Lua ‘wo’. Поскольку данный объект един, то нельзя создать его новый экземпляр.

Однако хотя единый объект ‘wo’ уже существует после загрузки уровня, мир всё ещё окончательно не определён. Уже в первой строке кода Lua можно использовать Глобальные атрибуты. Но мир фактически становится завершённым после создания мира (см. раздел Создание мира). После этого вызова у мира появляются вполне определённые размеры и он заполняется начальным набором объектов, с которыми, начиная с этого момента, уже можно работать.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.13.1 Создание мира

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

Синтаксис:

width, height = wo(topresolver, defaultkey, map)

width, height = wo(topresolver, libmap)

width, height = wo(topresolver, defaultkey, width, height)

topresolver = ti | resolver | localresolver

У любой секции есть код, который необходимо преобразовать к её описанию. Это можно сделать либо с помощью хранилища секцийti’, либо с помощью библиотеки Преобразования или используя функцию локального настраиваемого преобразования (см. раздел Настраиваемое преобразование). Этот аргумент задаёт преобразование наивысшего уровня, которое будет вызываться раньше остальных.

defaultkey

Строка, которая определяет код, присваиваемый по умолчанию. Он используется, если не задан ни один код, и присваивается секции при отсутствии покрытия. Количество символов этого кода определяет размер кода на карте.

map

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

libmap

Карта, полученная с помощью библиотеки libmap.

width

Полученный вместо карты, этот аргумент задаёт желаемую ширину мира.

height

Полученный вместо карты, этот аргумент задаёт желаемую высоту мира.

Примеры синтаксиса:
 
w, h = wo(ti, "  ", 20, 13)
w, h = wo(resolver, " ", {
       "                    ",
       ...
       "                    "})
w, h = wo(ti, mylibmap)
Подробности:

Этот конструктор мира может быть вызван лишь единожды. Каждый последующий вызов приводит к ошибке. Этот вызов устанавливает размеры мира согласно заданным значениям, которые можно узнать с помощью его двух возвращаемых значений. Также размер мира позже может быть получен через атрибуты мира Width и Height.

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

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.13.2 Установка секций мира

Синтаксис:

wo[<object | position | table | group | polist>] = tile_declarations

Подробности:

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

Примеры синтаксиса:
 
wo[no["myobjectname"]] = {"st_chess"}
wo[po(3, 4)] = ti["x"]
wo[{2, 5}] = ti["x"] .. ti["y"]
wo[no["floorgroup#*"]] = {"it_burnable_oil"}
wo[no["myobjectname"] + NEIGHBORS_4] = ti["x"]

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.13.3 Установка глобальных атрибутов

Синтаксис:

wo["attritbutename"] = value

Подробности:

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

Примеры синтаксиса:
 
wo["ConserveLevel"] = true

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.13.4 Чтение глобальных атрибутов

Синтаксис:

var = wo["attritbutename"]

Подробности:

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

Примеры синтаксиса:
 
var = wo["IsDifficult"]

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.13.5 add

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

Синтаксис:

wo:add(tile_declarations)

wo:add(target, tile_declarations)

tile_declarations

Одно или несколько описаний объектов, представленных в виде секций или анонимных списков.

target

YIN’, ‘YANG’ или допустимая Ссылка на объект

Примеры синтаксиса:
 
wo:add({"ot_rubberband", anchor1="a1", anchor2="w", length=2, strength=80, threshold=0})
wo:add(ti["r"] .. {"ot_wire", anchor1="w1", anchor2="w2"})
wo:add(YIN, {"it_magicwand"})
wo:add(no["mybag"], {"it_magicwand"} .. ti["h"] .. ti["c"])
Подробности:

Напрямую к миру могут быть добавлены только Остальные объекты. В инвентарь игроков ‘YIN’ и ‘YANG’ и в сумки (it_bag) могут быть добавлены только портативные Предметы. Никакая другая цель, кроме перечисленных выше, на данный момент не добавляет объекты этим методом.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.13.6 drawBorder

Рисует границу из заданных секций вокруг заданного прямоугольника.

Синтаксис:

wo:drawBorder(upperleft_edge, lowerright_edge, <tile | key, resolver>)

wo:drawBorder(upperleft_edge, width, height, <tile | key, resolver>)

upperleft_edge

Координаты верхнего левого угла прямоугольника.

lowerright_edge

Координаты нижнего правого угла прямоугольника.

width

Ширина прямоугольника.

height

Высота прямоугольника.

tile

Секция или описание объекта.

key

Кодовая строка, которая должна быть преобразована указанным преобразованием.

resolver

Преобразование, которое должно использоваться для преобразования кода в подходящую секцию.

Примеры синтаксиса:
 
wo:drawBorder(po(0, 0), wo["Width"], wo["Height"], ti["#"])
wo:drawBorder(no["myRectUL"], no["myRectLR"], {"st_grate1"})
wo:drawBorder(no["myRectUL"], no["myRectLR"], {"fl_water"} .. ti["X"])
wo:drawBorder(no["myRectUL"], no["myRectLR"], "x", myresolver)
Подробности:

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.13.7 drawMap

Даже если мир инициализирован картой при создании мира (см. раздел Создание мира), иногда удобно иметь возможность рисовать карты внутри существующей либо во время инициализации, либо внося динамические изменения уровня с помощью функции обратного вызова (см. раздел Функция обратного вызова). Безусловно, основное назначение ‘drawMap’ — рисование повторяющихся шаблонов.

Синтаксис:

wo:drawMap(resolver, anchor, ignore, map, [readdir])

wo:drawMap(resolver, anchor, libmap-map, [readdir])

subresolver

Преобразование, которому должны быть перенаправлены необработанные запросы. В качестве последнего преобразования цепи преобразований может выступать ‘ti’.

anchor

Позиция точки привязки, где должна располагаться верхняя левая секция карты.

ignore

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

map

Список строк. Каждая строка описывает ряд секций, используя их коды.

libmap-map

Если используемая карта была создана с помощью libmap, строку ‘ignore’ можно опустить. Тогда вместо неё будет игнорироваться код по умолчанию.

readdir

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

Примеры синтаксиса:
 
wo:drawMap(ti, po(5, 7), "-", {"abcabc"})
wo:drawMap(ti, anchor_object, "--", {"--##--##","##--##"})
wo:drawMap(ti, {12, 5}, " ", {"122  221"}, MAP_ROT_CW)
Подробности:

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

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

Длина игнорируемого кода задаёт длину кода карты. Настоятельно рекомендуется использовать ту же длину кода, что и на карте мира.

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

После создания мира drawMap можно использовать где угодно. Допускается даже её использование в преобразовании при создании мира.

Готовый пример:
 
01    ti[" "] = {"fl_plank"}
02    ti["X"] = {"st_oxyd"}
03    ti["B"] = {"st_passage_black", flavor="frame"}
04    ti["W"] = {"st_passage_white", flavor="frame"}
05    ti["y"] = {"it_yinyang"}
06    ti["1"] = {"#ac_marble_black"}
07    ti["2"] = {"#ac_marble_white"}
08
09    function myresolver(key, x, y)
10        if key == "w" then
11            wo:drawMap(ti, po(x-1, y-1), "-", {"-W-",
12                                               "WXW",
13                                               "-W-"})
14            return ti({})
15        elseif key == "b" then
16            wo:drawMap(ti, po(x-1, y-1), "-", {"-B",
17                                               "BXB",
18                                               "-B"})
19            return ti({})
20        else
21            return ti[key]
22        end
23    end
24
25    w, h = wo(myresolver, " ", {
26      "                    ",
27      "  b         b       ",
28      "       w       w    ",
29      "                    ",
30      "                    ",
31      "   w                ",
32      "         12      b  ",
33      "              w     ",
34      "         w          ",
35      "      b             ",
36      "   w           b    ",
37      "         b          ",
38      "                    "
39    })
40    wo:shuffleOxyd()

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.13.8 drawRect

Заполняет прямоугольник указанной секцией.

Синтаксис:

wo:drawRect(upperleft_edge, lowerright_edge, <tile | key, resolver>)

wo:drawRect(upperleft_edge, width, height, <tile | key, resolver>)

upperleft_edge

Координаты верхнего левого угла прямоугольника.

lowerright_edge

Координаты нижнего правого угла прямоугольника.

width

Ширина прямоугольника.

height

Высота прямоугольника.

tile

Секция или описание объекта.

key

Кодовая строка, которая должна быть преобразована указанным преобразованием.

resolver

Преобразование, которое должно использоваться для преобразования кода в подходящую секцию.

Примеры синтаксиса:
 
wo:drawRect(po(0, 0), wo["Width"], wo["Height"], ti[" "])
wo:drawRect(no["myRectUL"], no["myRectLR"], {"fl_water"})
wo:drawRect(no["myRectUL"], no["myRectLR"], {"fl_water"} .. ti["#"])
wo:drawRect(no["myRectUL"], no["myRectLR"], "x", myresolver)
Подробности:

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.13.9 world floor

Возвращает покрытия в указанной позиции или позициях.

Синтаксис:

result = wo:fl(<pos| {x, y}|x, y| obj | group| polist>)

Подробности:

Этот метод мира идентичен глобальной функции fl.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.13.10 world item

Возвращает предметы в указанной позиции или позициях.

Синтаксис:

result = wo:it(<pos| {x, y}|x, y| obj | group| polist>)

Подробности:

Этот метод мира идентичен глобальной функции it.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.13.11 shuffleOxyd

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

Многие уровни просто вызывают этот метод, не передавая ему аргументов. Это приводит к перераспределению цветов всех оксидов (st_oxyd), которые не исключены атрибутом ‘noshuffle’.

Но иногда уровням нужно контролировать перераспределение, то ли для того, чтобы гарантировать решаемость уровня, то ли чтобы просто убедиться в справедливом распределении оксидов. Представьте уровень, в котором в каждом углу находятся по два камня st_oxyd. Если игроку повезёт и он добьётся распределения, при котором в каждом углу будет по два оксида одинакового цвета, решение уровня становится банальным. В другом уровне может быть проход, через который шарик может пройти лишь несколько раз. При 5 и более оксидах с каждой стороны прохода вам нужно быть уверенным в том, что шарику не потребуется пересекать проход больше раз, чем возможно. Обе ситуации можно проконтролировать, используя в качестве аргументов к этому методу подходящие правила.

Синтаксис:

wo:shuffleOxyd(rules)

rules = rule, rule,...

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

rule = {group1, group2, maxrule, minrule, circularrule, linearrule, log}

Каждое правило — список с подмножеством перечисленных элементов. Элемент group1 обязателен. Все остальные необязательны и могут добавляться в любых сочетаниях.

group1 = group | objectreference | objectspecifier

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

group2 = group | objectreference | objectspecifier

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

maxrule = max = number

Максимальное количество пар оксидов.

minrule = min = number

Минимальное количество пар оксидов.

circularrule = circular = true

Избегать появление любых пар смежных оксидов из ‘group1’. Также избегать появления пары, состоящей из первого и последнего оксидов из ‘group1’.

linearrule = linear = true

Избегать появление любых пар смежных оксидов из ‘group1’.

log = log =    "solution" |"count" |"all"

В целях отладки и проверки безопасности автором уровня записывать в поток журналирования дополнительную информацию.

Примеры синтаксиса:
 
wo:shuffleOxyd()
wo:shuffleOxyd({no["borderoxyds#*"]:sort("circular"), circular=true})
wo:shuffleOxyd({"leftoxyds#*","rightoxyds#*", min=3}, {"islandoxyds#*", max=0})
Подробности:

Любой вызов ‘wo:shuffleOxyd()’ должен происходить после того, как все оксиды (st_oxyd) будут расположены на карте. Это значит, что он должен следовать за стандартной процедурой инициализации мира (см. раздел Создание мира). Побочным эффектом ‘shuffleOxyd’ является присвоение цветов всем st_oxyd, цвет которых определяется атрибутом ‘OXYD_AUTO’.

После первого вызова заданных правил перераспределения ими можно будет воспользоваться в любой момент. Любое последующее перераспределение нужно вызывать сообщениями ‘closeall’ и ‘shuffle’ для одного из экземпляров st_oxyd. Без уничтожения и удаления заданных правил не возможен никакой дополнительный вызов st_oxyd или последующего ‘wo:shuffleOxyd()’.

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

Существует два основных типа правил. Те, которые работают с одной группой, и те, которые работают с двумя группами оксидов (заметим, что группа — это определение из общего API для набора оксидов, а не математическая группа). При одной группе правила применяются к экземплярам оксидов в этой группе. При двух группах правила применяются к парам оксидов, которые состоят из камней, принадлежащих к разным группам.

Например, ‘{"islandoxyds#*", max=0}’ значит, что в этой группе оксидов нет ни одной пары. Тогда как ‘{"leftoxyds#*","rightoxyds#*", min=3}’ значит, что существует 3 различных пары оксидов, каждая с одним оксидом в группе ‘leftoxyds’ и вторым в группе ‘rightoxyds’.

Линейные и круговые правила можно применять только к одной группе. Они представляют собой ссылки на более общие правила, которые применяются к оксидам, расположенным в линии или по кругу. В обоих случаях они не допускают появления рядом пары одинаковых оксидов. Они эквивалентны ‘n-1’ перезапускам, ‘n’ правилам со всевозможными соседними парами оксидов двух групп и правилу о ‘max=0’.

Заметьте, что к заданным группам можно применить сразу несколько правил. Например, применить одним правилом и ‘minrule’, и ‘maxrule’!

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

При 16 оксидах 8 различных цветов и без ограничивающих правил выходит 2027025 (15 * 13 * 11 * 9 * 7 * 5 * 3) различных допустимых распределений. Имейте в виду, что у этих полезных правил для каждого уровня всегда должны быть сотни или тысячи различных допустимых распределений.

Для удобства отладки существует возможность добавить к одному из правил параметр ведения журнала (не имеет значения к какому). По запросу в поток журналирования будет выведен журнал ‘solution’ (определения) распределения пар.

Во время ‘count’ (подсчёта) будет подсчитано и занесено в журнал число различных распределений оксидов. При использовании сложных правил рекомендуется проверять количество распределений, чтобы убедиться, что осталось достаточно распределений для сохранения вариативности игры. Но будьте осторожны с попытками подсчитать количество распределений по простым правилам. При 16 оксидах может существовать 2027025 распределений, на определение которых у обычного ПК уйдет до 30 секунд — для того, чтобы узнать количество распределений для 20 оксидов, домножьте это число ещё на 17*19!

Будьте осторожны при журналировании ‘all’ (всех событий). Этот вызов пытается вывести все решения. При достаточно большом числе распределений их подсчёт может длиться веками. Перед тем, как заносить в журнал события, подсчитайте их.

Готовый пример:
 
01    wo["ConserveLevel"] = false
02
03    ti["~"] = {"fl_water"}
04    ti[" "] = {"fl_plank"}
05    ti["c"] = {"it_crack_l", brittleness=0}
06    ti["^"] = {"st_oneway_n"}
07    ti["1"] = {"ac_marble_black", 0, 0.5}
08
09    ti["x"] = {"st_oxyd", "island#"}
10    ti["y"] = {"st_oxyd", "left#"}
11    ti["z"] = {"st_oxyd", "right#"}
12
13    w, h = wo(ti, " ", {
14      "~~x  x  x  x  x  x~~",
15      "~~                ~~",
16      "~~~~^~~~~~~~~~~^~~~~",
17      "y       ~~~~       z",
18      "~       cccc       ~",
19      "y       ~~~~       z",
20      "~       cccc       ~",
21      "y       ~~~~       z",
22      "~       cccc       ~",
23      "y       ~~~~       z",
24      "~~~~c~~~~~~~~~~c~~~~",
25      "~~                ~~",
26      "~~        1       ~~"
27    })
28
29    wo:shuffleOxyd({"island#*", min=3, linear=true}, {"left#*","right#*", max=2, min=2})

В этом уровне 14 оксидов. 6 оксидов в верхнем ряду находятся на острове, который уже нельзя покинуть после того, как шарик ступит на него из одного из односторонних проходов. Поэтому, на этом острове нам нужны 3 пары оксидов. Их можно получить, применив правило минимума. Также, чтобы избежать появления на острове соседних пар оксидов, мы добавляем линейное правило. Между левым и правым островами шарик может пройти только трижды. Это позволяет при первом проходе сначала узнать цвета оксидов, а за два следующих прохода открыть по паре оксидов. Поэтому правилом максимума мы ограничиваем число пар до 2. Во избежание ситуации, когда с левой и правой стороны появится по две пары оксидов, мы добавляем правило минимума, которое заставляет появляться две разнесённые в пространстве пары оксидов.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.13.12 world stone

Возвращает камни с указанной позиции или позиций.

Синтаксис:

result = wo:st(<pos| {x, y}|x, y| obj | group| polist>)

Подробности:

Этот метод мира идентичен глобальной функции st.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.14 Функции

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.14.1 assert_bool

Если условие не выполняется, то функция ‘assert_bool’ выдаёт ошибку.

Синтаксис:

assert_bool(condition, message, level)

condition

Булево (или приводимое к нему) выражение. Если его значение false или nil, то выдаётся ошибка.

message

Строка с сообщением об ошибке. Если message пусто или представляет собой nil, то выдаётся "анонимное утверждение", но зачастую лучше предоставлять осмысленное сообщение об ошибке.

level

level указывает место обнаружения ошибки, так же как и функция Lua ‘error’. По умолчанию: 1.

Примеры синтаксиса:
 
assert_bool(no["mystone"]:exists(), "Stone 'mystone' has disappeared.")
Подробности:

Утверждения помогают определить ошибки кодирования. Наиболее часто они используются для проверки параметров библиотечных функций и реализаций преобразований. Чтобы утверждения не приводили к падению производительности во время игры, они обычно проверяются для уровней, у которых для параметра ‘status’ в элементе <version> XML-заголовка выставлены значения "test" или "experimental".

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

 
dummy = (assert_bool)(no["mystone"]:exists(), "Stone 'mystone' has disappeared.")

Как и для cond, для ‘message’ и ‘level’ справедливо появление побочных эффектов.

Более подробную информацию о функции ‘error’ можно найти в руководстве Lua.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.14.2 assert_type

Если тип первого параметра не совпадает ни с одним из указанных, то функция ‘assert_type’ выдаёт ошибку.

Синтаксис:

assert_type(var, vardescription, level, type1, type2, ...)

var

Переменная любого типа.

vardescription

Если тип ‘var’ не совпадает с типами ‘type1’, ‘type2’ ..., то выдаётся сообщение об ошибке, которое включает фактический тип переменной ‘var’ и желаемые типы. ‘vardescription’ — это строка с информацией, дополняющей сообщение об ошибке. Это должно быть довольно подробное описание переменной ‘var’ (фактически полное название), состоящее из символов в нижнем регистре, дополнительная информация приводится в скобках.

level

level указывает место обнаружения ошибки, так же как и функция Lua ‘error’. Указывается в обязательном порядке, если сомневаетесь, то выбирайте ‘1’.

type1, type2, ...

Последовательность строк. Если тип ‘var’ не совпадает с этими типами, то выдаётся сообщение об ошибке. Описание дескрипторов типа приведено в Подробностях ниже.

Примеры синтаксиса:
 
assert_type(arg1, "mygreatfunction first argument (level width)", 1, "nil", "positive integer", "position")
Подробности:

Утверждения помогают определить ошибки кодирования. Наиболее часто они используются для проверки параметров библиотечных функций и реализаций преобразований. Чтобы утверждения не приводили к падению производительности во время игры, они обычно проверяются для уровней, у которых для параметра ‘status’ в элементе <version> XML-заголовка выставлены значения "test" или "experimental".

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

 
dummy = (assert_type)(arg1, "myfunction first argument", 1, "integer")

Допустимы все типы данных Lua (например "nil", "number", "boolean", "string", "table", "function") кроме "userdata", пользовательские типы данных Enigma ("object", "position", "tile", "tiles", "group", "world", "polist", "unknown") и типы данных, указанные в метасписках ("map" из libmap), см. раздел etype. Кроме того, признаются следующие дескрипторы типов:

"integer"

Любое целое число (..., -2, -1, 0, 1, 2, ...)

"positive"

Любое положительное число, но не ноль.

"non-negative"

Любое неотрицательное число, включая ноль.

"natural"

Любое неотрицательное целое число (0, 1, 2, ...).

"positive integer"

Любое положительное целое число (1, 2, 3, ...).

"non-empty string"

Любая строка кроме пустой "".

"any table"

Если ‘var’ — список, то атрибут ‘_type’ соответствующего метасписка будет использован в качестве его etype. В частности, если атрибут ‘_type’ существует, то список уже не может рассматриваться как экземпляр типа данных "table". Например,

 
assert_type(mytable, "large table", 1, "table")

если ‘mytable’ это "map", то приведённый код выдаст утверждение, хотя, в принципе, "map" всегда является "table". Чтобы распознавались любые типы списков, вне зависимости от их внутреннего представления, можно использовать в качестве типа "any table".

"valid object"

Любой допустимый объект.

Как и для cond, для ‘vardescription’, ‘level’ и любых дескрипторов типов справедливо появление побочных эффектов.

Более подробную информацию о функции ‘error’ можно найти в руководстве Lua.

Готовый пример:
 
function paint_lawn(pos)
    assert_type(pos, "paint_lawn first argument", 2, "position", "object", "polist", "group", "table")
    if etype(pos) == "object" then
        assert_bool(-pos, "paint_lawn: Object not existing.", 2)
    end
    wo[pos] = ti["lawn"]
end
paint_lawn(no["mystone"])
paint_lawn("myotherstone")

Если ‘mystone’ не существует, no["mystone"] всё ещё будет объектом типа etype, недопустимым объектом. Поэтому assert_type больше срабатывать не будет, а assert_bool будет.

Если ‘mystone’ существует, то второй ‘paint_lawn’ выдаст ошибку через утверждение ‘assert_type’, поскольку pos теперь имеет тип "string". Сообщение об ошибке:

 
Wrong type for paint_lawn first argument, is string, must be one of position,
object, polist, group, table.

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.14.3 cond

cond’ — это условное выражение, замена тернарного оператора ‘?:’ из C-подобных языков. Однако стоит заметить, что это не точная замена, а просто обходной манёвр с некоторыми побочными эффектами замены.

Синтаксис:

cond(condition, iftrue, iffalse)

condition

Булево выражение.

iftrue

Выражение, которое выдаётся, если ‘condition’ истинно.

iffalse

Выражение, которое выдаётся, если ‘condition’ ложно.

Примеры синтаксиса:
 
ti["x"] = cond(wo["IsDifficult"], {"st_death"}, ti["#"])
ti["D"] = cond(wo["IsDifficult"], {"st_death"}, {"nil"})
Подробности:

cond’ всегда выполняет оба выражения — ‘iftrue’ и ‘iffalse’, независимо от ‘condition’. Поэтому,

 
mytable = {1,2,3,4,5,6,7,8,9,0}
removed_element = cond(i < 5, table.remove(mytable, i), table.remove(mytable, 5))

всегда будет удалять два элемента. При ‘i=2’ будет возвращаться ‘2’, а ‘mytable’ превратится в ‘{1,3,4,5,7,8,9,0}’, а при ‘i=6’ будет получена ‘5’, но ‘mytable’ будет ‘{1,2,3,4,7,8,9,0}’.

Другой пример Enigma, который может вызвать ошибки:

 
w,h = cond(wo["IsDifficult"], wo(ti, " ", map1), wo(ti, " ", map2))

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

 
w,h = wo(ti, " ", cond(wo["IsDifficult"], map1, map2))

Однако в большинстве случаев ‘cond’ всё равно используется с постоянными выражениями в качестве параметров ‘iftrue’ и ‘iffalse’ (например строками или переменными) и никаких побочных эффектов не возникнет.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.14.4 etype

Функция ‘etype()’ возвращает дополнительный тип своего параметра.

Синтаксис:

etype(var)

var

Переменная любого типа.

Примеры синтаксиса:
 
argtype = etype(value)
Подробности:

Типами Lua являются "nil", "number", "boolean", "string", "table", "function", "userdata" и "thread". Чтобы запросить тип любой переменной, можно воспользоваться функцией Lua ‘type’. Однако Enigma различными способами определяет целый ряд дополнительных типов и такие типы можно запросить с помощью ‘etype’. ‘etype’, как правило, возвращает тип Lua своего параметра, кроме двух случаев, рассмотренных ниже:

"userdata"

Вместо "userdata" будут возвращены спеыиальные типы Enigma. Такими особыми типами являются "object", "position", "tile", "tiles", "group", "world", "polist" и "default". Если встретится неизвестный пользовательский тип данных, то будет возвращено значение "unknown".

"table"

Если var — это список, то будет возвращён его метасписок. Если присутствует элемент ‘_type’, то он будет использован в качестве etype. Самыми характерными примерами являются карты libmap, Преобразования и res.maze с лабиринтами и клетками. Поэтому ‘etype’ будет возвращать ещё и типы "map", "resolver", "maze" и "cell". К системе etype можно обратиться через ‘_type’ из любого места, где используются метасписки.


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.14.5 fl

Функция ‘fl()’ получает объекты покрытия с указанной позиции или позиций.

Синтаксис:

result = fl(<pos| {x, y}|x, y| obj | group| polist>)

Подробности:

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

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

В любом случае полученному значению можно посылать сообщения.

Примеры синтаксиса:
 
floor = fl(po(3, 5))
floor = fl({3, 5})
floor = fl(3, 5)
floor = fl{3, 5}     — эквивалентная запись Lua
floor = fl(mystone)
group = fl(no["door#*"])
group = fl(po(3, 5)..po(4, 2))

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.14.6 grp

Функция ‘grp()’ составляет из своих Объектов-параметров группу.

Синтаксис:

grp(<{obj1,obj2,...}| obj1,obj2,... |group>)

Подробности:

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

Примеры синтаксиса:
 
newgroup = grp(obj1, obj2, obj3)
newgroup = grp({obj1,obj2})
newgroup = grp{obj1,obj2}   -- Lua syntax equivalence
newgroup = grp{}            -- empty group
newgroup = grp(group)       -- a copy of group cleaned of invalid ‘NULL’ objects

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.14.7 it

Функция ‘it()’ получает предметы с указанной позиции или позиций.

Синтаксис:

result = it(<pos| {x, y}|x, y| obj | group| polist>)

Подробности:

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

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

В любом случае полученному значению можно посылать сообщения.

Примеры синтаксиса:
 
item = it(po(3, 5))
item = it({3, 5})
item = it(3, 5)
item = it{3, 5}     — эквивалентная запись Lua
item = it(mystone)
group = it(no["door#*"])
group = it(po(3, 5)..po(4, 2))

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.14.8 ORI2DIR

Список ‘ORI2DIR[]’ преобразует значения положения в значения направления.

Синтаксис:

result = ORI2DIR[orientation]

Подробности:

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

Примеры синтаксиса:
 
direction = ORI2DIR[NORTH]      -- N  = po(0, -1)
direction = ORI2DIR[SOUTHEAST]  -- SE = po(1,  1)
direction = ORI2DIR[NODIR]      --      po(0,  0)

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.14.9 random

Функция ‘random()’ — замена стандартной фцнкции Lua ‘math.random()’, синтаксически совместимая с ней. Оба имени соответствуют одной и той же реализации получения случайного значения в Enigma.

Синтаксис:

result = random(<|n|l,u>)

Подробности:

При вызове без параметров math.random возвращает псевдослучайное действительное число из диапазона [0,1). Если параметром выступает число n, то math.random возвращает псевдослучайное целое число из диапазона [1,n]. При вызове с двумя заданными параметрами, l и u, math.random возвращает псевдослучайное целое число из диапазона [l,u].

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

Примеры синтаксиса:
 
float = random()          -- e.g. 0.402834
integer = random(20)      -- e.g. 13
integer = random(5, 10)   -- e.g. 5

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.14.10 st

Функция ‘st()’ получает объекты камней с указанной позиции или позиций.

Синтаксис:

result = st(<pos| {x, y}|x, y| obj | group| polist>)

Подробности:

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

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

В любом случае полученному значению можно посылать сообщения.

Примеры синтаксиса:
 
stone = st(po(3, 5))
stone = st({3, 5})
stone = st(3, 5)
stone = st{3, 5}     — эквивалентная запись Lua
stone = st(myfloor)
group = st(no["cherry#*"])
group = st(po(3, 5)..po(4, 2))

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

5.14.11 usertype

Функия ‘usertype()’ возвращает для типов данных Enigma информацию о типе.

Синтаксис:

usertype(var)

var

Переменная любого типа.

Примеры синтаксиса:
 
argtype = usertype(value)
Подробности:

Особая информация о типе возвращается только для используемого в Enigma типа данных Lua "userdata". Такими особыми типами являются "object", "position", "tile", "tiles", "group", "world", "polist" и "default". Для других типов данных будет возвращено значение "unknown".

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

6. Общие атрибуты и сообщения

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

6.1 Общие атрибуты


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

6.1.1 name

Атрибут именования объектов (см. раздел Именование объектов), который позволяет присвоить имя любому объекту. Чтобы ссылаться на него, следует проверять уникальность имён. Но движок помогает вам, автоматически добавляя уникальный номер к именам, оканчивающимся на знак ‘#’ (см. раздел Именование объектов). Если вы повторно используете уже использовавшееся имя, первый объект будет разименован и все ссылки на имя будут указывать на новый именованный объект. Если вам нужно присвоить объекту имя, вы должны сделать это при создании объекта, так как некоторым объектам имена необходимы и в случае их отсутствия уникальные имена им присвоит движок.

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

Тип:   string
Значения:   {a-zA-Z0-9_}+

Последовательность символов, состоящая из букв и специальных символов, указанных выше.

По умолчанию:   nil

Если имя не задано, то некоторым объектам имена будут присвоены автоматически.

Доступ:   read/write
Поддерживается:   всеми объектами

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

6.1.2 state

Ключевой атрибут любого объекта, описывающий текущее состояние объекта в его стандартном жизненном цикле. Это Состояние объекта описывается простым числом. Большинство динамических объектов могут находиться только в двух состояниях. Для других объектов характерно большее количество состояний. Возможные состояния перечисляются в описании каждого объекта. Этот универсальный атрибут допускает общие сообщения, такие как toggle, signal, on, off, open, close.

Тип:   number
Значения:   зависят от конкретного объекта

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

По умолчанию:   0
Доступ:   read/иногда write

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

Поддерживается:   всеми объектами

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

6.1.3 target

Все активные объекты при срабатывании выполняют некое действие по отношению к своей цели. Данный атрибут является частью парадигмы Цель-действие, которая гарантирует возможность связывать объекты. Вы можете задать общий атрибут ‘target’ для объекта либо зависимые от состояния (атрибут state) атрибуты ‘target_0’, ‘target_1’ и т. д. (см. раздел Состояние объекта). Для всех них характерен одинаковый синтаксис:

Тип:   строка, объект, группа, метки  См. раздел Описание объектов

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

Значения:   см. раздел Атрибуты объектов
 
target = "myDoor"
target = myObject
target = {"myDoor", myObject}
target = {grp(obj1, obj2), "myDoor", myObject}
По умолчанию:   nil
Доступ:   read/write
Поддерживается:   всеми объектами

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

6.1.4 action

Все активные объекты при срабатывании выполняют некое действие по отношению к своей цели. Данный атрибут является частью парадигмы Цель-действие, которая гарантирует возможность связывать объекты. Вы можете задать общий атрибут ‘action’ для объекта либо зависимые от состояния (атрибут state) атрибуты ‘action_0’, ‘action_1’ и т. д. (см. раздел Состояние объекта). Для всех них характерен одинаковый синтаксис:

Тип:   строки, метки строк   См. раздел Цель-действие

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

Значения:   см. раздел Атрибуты объектов
 
action = "open"
action = {"open", "turn", "toggle"}
По умолчанию:   nil
Доступ:   read/write
Поддерживается:   всеми объектами

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

6.1.5 nopaction

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

Тип:   bool
Значения:   true, false
По умолчанию:   false
Доступ:   read/write
Поддерживается:   всеми объектами

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

6.1.6 safeaction

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

Тип:   bool
Значения:   true, false
По умолчанию:   false
Доступ:   read/write
Поддерживается:   всеми объектами

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

6.1.7 inverse

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

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

Тип:   bool
Значения:   true, false
По умолчанию:   false
Доступ:   read/write
Поддерживается:   большинством объектов

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


[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

6.1.8 destination

Атрибут, который описывает один или несколько пунктов назначения. Он используется такими объектами, как it_vortex и it_wormhole, для описания точек, в которые они должны телепортировать объекты, а также ac_horse для описания пути перемещения объекта.

Следует отметить, что этот атрибут поддерживается, только если он присутствует в описании конкретного объекта.

Тип:   метки или отдельная позиция

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

Значения:   см. раздел Атрибуты объектов
 
destination = po(3.0, 4.7)
destination = "myFloor"
destination = myObject
destination = {"vortex2","vortex3","vortex4"}
po["dest1"] = po(3,4)
po["dest2"] = po(7,8)
destination = {"dest1","dest2","myFloor"}

Обратите внимание, что объекты, имеющие только один пункт назначения, такие как ‘it_wormhole’, принимают первый объект-метку. Также обратите внимание на то, что в отличие от ссылок на цель (target), ссылки на пункт назначения могут принимать в качестве аргументов и именованные позиции. Также можно без опаски ссылаться на покрытия, которые могут быть разрушены бомбами, трещинами, камнями, изменяющими покрытие, и т.п.

По умолчанию:   nil
Доступ:   read/write
Поддерживается:   объектами-телепортами

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

6.1.9 friction

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

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

Тип:   number
Значения:   любое число с плавающей запятой
По умолчанию:   nil
Доступ:   read/write
Поддерживается:   всеми покрытиями, а также предметами, по функции схожими с покрытиями

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

6.1.10 adhesion

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

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

Тип:   number
Значения:   любое число с плавающей запятой
По умолчанию:   nil
Доступ:   read/write
Поддерживается:   всеми покрытиями, а также предметами, по функции схожими с покрытиями

[ < ] [ > ]   [ << ] [ Вверх ] [ >> ]         [В начало] [Содержание] [Предметный указатель] [ ? ]

6.1.11 checkerboard

Атрибут, который определяет, должно ли данное объявление объекта применяться на чётных или нечётных участках решётки. Если значение атрибута равно ‘0’, объект будет размещаться только на участках решётки с чётной суммой координат x и y, если же значение равно ‘1’, то он будет размещаться на участках решётки с нечётной суммой координат. Таким образом можно сделать два различных объявления объекта для одного участка решётки, чтобы создать карту произвольной формы, состоящую из покрытий, предметов или камней, расположенн