[В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Данное руководство описывает внутреннюю структуру игры Enigma версии 1.20, в частности процесс создания уровней с использованием языка программирования Lua и особенности взаимодействия с движком игры.
1. Использование Enigma | ||
2. Основы работы с пакетами уровней | Добавление уровней в пакеты и управление уровнями | |
3. Основы работы с уровнями | Введение в описание уровней | |
4. Ключевые понятия Enigma | Принципы структуры мира, объектов и сообщений | |
5. Lua API | Описание уровня со всеми его объектами и взаимодействиями | |
6. Общие атрибуты и сообщения | Те, которые поддерживаются всеми объектами | |
7. Покрытия | ||
8. Предметы | ||
9. Камни | ||
10. Актёры | ||
11. Остальные объекты | Проволоки, резиновые ленты и гаджеты | |
12. Библиотеки | Вспомогательные функции и возможности | |
13. Дополнительные возможности | Преобразования, прокрутка, наводнение, пожар и т.п. | |
14. Разработка расширений | Разработка преобразований, библиотек | |
15. Советы и рекомендации | Рекомендации и подсказки для авторов уровней | |
16. Совместимость | Совместимость режимов и версий движка | |
Указатель объектов | ||
Указатель атрибутов | ||
Указатель сообщений | ||
Указатель функций | ||
Указатель терминов | ||
Список изменений | Список отличий между старым и новым API |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
После того, как вы установили Enigma и прошли несколько уровней, вам, надеемся, будет интересно узнать, как Enigma устроена, как настроить её в соответствии со своими предпочтениями и для чего служат некоторые опции и атрибуты игры.
Эта глава должна помочь ответить на эти вопросы, а также предоставить основные сведения, которые могут понадобиться для того, чтобы управлять пакетами уровней, отдельными уровнями и создавать собственные уровни (подробно обо всём этом в следующих главах).
1.1 Размещение ресурсов | ||
1.2 Опции запуска | ||
1.3 Пользовательские опции | ||
1.4 Консольная функция инвентаря | Ввод команд и история документов | |
1.5 Информация об уровне | ||
1.6 Гандикап и пар (PAR) | Принципы подсчёта очков и гандикапа | |
1.7 Пользовательские наборы звуков | Создание и установка звукового сопровождения |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Для создания резервных копий, внесения изменений в систему, особых конфигураций, а также для добавления новых уровней (в том числе, надеемся, и своих собственных) вам нужно знать, где хранятся ресурсы 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-файла.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Помимо запуска игры с помощью щелчка по ярлыку на рабочем столе или меню "Пуск", Enigma можно запустить из оболочки или командной строки. Это позволяет добавить при запуске игры набор опций, который будет действовать только для данного запуска.
Для многократного использования одинакового набора опций запуска можно создать ярлык на рабочем столе или запись в меню "Пуск" и добавить опции запуска к целевой строке исполняемого файла Enigma.
Ниже следует список поддерживаемых программой пользовательских опций. Если в списке указано как полное имя опции, следующее за двумя знаками "минус", так и однобуквенное сокращение, следующее за одним знаком "минус", используйте один из вариантов (а не два одновременно), например: ‘--data путь’ или ‘-d путь’.
Опция, предназначенная для разработчиков игры, которая принудительно выполняет все отладочные проверки, даже ресурсоёмкие. Результаты дополнительной проверки выглядят примерно так: ‘ASSERT(noAssert || long_lasting_check(), XLevelRuntime, "remark");’.
Опция, предназначенная для разработчиков игры; она позволяет добавить к списку путей ресурсов дополнительный путь, который будет располагаться перед путём к системной папке (см. раздел Опции запуска). Разработчик может проверить сборку Enigma, не устанавливая её, посредством её вызова из оболочки с текущей рабочей директорией в качестве главной директории с помощью опции ‘src/Enigma -d ./data’.
Просто выводит на экран все опции запуска и завершает свою работу.
Опция, позволяющая выбрать язык игры. Язык задаётся стандартным двухбуквенным обозначением, например ‘fr’ для французского или ‘ru’ для русского.
Эта опция добавляет к стандартному выводу внутреннюю информацию. Пользователи Windows могут найти её в файле ‘Output.log’ в стандартной ‘Папке пользователя’. В ещё одном файле ‘Error.log’ перечислены критические ошибки.
В выводимую информацию будут включены, к примеру, пути, описанные в разделе Размещение ресурсов.
Опция для разработчиков Enigma; она отключает в игре управление мышью. В этом режиме вы вряд ли сможете пройти хотя бы один уровень, но это даёт возможность исправлять ошибки в ядре приложения.
Запускает игру с выключенным музыкальным сопровождением.
Запускает игру с выключенным звуковым сопровождением.
Имя альтернативного файла настроек (без точки для имён скрытых файлов в начале). Как поясняет раздел Размещение ресурсов, эта опция предназначена исключительно для разработчиков игры.
Путь к альтернативной директории, в которой содержится файл настроек со стандартным именем ‘.enigmarc.xml’. Если файл настроек или папка не существуют, они автоматически создаются. При создании файла настроек здесь по умолчанию располагается пользовательская папка данных. Это позволяет размещать все пользовательские данные игры в отдельной директории, которая может располагаться где угодно, например на USB-накопителе. Чтобы использовать эту новую настройку, всегда нужно запускать Enigma с данной опцией. Помните, что путь, содержащий пробелы, нужно заключать в кавычки.
Перенаправляет ‘stdout’ и ‘stderr’ в файлы с названиями ‘Output.log’ и ‘Error.log’ в стандартной папке пользователя (см. раздел Размещение ресурсов). Для Windows данная опция всегда имеет значение true
, но она полезна на любых операционных системах, если Enigma запускается через ярлык на рабочем столе или кнопку меню.
Закрытие всех подключений к Интернету. При этом не будут выполняться никакие автоматические обновления и на все запросы пользователя, для выполнения которых требуется подключение к Интернету, будет выводиться сообщение об ошибке.
Позволяет отображать во время игры количество кадров в секунду (FPS).
Отображает номер версии и завершает свою работу.
Позволяет запустить 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.00 оставьте следующее значение для этой опции: ‘Не обновлять’.
Введите ник, под которым Enigma будет сохранять ваши баллы. Пожалуйста, взгляните на список уже используемых ников на домашней странице Enigma и выберите ещё не используемый. Свой ник можно изменить в любой момент, не рискуя потерять свои данные.
Как поясняет раздел Размещение ресурсов, здесь можно указать произвольную директорию для хранения ваших пользовательских данных.
Если очистить данную строку, то данные будут размещены в директории по умолчанию.
Enigma активирует новый путь к папке эскизов после выхода из меню настроек. Хотя она сохраняет все файлы в новой папке и ещё может находить файлы в старой, мы советуем сразу выйти из игры и перенести данные из старой папки в новую. Это необходимо, так как при следующем запуске Enigma будет работать с данными только в новой директории.
Как поясняет раздел Размещение ресурсов, здесь можно ввести новую директорию для пользовательских изображений.
Если очистить данную строку, то данные будут размещены в директории по умолчанию.
Enigma активирует новый путь к папке эскизов после выхода из меню настроек. Хотя она сохраняет все файлы в новой папке и ещё может находить файлы в старой, мы советуем сразу выйти из игры и перенести данные из старой папки в новую. Это необходимо, так как при следующем запуске Enigma будет работать с данными только в новой директории.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Нижняя правая область экрана, где обычно отображаются предметы, содержащиеся в инвентаре, и прокручиваются тексты активированных документов, также позволяет пользователю выводить тексты ранее активированных документов, вводить тесктовые команды и повторно вводить предыдущие команды.
Команду можно ввести с клавиатуры. Просто введите команду и активируйте её, нажав клавишу <Enter>. Поддерживаются следующие команды:
Выводит список всех доступных пользователю команд.
Завершение уровня и возврат в меню уровней. Аналогично нажатию <Alt+X>
Просто шутка.
Выводит "читы", предназначенные для быстрого тестирования уровня его разработчиком.
Необходимый разработчикам "чит", отключающий столкновения между камнями и шариками или жемчужинами. При его использовании счёт в случае успешного завершения уровня сохранён не будет.
Перезапуск уровня в простом режиме.
Осуществляет во всех пакетах уровней поиск уровней, которые содержат указанную строку в заглавии, имени автора или имени файла.
Необходимый разработчикам "чит", который защищает актёров, присвоенных текущему игроку, подобно активированному объекту it_umbrella. При его использовании счёт в случае успешного завершения уровня сохранён не будет.
Переход в режим охоты за мировыми рекордами. Аналогично выбору иконки мирового рекорда на левой кнопке в меню уровней.
Выводит информацию об уровне, включая пакет уровней, расположение в пакете, путь к файлу, заглавие, автора, версию и внутренний идентификатор уровня.
Непосредственный запуск указанного уровня. Для пакета уровней указывается его название. Для уровня указывается его порядковый номер в пакете. Например jumpto Enigma IV,33
.
Выключает режим охоты за мировыми рекордами. Аналогично выбору иконки шарика на левой кнопке в меню уровней.
Перезапуск уровня в сложном режиме.
Перезапуск уровня в ранее выбранном режиме сложности.
Убивает актёров, но без завершения уровня, если это возможно. Аналогично нажатию клавиши F3>.
Игра сохраняет историю команд и отображаемых документов. Эту историю можно вывести с помощью стрелок вверх и вниз.
Стрелка вверх, изначально нажатая при отображении предметов в инвентаре, позволяет прокручивать ранее введённые команды. Повторно отправить команду можно нажатием клавиши <Enter>. История будет пересортирована, при этом последняя команда окажется непосредственно над инвентарём. Историю команд можно редактировать в любой момент, в том числе и добавлять туда новые команды. Если после ввода команды не была нажата клавиша <Enter>, строка всё же будет записана и выведена в качестве первой команды над инвентарём. История команд постоянно поддерживается в актуальном состоянии.
Историю документов можно вызвать с помощью стрелки вниз. Все документы, ранее отображённые на уровне, можно вывести повторно. Кроме того, можно снова вывести информацию, отображаемую при запуске уровня.
Если клавишу вверх или вниз нажать после вывода на экран, соответственно, самой ранней команды или сообщения, то будет осуществлён возврат к инвентарю.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Enigma хранит гораздо больше сведений о каждом уровне, чем можно отобразить в меню уровней. Эти сведения можно просмотреть с помощью инспектора уровней. Его можно вызвать, щёлкнув правой кнопкой мыши (или любой кнопкой мыши + Ctrl) по эскизу уровня в меню уровней.
Помимо названия и автора уровня Enigma приводит публичный рейтинг уровня, различные типы баллов, информацию о версии уровня, размещении файла уровня и др. Также в инспекторе уровней можно ввести своё примечание к уровню. Из инспектора уровней можно просмотреть и сделанные снимки экрана уровня.
1.5.1 Публичные рейтинги | ||
1.5.2 Баллы | ||
1.5.3 Версии | ||
1.5.4 Личные примечания и оценки | ||
1.5.5 Снимки экрана |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Большинство уровней игры оцениваются по пяти различным категориям:
Мы используем сокращения, чтобы подчеркнуть отличие наименований категорий рейтинга от повседневных значений этих слов. В каждой из этих категорий уровни оцениваются по пятибалльной шкале от 1 (лёгкий) до 5 (сложный), за исключением категории ‘знан’, где уровням может присваиваться 6 баллов (уникальный механизм).
Пожалуйста, имейте в виду, что строго придерживаться приведённых ниже критериев в почти 750 случаях чрезвычайно затруднительно, поэтому в отдельных уровнях могут быть (и будут) отклонения от них.
Интеллект (инт)
Этот рейтинг отражает творческие и аналитические способности, а также способность к планированию, необходимые для решения уровня. Интеллект сам по себе довольно сложное понятие, которое не так-то легко сразу понять или оценить. Следовательно, фиксированные определения каждого пункта оценочной шкалы не только желательны, но и необходимы при выставлении оценок. Допустим, вам известны все отдельные элементы уровня. Задайте себе следующие вопросы:
Высокие рейтинги обычно присваиваются головоломкам. Инт-рейтинги не накапливаются: рейтинг уровня определяет его самая сложная задача.
Ловкость (ловк)
Для решения многих уровней вам потребуются, главным образом, точные движения мышкой или терпение. В данном случае под ловкостью мы понимаем не "аккуратность в противоположность поспешным, нетерпеливым движениям", а точность, необходимую, чтобы избежать смерти. Таким образом, данный рейтинг учитывает все потенциальные смертельные опасности уровня, не только пропасти и смертоносные камни, но и, например, потенциальную возможность случайно толкнуть нужный камень в угол, достать из которого его невозможно.
В противоположность инт-рейтингам, данные рейтинги могут накапливаться: так, уровень, на котором имеется множество опасностей, оцениваемых в три балла, в результате может получить 4 или даже 5. Роторы в уровнях также ассоциируются с рейтингами ловкости ‘ловк’ и скорости (‘скор’). Поэтому уровни с комбинацией из ‘ловк-скор’ в основном подвижные и активные, тогда как комбинации с высокими ‘ловк-терп’ характерны для лабиринтов.
Терпение (терп)
Терпение — это субъективный рейтинг; он в большей степени отражает "ощущение времени", много ли времени потребовалось для решения уровня, исходя из субъективных ощущений игрока. Таким образом, два однотипных уровня могут иметь различные терп-рейтинги, если, например, у одного из них более привлекательный дизайн или прогресс игрока на нём более очевиден (например, сразу видно количество открытых оксидов). Необходимость много раз начинать уровень заново оказывает существенное влияние на этот рейтинг; важно не время в нижнем левом углу экрана и не счёт, а субъективное ощущение времени, прошедшего с момента, как вы впервые окинули уровень взглядом, до завершения уровня.
Большое количество оксидов на уровне может повысить его терп-рейтинг или же понизить его: если игроку нужно обойти уровень несколько раз, чтобы открыть все пары оксидов, это определённо повышает рейтинг. Однако, если оксиды расположены так, чтобы отмечать прогресс игрока, и являются своего рода наградой за пройденные этапы, они могут понизить терп-рейтинг. То же самое и с уровнями, на которых имеется большое количество дверей: решающим фактором является их расположение.
Высокие терп-рейтинги обычно присваиваются лабиринтам. В сочетании с "инт 3" высокий терп-рейтинг обычно свидетельствует о наличии на уровне спрятанного предмета или полого камня. Терп-рейтинги относятся ко всему уровню, так что они не могут накапливаться.
Знание игры (знан)
Рейтинг знания игры в основном учитывает функции и способы взаимодействия различных объектов игры, таких как камни, покрытия, предметы и актёры. Однако в некоторых случаях он учитывает и особые способы решения уровней. Основой при выставлении рейтинга является уровень "Advanced Tutorial", которому присвоен рейтинг "знан 3". Рейтинг "знан 4" присваивается уровням, на которых встречаются стандартные объекты, отсутствующие в "Advanced Tutorial". Уровни с рейтингом "знан 5" требуют более глубокого знания внутренних закономерностей игры. И, наконец, рейтинг "знан 6" является показателем уникальных либо редко встречающихся способов решения уровня. Общий знан-рейтинг уровня определяется по наиболее сложным объектам или способам решения, встречающимся на нём, и, таким образом, не накапливается:
"Знан 6" не обязательно означает, что уровень сложен для понимания; уникальный объект или механизм может быть и интуитивно понятным, как, например, во "Flood Gates".
Скорость и контроль над скоростью (скор)
Скор-рейтинг отражает не только максимальную скорость, которая требуется от игрока (например, когда вам нужно убежать от ротора), но и степень контроля над мышью, которой обладает игрок. Отличными примерами применения второго критерия являются уровень "Mourning Palace" и средняя часть "Sacrifice". Этот критерий учитывает необходимость двигать мышью с постоянной скоростью на протяжении длительного времени, а также необходимость правильной оценки скорости, требуемой для выполнения определённого задания (например, разбить стекло).
Скор-рейтинг накапливается, так как множество медленных роторов могут в сумме составить задание, по сложности подпадающее под "скор 3" или "скор 4", а несколько медленных таймеров-переключателей, которые нужно нажать в определенном порядке, могут представлять собой задание, находящееся на пределе человеческих возможностей. В отличие от остальных категорий, для которых средний рейтинг приближается к 3 (или находится между 3 и 4 для знан), большинство уровней определённо подпадают под определение "скор 1". Таким образом, скор-рейтинг можно, скорее, рассматривать как приложение к трём основным рейтингам: инт, ловк и терп.
Совокупность рейтингов
Иногда бывает интересно определить общую шкалу, по которой можно измерить сложность уровня. Простейший способ вычислить такой общий рейтинг — это выбрать линейную комбинацию пяти отдельных рейтингов, взятых с определёнными коэффициентами. Эти коэффициенты должны соответствовать тому вкладу, который данная категория вносит в общую сложность уровня. Но коэффициенты следует выбирать с осторожностью, чтобы избежать теоретических казусов при вычислении (к примеру, если коэффициенты для всех рейтингов, кроме скор, являются чётными, мы получим заметное различие в распределении чётных и нечётных общих рейтингов, что может заметно исказить картину). Ниже мы приводим работающую и чрезвычайно интересную линейную комбинацию, которую мы применяли для изменения порядка следования уровней:
общая сложность = 7*инт + 6*ловк + 4*терп + 3*знан + 4*скор - 23 |
Особенностью данного способа вычисления общего рейтинга является то, что он распределяет уровни по относительно широкой и непрерывной шкале от 1 (все рейтинги равны 1) до 100 (все рейтинги 5, знание 6) и основной упор в нём сделан на самые сложные категории — интеллект и ловкость. Однако некоторые очень низкие и очень высокие значения (например 2 и 99) не могут встречаться в этой комбинации. Другие комбинации позволяют получить полную, но узкую, либо широкую, но не являющуюся непрерывной шкалу.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
В колонке баллов находятся ваши баллы, а также некоторые сравнительные величины для нормального и лёгкого режимов (если уровень поддерживает лёгкий режим).
Мировой рекорд — это лучшее время прохождения уровня, которое было прислано команде разработчиков Enigma. Мировые рекордсмены перечислены под колонкой с баллами.
PAR — это профессиональный усреднённый рейтинг (professional average rating) уровня. Это гармоническое среднее всех баллов, которые были присланы нам игроками. Однако мы учитываем только баллы, набранные игроками, которые решили определённое количество уровней. В отличие от мирового рекорда, побить который очень сложно, PAR представляет собой куда более достижимую цель для амбициозного игрока. Уровни, пройденные за время, равное PAR либо лучшее, чем PAR, отмечаются изображением ускоряющегося шарика в меню уровней.
Ещё один тип сведений в данной колонке — это авторское время. Большинство авторов не ставят себе цели удерживать мировые рекорды для своих собственных уровней. Однако им обычно известен кратчайший путь решения уровня. Если ваше время значительно превышает авторское, то для этого уровня, вполне возможно, существует более простое решение.
Количество решивших — это количество игроков, которые решили данную версию уровня.
Процент решивших — это отношение количества игроков, решивших данный уровень, к общему количеству игроков, приславших нам свои баллы. Мы учитываем только тех игроков, у которых была возможность решить данный уровень. К примеру, игроки, которые прислали нам свои баллы до написания уровня и после их не обновляли, нами не учитываются. Низкий процент решивших указывает на то, что решить уровень не так-то просто.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
В колонке "Версия" содержится подробная информация об уровне. Обратитесь к разделам <version> и <modes> главы Основы работы с уровнями, где объяснены приведённые в колонке значения.
Для игрока наибольший интерес представляет строка ‘Номер версии’. На эскизе уровня, который решён за определённое время, после обновления Enigma может появиться красный треугольник. Хотя медали, которые указывают на то, что уровень решён, и присутствуют в меню уровней, баллы больше не указываются. Это происходит из-за того, что обновлённый уровень требует нового решения и баллы, набранные при решении предыдущего уровня, к нему неприменимы. В таких случаях автор увеличит номер версии уровня.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
В этом поле можно ввести свою аннотацию уровня, которую можно изменить при повторных попытках пройти уровень. Обратите внимание, что нынешние возможности текстового поля ограничены (нельзя ввести некоторые символы, и курсор при введении текста должен оставаться в границах поля). Тем не менее, это поле подходит для ввода кратких примечаний, которые могут пригодиться позже.
Примечания хранятся в вашем персональном файле ‘state.xml’. Для каждого уровня, вне зависимости от его версии, можно ввести только одно примечание.
Кроме того, уровням можно выставлять оценки. Просто нажмите на кнопку "Рейтинг". Можно выставлять оценки от 1 до 10, знак ‘-’ означает "воздержался". 0 означает очень плохой уровень, в который, по вашему мнению, не стоит и играть, 5 обозначает средний уровень, 10 — самый лучший. Старайтесь использовать все значения при выставлении оценок.
Ваши оценки хранятся вместе с баллами и обрабатываются анонимно. Средняя оценка, полученная после обработки всех оценок, выставленных уровню игроками, выводится для вашего сведения. Обратите внимание, что для различных версий уровня возможны разные оценки, так как уровни могут улучшиться под влиянием комментариев пользователей. Если вы не выставили оценку новой версии уровня, Enigma оставляет оценку, которая была выставлена предыдущей версии.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Играя на уровне, можно делать снимки экрана при помощи клавиши <F10>. Можно сделать целую серию снимков экрана, чтобы получить документальные свидетельства об игре. Каждый снимок экрана программа сохраняет под уникальным именем файла. При помощи инспектора уровней можно просматривать снимки экрана прямо из игры. Просто нажмите на кнопку <Снимок экрана> для просмотра первого изображения.
Так как кнопки могут помешать просмотру, управление игрой в данном режиме осуществляется исключительно с помощью клавиатуры. Нажмите <F1> для вызова экрана помощи, <ESC>, чтобы вернуться к инспектору уровней, <Page Up> и <Page Down> для просмотра, соответственно, предыдущего и следующего снимка экрана. Если продолжить просмотр после показа последнего снимка экрана, "недостающий" файл снимка экрана получит имя. Это может быть полезной подсказкой, где искать другие файлы снимков экрана в вашей ‘папке эскизов’ (см. раздел Размещение ресурсов).
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
В то время как 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/размер пакета" для получения сравнимых величин гандикапа. Гандикапы указываются с точностью до одной десятой.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
(Следующая информация действительна только для версии 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 }, |
Данные атрибуты имеют следующие значения:
Чтобы создать новый набор звуков, следуйте следующей инструкции:
(папка пользователя)/soundsets/my_sounds/ /soundset.lua /high_pitch.wav /soundfile_13.wav ... |
... coinsloton = { file="enigma/st-coinslot" }, ... |
Если используете свои собственные файлы, не забудьте создать соответствующие подпапки, например:
... coinsloton = { file="my_sounds/soundfile_13" }, ... |
Ни в коем случае не указывайте расширение ".wav"! Оно добавляется автоматически. Проверьте, чтобы расширение было написано строчными буквами.
Не забудьте выбрать нужный набор звуков в меню настроек всякий раз, когда вы переименовываете его. И всегда выходите из игры, когда вносите изменения в набор звуков: Enigma не определяет новые наборы звуков без перезапуска.
У вас есть право свободно распространять zip-архивы, содержащие ваш набор звуков и файл ‘soundset.lua’. Новый набор звуков можно установить из архива, просто распаковав его в поддиректорию ‘soundsets’ вашей папки пользователя. Убедитесь, что файл ‘soundset.lua’ находится на одну поддиректорию ниже, чем ‘soundsets’. Деинсталлировать набор звуков можно, просто удалив соответствующую папку. Если вы хотите сделать набор звуков невидимым для Enigma, одного только переименования директории недостаточно, следует переименовать и файл ‘soundset.lua’. Это может пригодиться, если вы используете взаимозависимые наборы звуков (звуковые наборы, использующие общие файлы) и хотите сделать недоступным только один из них.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Теперь, после ознакомления с основами работы с Enigma, вам, наверное, будет интересно узнать, как уровни организуются в пакеты и как можно добавить уровни либо пакеты уровней в игру.
Пакеты уровней — это отсортированные коллекции уровней, состоящие из указателя и (необязательно) приложенных к нему источников уровней. Не все источники уровней должны обязательно находиться в самом пакете. В пакете уровней могут использоваться перекрёстные ссылки на уровни из других пакетов. Если пакет не содержит собственных источников и состоит исключительно из перёкрестных ссылок, уместно называть его перекрёстным указателем, поскольку он представляет собой только файл-указатель.
Приведённое выше описание соответствует всем версиям Enigma. Однако вплоть до версии 0.92 пакеты уровней нужно было редактировать вручную, а регистрация пакетов была сложным процессом. Поэтому для версии Enigma 1.00 мы решили переписать всю систему пакетов, постаравшись сделать её гибкой и лёгкой в использовании. Мы преследовали следующие цели:
Некоторые из этих функций работают автоматически. Их можно задействовать прямо из меню уровней. Для использования других необходимо знать, где размещать файлы. Более подробная информация приводится в следующих разделах:
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Одна из наиболее выдающихся черт Enigma — это возможность добавлять в неё новые уровни. А сообщество пользователей обычно присылает нам несколько новых превосходных уровней каждую неделю.
Добавить новый уровень, полученный в виде XML-файла, очень просто. Найдите поддиректорию ‘levels/auto’ в вашей ‘папке пользователя’ (см. раздел Размещение ресурсов), скопируйте файл уровня в эту папку и перезапустите игру. Новый уровень появится в пакете ‘Auto’ и в него можно будет сыграть точно так же, как и в любой другой.
Обратите внимание, что для уровней, содержащих ошибки либо несовместимых с данной версией, в меню уровней отображается значок ошибки. Естественно, при попытке сыграть в уровень будет получено сообщение об ошибке Просмотрите метаданные уровня в инспекторе уровня (см. раздел Информация об уровне), чтобы определить совместимую версию, и свяжитесь с автором уровня по электронной почте в случае наличия ошибок в коде.
Второй способ сыграть в новые уровни — это добавить адреса файлов этих уровней в командную строку (см. раздел Опции запуска). Так можно играть в уровни, которые хранятся где угодно, можно даже вводить URL уровней, размещённых в Интернете. Уровни, добавленные в командную строку, находятся в пакете ‘Startup Levels’.
При желании сыграть в уровень Lua, написанный с использованием устаревшего формата для Enigma 0.92 или более ранней версии, можно попытаться запустить его из командной строки. В таких уровнях отсутствуют необходимые метаданные для автоматического определения. Однако уровни, заданные из командной строки, воспринимаются игрой как временные, доступные только для одного запуска; недостающие данные заменяются уместными данными по умолчанию. Такой уровень, возможно, запустится, но сохранение баллов, копирование и ссылки на уровень будут невозможны.
Помимо отдельных новых уровней в сообществе пользователей могут распространяться целые пакеты. Эти пакеты могут представлять собой папки с уровнями, zip-архивы или отдельные XML-файлы. Все пакеты могут быть установлены простым копированием файлов, но между этими тремя форматами существуют различия.
Пакеты уровней, распространяемые в виде папок с файлами уровней и файлом-указателем, следует копировать в папку ‘levels’ в вашей папке пользователя (см. раздел Размещение ресурсов).
Пакеты уровней, распространяемые в виде zip-архивов, следует копировать в папку ‘levels’ в вашей папке пользователя. Распаковывать их не обязательно, хотя это и возможно (см. раздел Пакеты в формате zip).
Пакеты уровней, распространяемые в виде отдельных файлов-указателей XML, следует копировать в папку ‘levels/cross’ в вашей папке пользователя.
Все новые пакеты должны быть доступны из меню пакетов уровней после перезапуска Enigma.
Это всё, что нужно знать для того, чтобы добавлять новые уровни и пакеты уровней для тестирования и игры. Если вы заинтересованы прежде всего в том, чтобы создавать новые уровни, то стоит сразу же обратиться к главе Основы работы с уровнями. В остальных же разделах этой главы объясняется, как размещать и сортировать уровни в пользовательских пакетах.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
В связи с изменением формата указателя пакета уровней возникла необходимость в конвертировании пакетов из предыдущих версий. Хотя основная работа выполняется автоматически при запуске 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 (см. раздел Основы работы с уровнями).
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Помимо классических пакетов уровней, представляющих собой поддиректорию папки ‘levels’ с файлом ‘index.xml’ и несколькими файлами уровней, в Enigma 1.00 предусмотрен совместимый формат zip-архивов. Zip-формат позволяет уменьшить размер ресурсов игры и облегчает распространение пакетов.
Этот формат стопроцентно совместим. Если у вас есть обычный пакет уровней, размещённый в поддиректории, то можно просто заархивировать всю папку и назвать архив её именем с расширением .zip. Теперь эту папку можно полностью удалить: Enigma автоматически определит пакет уровней и позволит играть в него без ограничений. Сохранятся даже перекрёстные ссылки на этот пакет.
С другой стороны, Enigma предоставляет возможность превратить заархивированный пакет в папку с указателем и файлами уровней. Опять же, на уровнях из пакета можно играть без ограничений и все перекрёстные ссылки сохраняются.
Если сохранить и архив, и папку, то файлы папки обладают приоритетом перед архивом. Таким образом, Enigma сохраняет обновления в поддиректориях параллельно с существующими zip-архивами.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
С увеличением количества пакетов возникла необходимость сгруппировать их в меню. Мы попытались предложить набор групп по умолчанию, основанный на простом и удобном распределении пакетов по группам:
Тем не менее, своё мнение мы не навязываем. Вы вольны переименовывать группы, добавлять новые и изменять расположение пакетов. Как и в случае с другими аспектами Enigma, управлять пакетами и группами пакетов можно, щёлкнув любой кнопкой мыши + Ctrl либо одной правой кнопкой по кнопке соответствующего пакета или группы.
В меню конфигурации групп можно переименовать группу либо изменить её расположение. Группе можно выбрать любое имя, которое ещё не использовалось, не заключено в квадратные скобки и отличается от ‘Every Group’. Обратите внимание, что, возможно, вам не удастся ввести все желаемые символы. Приносим извинения за это неудобство.
В меню конфигурации пакетов можно поместить пакет в какую-либо группу. Список групп содержит два специальных пункта: ‘[Every Group]’ и ещё одно имя в квадратных скобках. При выборе первой псевдогруппы пакет уровней отображается в каждой группе. Это является размещением по умолчанию группы ‘Startup Levels’. Второе имя в квадратных скобках — это группа, в которой по умолчанию размещается сам пакет уровней. Это подсказка, которая позволит переадресовать пакет уровней в группу по умолчанию, даже если удалить саму группу.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Чтобы создать новый пакет уровней, выберите группу, в которую следует этот пакет добавить. Скорее всего, это будет группа ‘User’. Щёлкните любой кнопкой мыши + Ctrl или одной правой кнопкой по группе, затем нажмите на кнопку ‘Новый пакет’. На экране отобразится меню конфигурации пакетов, где можно ввести все данные, необходимые для создания пакета уровней.
Вначале нужно ввести имя пакета. Здесь также есть ограничение в выборе символов, которые можно использовать для имён файлов. Разрешается использовать алфавитно-цифровые символы A-Z, a-z, 0-9, пробелы, "_" и дефис. Обратите внимание, что позже вы сможете переименовать пакет, если захотите дать ему более удачное или более подходящее имя (см. раздел Изменение и удаление пакетов).
После этого следует определить, хотите ли вы создать пакет, в котором могут находиться источники уровней или только перекрёстные ссылки. Первый вариант удобен для хранения самостоятельно написанных либо загруженных из Интернета уровней. Пакет с перекрёстными ссылками можно использовать для хранения коллекций своих любимых уровней, для которых просто отбираются существующие уровни из других пакетов, исходя из своих собственных критериев. Нужный тип пакета можно выбрать с помощью кнопки ‘Типы уровней’, которая использует символы для ссылок и копий.
‘Размещение’ — это величина, которая определяет размещение среди групп пакетов, если пакет не был пересортирован вручную (см. раздел Группировка и сортировка пакетов). Эта задаваемая по умолчанию величина имеет значение, только если вы распространяете свой пакет и хотите быть уверенным, что пользователи поместят этот пакет там, где нужно. В большинстве случаев величина, автоматически задаваемая после создания нового пакета, является оптимальной.
Можете указать себя в качестве владельца или создателя пакета. Это просто строка для идентификации.
Наконец, после окончания настройки вы можете создать пакет, нажав кнопку ‘OK’. Enigma создаст новый пакет в папке пользователя (см. раздел Размещение ресурсов).
Если вы решите не создавать новый пакет, нажмите кнопку ‘Отмена’. В этом случае всё останется без изменений.
Если вы хотите сразу же добавить уровни в пакет, нажмите кнопку ‘Составить пакет’. Enigma создаст пакет, и вы сможете использовать составитель пакетов, чтобы наполнить этот пакет уровнями.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Чтобы изменить пакет, щёлкните по нему любой кнопкой мыши + Ctrl либо одной правой кнопкой в меню пакетов уровней. На экран будут выведены метаданные всех пакетов. Однако кнопка ‘Ред. метаданные’ появится только для ваших собственных пакетов, которые хранятся в папке пользователя. Нажав на неё, можно отредактировать метаданные.
Пакет можно переименовать, но нельзя изменить имена файлов. Enigma будет использовать новое имя в качестве логического имени пакета, которое отображается в меню.
Другие свойства, которые можно изменить, включают в себя ‘Размещение’ и ‘Владелец’.
Имейте в виду, что изменить тип уже существующего пакета нельзя. Следует создать новый пакет нужного типа и скопировать туда уровни (см. раздел Составление пакетов уровней).
Во избежание случайной потери файлов уровней в игре не предусмотрена функция удаления пакетов. Тем не менее, пакет можно удалить, просто удалив папку с этим пакетом из вашей папки пользователя. Для пакетов, состоящих из перекрёстных ссылок, просто нужно удалить файл-указатель XML в поддиректории ‘levels/cross’ вашей папки пользователя.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Изменения в состав пакета могут быть внесены при помощи составителя пакетов. Его можно вызвать, нажав любой кнопкой мыши + Ctrl либо одной правой кнопкой мыши на кнопку пакета в меню пакетов уровней, а затем на кнопку ‘Составить пакет’ в меню конфигурации пакета.
Составитель выглядит почти так же, как меню уровней, но отличается от него своими функциями. Список всех команд можно вывести на экран с помощью клавиши <F1>. При составлении собственного пакета уровни пакета отображаются в красной рамке. Это предупреждение о том, что вы можете модифицировать этот пакет. В системных пакетах (поставляемых с дистрибутивом Enigma) уровни отображаются в серой рамке, поскольку составитель можно использовать только для того, чтобы скопировать уровни в буфер обмена.
Буфер обмена позволяет выбрать уровни из одного или нескольких пакетов и вставить их в виде копии либо перекрёстной ссылки в свой пакет. Сначала очистите буфер обмена при помощи клавиш ‘Shift + delete’. Затем выберите нужные уровни из составителя. Добавьте их в буфер обмена с помощью ‘Shift + щелчок’. Они отобразятся в строке вверху экрана. Вернитесь в пакет, в который желаете добавить уровни. Выберите уровень, после которого желаете вставить уровни. Используйте клавишу ‘F8’, чтобы вставить уровни из буфера в виде ссылок. При редактировании пакета, в котором допускаются копии уровней, их можно вставить при помощи клавиши ‘F9’.
При внесении каких-либо изменений в пакет уровней в левом верхнем углу экрана появляется маленький красный треугольник. Подтвердить все изменения можно, выйдя из составителя с помощью кнопки ‘OK’. Если выйти из составителя с помощью кнопки ‘Отмена’, то все изменения будут отменены.
Помимо добавления уровней, их можно удалять с помощью клавиши ‘delete’. Имейте в виду, что если удалить уровень, который является файлом, а не просто ссылкой, то Enigma удалит и сам файл уровня. Будьте осторожны с уровнями, на эскизе которых имеется изображение документа. Отменить удаление можно с помощью кнопки ‘Отмена’.
Сортировка уровней осуществляется с помощью клавиш ‘alt + стрелка влево’ и ‘alt + стрелка вправо’. Новый порядок следования уровней отображается на экране незамедлительно, и его можно сохранить, нажав кнопку ‘OK’.
Чтобы обновить индекс уровней, можно воспользоваться клавишей ‘F5’. Это может пригодиться во время редактирования уровня. В пакете отразятся изменения названия, версии, появление простого режима для уровней и т. д. Все уровни пакета Enigma обновляет одновременно.
Используя пакет уровней Auto и составитель пакетов, можно создавать пакеты, состоящие из ваших собственных уровней: создайте новый пакет, добавьте файлы уровней в папку ‘auto’, перезапустите игру, добавьте уровни из папки ‘auto’ в буфер обмена, с помощью составителя вставьте уровни из буфера обмена в свой пакет в виде копии и удалите неиспользуемые файлы уровней из папки ‘auto’.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Сыграв в несколько уровней 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 (см. раздел Начало работы с пакетами уровней) либо используйте как аргумент в командной строке (см. раздел Опции запуска).
В следующих разделах мы остановимся на подробностях форматирования и поясним необязательные части.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Скорее всего, вам хочется понять основные принципы расположения объектов на уровне. Здесь приведено описание очень простого уровня, которое также может служить отправной точкой для новых уровней. (Вообще-то, это первый уровень 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 "####################"}) |
Результат выглядит в игре следующим образом:
Давайте теперь проанализируем эту программу строка за строкой:
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.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Давайте начнём с полного обзора всех существующих ключевых элементов верхнего уровня 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, ‘&’ и ‘<’. Эти два символа зарезервированы как начальные символы тега и объекта. Если они понадобятся в текстовом содержимом или значениях атрибутов, их следует заменить соответственно объектами ‘&’ и ‘<’. К тому же, значения атрибутов следует заключать между ‘"’ или ‘'’. Безусловно, необходимо заменять и кавычки, используемые в значениях атрибутов. Используйте ‘"’ и ‘&apos’.
Элементы:
Это основной элемент. В каждом файле он присутствует только в одном экземпляре. Как и первая строка объявления XML, эта вторая строка довольно фиксирована. Существует две её версии. Простая версия с 3 атрибутами, которая использовалась в первом примере, и версия предназначенная только для редакторов уровней, использующая 4 атрибута, как в последнем примере. Для ручного редактирования уровня просто скопируйте простую версию во вторую строку своего файла уровня.
Атрибуты:
Определение пространства имён схемы. Содержимое установлено в “http://www.w3.org/2001/XMLSchema-instance”. Тег атрибута ‘xsi’ должен соответствовать префиксу следующего тега атрибута и является стандартным.
Размещение используемых схем. Содержимым служит фиксированное пространство имён уровней Enigma, после которого следует URL размещения схемы. Редакторы уровней добавят свои пространства имён и URL размещения схемы, как во втором примере.
Определение пространства имён для “уровня Enigma”. В качестве префикса пространства имён всех тегов элементов и атрибутов уровня мы, как правило, используем ‘el’. Используемый префикс может быть произвольным, но должен соответствовать своему тегу атрибутов. Содержимое атрибута зависит от пространства имён уровня Enigma.
Последнее определение пространства имён используют только редакторы уровней. Например, мы объявили ‘ee’ как префикс пространства имён для всех тегов элементов и атрибутов редактора. Используемый префикс может быть произвольным, но должен соответствовать своему тегу атрибутов. Содержимым атрибута является пространство имён редактора.
Защищённый раздел содержит все данные уровня, полученные от автора, которые не может изменять никто другой.
Информационный раздел содержит все метаданные уровня. Он обязателен и детально описан в разделе Информация о метаданных.
Раздел элементов необязателен. Он содержит части описания уровня, полученные с помощью управления данными. Хотя данный элемент введён для поддержки редакторов уровней, автор уровня может использовать любые части этого раздела как ему или ей захочется.
Раздел luamain является частью для ручной вставки определений уровня Lua. Он подробно описан в разделе Код LUA.
Раздел редактора — это зона расширений, открытая для редакторов уровней. Они могут добавлять в этот раздел необходимую дополнительную информацию. Enigma же игнорирует этот раздел.
Раздел интернационализации содержит строки на английском, переводы на другие языки и комментарии, предоставляемые переводчикам самим автором. Это обязательный раздел, и он подробно описан в разделе Интернационализация (i18n).
Общедоступный раздел — это необязательное расширение защищённого раздела. Он содержит информацию не подтверждённую автором и даже может быть добавлен после последнего пересмотра самим автором.
Общедоступный раздел интернационализации содержит дополнительные переводы, поставляемые с уровнем. Их можно получить у автора или из других источников. Переводчики утверждают эти переводы и они могут использоваться, пока переводчики не предоставят исправленные версии (см. раздел Интернационализация (i18n)).
Раздел обновления является частью системы Обновление и дополнение.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Раздел информации содержит все предоставленные автором метаданные для этого уровня. Это источник таких данных. Все остальные части 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> |
Атрибуты:
“level”
, “library”
, “multilevel”
Схему можно использовать для отдельных уровней Enigma, библиотек, содержащих описания частей уровня для многократного использования, и описаний сразу нескольких уровней.
‘level’ предназначен для всех определений отдельного уровня. Не имеет значения, редактируется ли он вручную или с помощью редактора уровней и какое используется описание элементов.
‘library’ предназначен для описания частей уровня, которые могут быть включены в другой уровень. Библиотека состоит только из кода Lua в разделе luamain. Библиотеки могут использовать почти все разделы кроме ‘/level/protected/info/score’ и ‘/level/*/i18n’, которые должны предоставляться, но не могут использоваться. Библиотеки включаются в уровни с помощью элемента зависимости (см. раздел <compatibility>).
‘multilevel’ предназначен для описания сразу нескольких уровней. Основная цель — поддержка форматов уровней сторонних игр, таких как формат уровней Сокобан, которые обычно описывают в одном файле целый набор карт уровней (см. раздел Многоуровневые файлы).
Количество уровней в файле, содержащем несколько уровней (см. раздел Многоуровневые файлы).
Содержимое — элементы:
Заголовок, подзаголовок и основная строка описания уровня (см. раздел <identity>).
Все аспекты версии уровня (см. раздел <version>).
Вся предоставленная автором информация о себе (см. раздел <author>).
Сообщение об авторском праве на уровень (см. раздел <copyright>).
Информация об условиях лицензии (см. раздел <license>).
Вся информация о совместимости с версиями Enigma, зависимостях от библиотек, внешних данных и редакторе, в котором был создан уровень (см. раздел <compatibility>).
Режимы, которые поддерживает уровень, такие как сложность, сетевой режим и контроль (см. раздел <modes>).
Необязательные комментарии, такие как благодарности, посвящения и комментарии по коду (см. раздел <comments>).
Результат самого автора уровня (см. раздел <score>).
3.3.1 <identity> | ||
3.3.2 <version> | ||
3.3.3 <author> | ||
3.3.4 <copyright> | ||
3.3.5 <license> | ||
3.3.6 <compatibility> | ||
3.3.7 <modes> | ||
3.3.8 <comments> | ||
3.3.9 <score> |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Элемент ‘identity’ необходим, т.к. он предоставляет информацию для идентификации уровня человеком и системой.
<el:identity el:title="Demo I18N" el:subtitle="Translate or let it be translated" el:id="20060211ral002"/> |
Атрибуты:
Английское название уровня. Строка может содержать произвольные символы, которые могут быть отображены шрифтом Enigma и поддерживаются XML. В многоуровневых файлах заключительный знак "#" имеет особое значение. Название не должно быть слишком длинным, потому что Enigma использует его в меню выбора уровня. Переводы названия можно предусмотреть в разделе Интернационализация (i18n).
Необязательный английский подзаголовок. Используется для частей названия, которые не помещаются в основное название, или для небольшого совета в начале. Enigma показывает подзаголовок на странице информации об уровне и в начале уровня. Переводы подзаголовка можно предусмотреть в разделе Интернационализация (i18n).
Это основная строка идентификации уровня, которая всегда актуальна, независимо от последующих обновлений выпуска. Данная строка не должна содержать пробелов, скобок и масок (‘*? ()[]{}’. Главное требование Enigma к id в том, чтобы он был уникальным для всех уровней, созданных всеми авторами по всему миру, и не заканчивался закрывающей квадратной скобкой.
Так как уровни можно редактировать в любом текстовом редакторе или различных специальных редакторах уровней Enigma, то проконтролировать уникальность id не представляется возможным. Поэтому мы предлагаем простой стандарт id во избежание возможных противоречий между id различных уровней:
ГГГГММДДпользовательNNN
Где ‘ГГГГ’,‘ММ’,‘ДД’ — это дата создания первой экспериментальной версии, ‘пользователь’ заменяется уникальным именем автора, а ‘NNN’ случайным числом. Например, у моего уровня под названием ‘Houdini’ такой id: ‘20060816ral719’. Конечно, у всех уровней созданных в один день случайное число должно отличаться. id — это системный идентификаток уровня Enigma, и пользователь никогда в нём не нуждается.
Для обратной совместимости уровни, созданные для предыдущих версий игры, сохраняют своё прежнее имя файла в качестве id нового уровня и не используют приведённую выше схему. Это не создаёт никаких проблем, потому что единственным требованием к id является его уникальность.
Содержимое:
Этот элемент пуст — его нельзя заполнять.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Этот элемент предоставляет системе информацию о версии.
<el:version el:score="1" el:release="1" el:revision="0" el:status="experimental"/> |
Атрибуты:
Номер версии задаётся как положительное целое число. Новые уровни начинают с версии под номером "1". Если изменения в уровне вызывают появление новых способов решения с несопоставимыми результатами, то новые версии уровня должны увеличивать номер версии. Конечно, авторы уровней должны прибегать к подобным изменениям только в крайних случаях.
При создании уровня, следует пользоваться атрибутом ‘status’, чтобы пометить уровень как неготовый. Когда автор сменит ‘status’ на ‘released’, он должен проверить сопоставимость результатов и, при необходимости, увеличить номер версии.
Этот атрибут — логический эквивалент атрибута ‘revision’ файла ‘index.txt’ в Enigma 0.92.
Технический выпуск версии задаётся как положительное целое число. Новые уровни начинают с выпуска под номером "1". Если изменения в уровне вызывают техническую несовместимость с предыдущими выпусками Enigma или несовместимость номеров версии, то следует увеличить номер выпуска версии.
Основной причиной технической несовместимости может стать изменение движка Enigma. Поскольку такая коррекция не будет работать на старой версии Enigma, версии уровней должны отличаться различными номерами выпуска.
И в случае технической несовместимости, и в случае несовместимости номеров версий также должно быть изменено имя файла уровня. Это необходимо, потому что на некоторые системы одновременно могут быть установлены различные версии Enigma. Они нуждаются в одновременном доступе к обеим версиям уровня. Также одновременный доступ к различным версиям уровней Enigma должны предоставлять интернет-сервера.
Чтобы дать игрокам возможность пользоваться различными выпусками файлов уровней, мы настоятельно рекомендуем использовать следующий стандарт именования уровней ИдентификаторАвтораНомерУровня_НомерВыпуска.Суффикс, где номер уровня состоит по крайней мере из 2 цифр; например, ‘ral01_2.xml’
Номер издания — это простой, постоянно увеличивающийся номер версии. Новый номер издания должен быть у каждой изданной версии уровня. Номер издания независим от номера версии и выпуска.
Если в своих путях поиска данных Enigma найдёт два файла уровней с одинаковыми именами, идентификатором, номером и выпуском версии, она загрузит тот, у которого номер издания больше. Эта возможность гарантирует, что более старое издание уровня, хранящееся в домашней директории уровней пользователя, не будет использоваться вместо нового издания уровня, поставляемого с новым выпуском Enigma. Обновления по сети также проверяют номера изданий уровней.
Хотя издание задается числом, атрибут может получить другую строку в виде ключевого слова ‘$Revision: 1.19 $’. Этот формат Subversion позволяет репозиторию Subversion авторов уровней автоматически вставлять номер издания уровня. Они должны просто установить значение ‘svn propset svn:keywords "Revision" level.xml’ для каждого файла уровня в своем репозитории. Поскольку номер издания Subversion — это постоянно увеличивающаяся величина, она удовлетворяет нашим критериям. Заметьте, что Enigma не требует, чтобы номера издания шли по порядку.
Этот атрибут описывает качество уровня во время разработки. Enigma использует состояние для защиты базы данных результатов от искажений непредусмотренными результатами решения. Она записывает только результаты уровней, помеченных как ‘released’.
К сведению авторов уровней: при изменении уже выпущенного уровня следует снова сменить его статус на ‘experimental’. Затем внесите изменения и протестируйте уровень. Когда будете абсолютно уверены, что не добавили никаких спойлеров, то можете снова выпустить уровень с новым изданием, а может, и новым выпуском или номером версии.
Содержимое:
Этот элемент пуст — его нельзя заполнять.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Информация о самом авторе. Enigma необходим только сам элемент автора, а все атрибуты необязательны, чтобы оставить автору возможность остаться анонимом. Пожалуйста, помните, что у администраторов уровней и переводчиков может возникнуть необходимость связаться с вами. Поэтому, пожалуйста, предоставьте им такую возможность.
Элемент автора может выглядеть следующим образом:
<el:author el:name="Ronald Lamprecht" el:email="ral@users.berlios.de" el:homepage="http://myhomepage.domain"/> |
Атрибуты:
Имя автора в том виде, в каком оно будет показываться на странице информации об уровне и в начале уровня. По умолчанию это имя ‘anonymous’.
Электронная почта автора, новостная лента или форум, которые он просматривает. В основном это указание о том, как связаться с ним/ней. Значение просто показывается в виде строки на странице информации об уровне.
Домашняя страница автора или место, где он размещает дополнительные уровни Enigma. Значение просто показывается в виде строки на странице информации об уровне.
Содержимое:
Этот элемент пуст — его нельзя заполнять.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Стандартизованное расположение сообщения об авторских правах:
<el:copyright>Copyright © 2006 Ronald Lamprecht</el:copyright> |
Атрибуты:
отсутствует
Содержимое:
Уведомление об авторских правах.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Конечно, каждый автор волен сам выбирать условия лицензии для своих уровней. Однако, автор должен указать эти условия. Для этого и нужен этот элемент и его атрибуты:
<el:license el:type="GPL v2.0 or above" el:open="true"/> |
Атрибуты:
Короткое определение типа лицензии, с необязательной ссылкой на текст лицензии или строкой ‘special’, если автор приводит в содержимом этого элемента свою собственную лицензию.
Булево значение, показывающее, удовлетворяет ли выбранная лицензия критериям Open Source Initiative (OSI). Пожалуйста, имейте в виду, что значение ‘false’ может помешать распространению вашего уровня с Enigma.
Содержимое:
В качестве содержимого этого элемента допускается использовать полный текст лицензии. Воспользуйтесь атрибутом ‘type’, чтобы определить уровень.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
<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-Lua, от которой зависит этот уровень. Используя несколько экземпляров данного элемента, можно указать несколько библиотек. Если библиотека настроена на предварительный запуск, движок загрузит её перед загрузкой или исполнением любого кода Lua, используемого в уровне. Порядок загрузки нескольких библиотек основывается непосредственно на порядке элементов зависимостей.
Атрибуты:
Путь к ресурсам библиотеки без её суффикса и любого расширения. Большинство библиотек Enigma хранит в поддиректории ‘lib’ своей директории ‘levels’, в большинстве случаев путь к ресурсам будет примерно таким: ‘lib/ant’. Это действительный путь к файлу библиотеки, который может иметь вид ‘levels/lib/ant.xml’, ‘levels/lib/ant.lua’ или ‘levels/lib/ant_1.xml’.
Однако, библиотеки могут быть и полностью зависимыми от пакета уровней. В этом случае можно выбрать относительный путь, такой как ‘./mylib’, и хранить библиотеку в самой директории пакета уровней.
Это независимый от версии id библиотеки, указанный в её метаданных. Во избежание проблем Enigma проверит данный атрибут при загрузке библиотеки и, вместе с номером выпуска версии, может использовать для определения перемещённых библиотек.
Хотя у различных выпусков библиотек должны быть различные имена, следует указывать версию библиотеки. Во избежание проблем Enigma проверит данный атрибут при загрузке библиотеки и, вместе с номером выпуска версии, может использовать для определения перемещённых библиотек.
Булево выражение, которое указывает, должна ли библиотека загружаться предварительно. Если библиотека не является предварительно загружаемой, её всё равно можно загрузить с помощью кодовых выражений Lua. Но даже эти библиотеки должны быть определены, т.к. Enigma проверит их на соответствие. При использовании раздела ‘elements’ всегда нужно предварительно загружать библиотеки.
Этот необязательный атрибут позволяет указать для библиотеки запасной адрес. Это будет полезно при использовании новых библиотек, ещё не поставляемых с системой.
Во время разработки и тестирования новых библиотек разработчик может распространять пробные уровни с пустым атрибутом пути к ресурсам ‘library’. Пробные уровни загрузят новейшую версию библиотеки, которая указана в полученном URL.
Содержимое:
отсутствует
Этот элемент можно использовать для указания любого внешнего текстового файла данных, от которого зависит этот уровень. Добавляя несколько экземпляров этого элемента, можно указать несколько файлов. Заданные файлы могут быть прочитаны с помощью интерфейса Lua.
Эта возможность призвана обеспечить поддержку других игр в Enigma, например Сокобана. В виду авторских прав и лицензионных соглашений, включение некоторых данных в уровень может оказаться неприемлемым. Однако распространение данных в оригинальном неизменном формате, возможно, будет законным.
Атрибуты:
Путь к ресурсам внешнего файла данных без расширения ‘.txt’. Путь должен иметь формат "./name"
для внешнего файла данных, который хранится локально в той же папке, что и файл уровня, или будет сохранён по данному адресу при загрузке, или "externaldata/name"
для общих внешних файлов данных, на которые ссылаются различные файлы уровней, хранящиеся в разных папках. Внешний файл данных может храниться локально или сохраняться в папке "levels/externaldata"
. В любом случае локальное имя внешнего файла данных будет иметь расширение ‘.txt’, чтобы обозначить его как читаемый, но не исполняемый для локальной операционной системы.
Этот необязательный атрибут позволяет задать сетевой адрес к внешнему файлу данных. При первом доступе отсутствующий внешний файл данных будет загружен, а его копия будет сохранена локально для последующего доступа.
Содержимое:
отсутствует
Специальные редакторы уровней используют этот элемент, чтобы хранить информацию о себе.
Атрибуты:
Название редактора уровней.
Номер версии редактора, полученный в виде строки .
Содержимое:
отсутствует
Содержимое:
отсутствует
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Элемент ‘modes’ позволяет автору указать поддерживаемый режим и режим уровня по умолчанию. Движок Enigma проверяет, используется ли уровень в поддерживаемом режиме.
<el:modes el:easy="false" el:single="true" el:network="false" el:control="force" el:scoreunit="duration" el:scoretarget="time"/> |
Атрибуты:
Если уровень поддерживает второй, упрощённый, режим, установите этот атрибут в ‘true’. Если поддерживается только сложный режим, установите атрибут в ‘false’.
Если уровень, как и стандартные, предоставляет возможность одиночной игры, установите этот атрибут в ‘true’. Установите этот атрибут в ‘false’, только если уровень представляет собой сетевую игру для 2 игроков.
Если уровень предоставляет возможность сетевой игры для 2 игроков, установите этот атрибут в ‘true’. В противном случае установите этот атрибут в ‘false’.
Этот атрибут задаёт для уровня стандартный режим управления. На уровне можно играть, пользуясь мышью, движение которой влияет на положение шариков: это стандартный способ, и он был единственным вплоть до Enigma 0.92. Или же можно играть на уровне, используя мышь или другие устройства ввода для балансировки шариками в мире уровня. Кроме того, можно использовать клавиши со стрелками на клавиатуре, чтобы перемещать актёра, как в классических играх Сокобан.
Хотя последнее слово в выборе более подходящего способа управления всегда остаётся за пользователем, автор должен задать стандартный режим управления, который использует система подсчёта результатов. Enigma сохранит и рассчитает для списков рекордов только результаты, полученные в заданном режиме управления.
Этот атрибут задаёт режим подсчёта и отображения результатов. В режиме по умолчанию, ‘duration’ результаты интерпретируются как время решения уровня и отображаются в формате ММ:СС. Режим ‘number’ отображает результаты в виде обычных чисел, а меньшие числа означают лучшие результаты. Этот режим подходит для подсчета толчков и шагов.
Целевое значение задаёт способ подсчёта баллов. При выборе ‘time’ учитывается время решения, при выборе ‘pushes’ — количество толчков камней, при выборе ‘moves’ — шаги актёра. Любое другое значение вызовет функцию Lua для подсчёта результатов. Цель в пользовательском интерфейсе представлена в виде короткого наименования для результатов.
Содержимое:
отсутствует
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Необязательный раздел комментариев позволяет автору добавить несколько комментариев и определить, как их нужно использовать. Пожалуйста, имейте в виду, что поддержка интернационализации не позволяет переводить комментарии.
<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> |
Атрибуты: отсутствуют
Содержимое — элементы:
Сам элемент комментариев в качестве содержимого включает только подэлементы.
Место для того, чтобы поблагодарить людей, помогавших вам при создании уровня.
Атрибуты:
Значение ‘true’ покажет сообщение на странице информации об уровне.
Значение ‘true’ покажет сообщение при запуске уровня. Пожалуйста, используйте эту возможность только в исключительных случаях.
Содержимое:
Сами благодарности. Он может быть разделён на несколько строк. Перед отображением лишние пробелы убираются.
Место для посвящения уровня уважаемому или любимому человеку. Пожалуйста, используйте для этой цели данное поле, а не документы на самом уровне.
Атрибуты:
Значение ‘true’ покажет сообщение на странице информации об уровне.
Значение ‘true’ покажет сообщение при запуске уровня. Пожалуйста, используйте эту возможность только в исключительных случаях.
Содержимое:
Само посвящение. Он может быть разделён на несколько строк. Перед отображением лишние пробелы убираются.
Атрибуты:
отсутствуют
Содержимое:
Главный комментарий к коду, который может представлять собой пояснение состояния <version> или список запланированных изменений. Он может быть разделён на несколько строк. Этот комментарий не обрабатывается.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
В этом разделе автор должен опубликовать свои собственные результаты в качестве ориентира и вызова для остальных игроков. Все значения связаны с режимом управления, заданным в <modes>.
<el:score el:easy="01:07" el:difficult="-"/> |
Атрибуты:
Время решения уровня в упрощённом режиме. Формат: ММ:СС, где ММ означает минуты, а СС секунды, либо - если автор ещё не решил свой уровень. Для уровней с единицей измерения результатов ‘number’ значение должно быть числом шагов шарика или толчков.
Время решения уровня в сложном режиме. Формат: ММ:СС, где ММ означает минуты, а СС секунды, либо - если автор ещё не решил свой уровень. Для уровней с единицей измерения результатов ‘number’ значение должно быть числом шагов шарика или толчков.
Содержимое:
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Этот элемент включает в себя любой код 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.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Интернационализация уровней стала движущей силой изменений в формате уровней. Можно заметить, что существует два элемента ‘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’.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
После сухой теории давайте посмотрим, как обращаться с форматом уровней 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. Такие редакторы выделят во всех местах все возможные элементы и атрибуты.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Поскольку мы указываем все необходимые атрибуты в элементе <version>, Enigma может загружать новые версии уровней.
Если Enigma загружает новые версии, у которых отличается только номер издания ‘revision’, мы подразумеваем ‘обновление’. Обновление может быть выполнено автоматически с заменой старых версий обновлёнными, поскольку автор гарантирует их совместимость в результатах и зависимостях. Автор должен предоставить адрес для загрузки автоматических обновлений в защищённом информационном элементе:
<el:update el:url="http://myLevelServer.org/path/level_1.xml"/> |
Атрибуты:
Доступный в течение длительного времени, полный адрес для загрузки обновлений этого уровня с теми же номером и выпуском версии.
Если автор уровня вносит в уровень элементы, приводящие к несовместимости, он увеличивает выпуск версии уровня и сохраняет файл уровня под новым именем. Мы называем загрузку такой версии нового уровня ‘дополнением’.
Чтобы сообщить о доступности выпуска дополнения, автор должен обновить предыдущий выпуск с последним номером издания, просто добавив элемент дополнения, сообщающий о новом выпуске:
<el:upgrade el:url="http://myLevelServer.org/path/level_2.xml" el:release="2"/> |
Атрибуты:
Действительный в течение длительного времени, полный адрес для загрузки дополнений этого уровня. Путь к новому файлу.
Выпуск версии дополнения.
Поскольку автор самостоятельно не может обновить все распространяемые уровни, чтобы сообщить о доступности нового выпуска, мы добавили ещё один элемент дополнения в общедоступном разделе. Администраторы уровней могут использовать этот элемент в тех же целях, с тем же синтаксисом без изменения защищённого раздела автора.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Библиотеки — это набор функций Lua для многократного использования во многих уровнях. Для использования библиотеки её необходимо объявить как зависимость, как описано в разделе <compatibility>. Предварительная загрузка библиотеки — это всё, что необходимо для её использования. Кроме того, для загрузки библиотеки в определенной точке выполнения можно использовать функцию @ref{enigma.LoadLib}.
Enigma предоставляет несколько очень полезных библиотек. Их можно найти по системному пути в поддиректории ‘levels/lib’. Большинство из них хорошо прокомментированы непосредственно в исходном коде. Для ‘ant’ существует отдельный файл с документацией: ‘doc/ant_lua.txt’.
В этом разделе, мы сконцентрируемся на аспектах написания и поддержки библиотек:
3.8.1 Написание библиотеки | ||
3.8.2 Поддержка библиотеки |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Файлы библиотеки практически аналогичны файлам уровней. Основное отличие в атрибуте ‘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> |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Библиотеки могут существовать в виде различных выпусков и изданий. Версии библиотек, которые отличаются только номером издания, считаются совместимыми версиями. Версии библиотеки, которые привносят несовместимость, должны отличаться номером выпуска версии. Однако ввиду того, что существующие уровни могут зависеть от поведения более ранних выпусков, необходимо поддерживать оба выпуска версии библиотеки и распространять их вместе с Enigma.
Для сосуществования эти различные выпуски библиотек должны следовать жёсткой схеме именования. У каждой библиотеки должно быть своё основное имя. В предыдущем примере это ‘lib/ant’. Имя файла данного конкретного выпуска — это основное имя с добавлением символа подчёркивания и номера версии плюс суффикс ‘xml’. Поэтому выпуск ‘lib/ant’ следует хранить в виде ‘lib/ant_2.xml’.
Если посмотреть в директорию lib, то можно обнаружить, что Enigma хранит большинство библиотек без добавления к основному имени номера выпуска. Это из-за поддержки совместимости форматов уровней Lua для версии 0.92. Допускается хранить один, и только один, выпуск каждой библиотеки без добавления к основному имени номера выпуска. Enigma загрузит эту версию из обычных уровней Lua, которые не предоставляют никакой информации о необходимом выпуске библиотеки.
Если файла библиотеки с полным основным именем файла нет, для запросов загрузки XML также будет загружен файл библиотеки по умолчанию, в названии которого отсутствует номер выпуска. Однако в будущем новая схема именования станет единственной, и каждая новая библиотека должна с самого начала следовать ей.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Ещё одним способом повторного использования кода, помимо файлов библиотек, являются многоуровневые файлы. Код, содержащийся в одном файле, может генерировать несколько уровней, называемых подуровнями, которые присутствуют в пакете в качестве автономных уровней. Конечно, данный способ является менее гибким, чем использование библиотек, поскольку другие файли не могут повторно использовать код. Но вы можете использовать данный способ, если вы написали большой объём специфического кода для сложного уровня, предусматривающего более двух вариантов (в противном случае эти варианты указывались бы как ‘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
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Теперь, когда вам уже известна структура формальной описательной XML-части уровня, вам, наверное, будет интересно узнать основные принципы структуры мира уровня Enigma. В этой главе мы объясним все основные понятия и термины, присутствующие в дальнейших главах, которые описывают видение уровня его автором.
Обратите внимание, что мы описываем характеристики нового API версии Enigma 1.10. В API предыдущих версий отсутствуют некоторые его характеристики, и в ряде аспектов он может отличаться от нового API.
4.1 Структура мира | ||
4.2 Описание объектов | ||
4.3 Способы взаимодействия | ||
4.4 Жизненный цикл уровня |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
В данном разделе мы рассмотрим уровень как отдельное произведение и как единое целое и опишем первоначальное размещение компонентов игрового мира и его динамическое поведение во время игры. Давайте подробно рассмотрим задействованные в нём объекты:
4.1.1 Очертания и координаты мира | Участки решётки и секции | |
4.1.2 Слои объектов | Покрытия, предметы, камни, актёры и др. | |
4.1.3 Мир как объект | Общие атрибуты и контроль | |
4.1.4 Неразмещаемые объекты | Другие объекты, такие как резиновые ленты и гаджеты | |
4.1.5 Игрок и инвентарь | Инь и Ян | |
4.1.6 Подконтрольные объекты | Объекты, временно находящиеся в подчинении |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Сыграв в несколько уровней, вы наверняка заметили, что каждый экран в игре разделён на квадратные секции — 20 по горизонтали и 13 по вертикали. Хотя игроку и бывает трудно определить очертания некоторых больших уровней, мир каждого уровня имеет форму прямоугольника. Тем не менее, некоторые части уровня игрок может никогда и не увидеть, потому что путь к ним ему преграждают каменные стены или водные барьеры.
При создании мира автор должен задать его размер в секциях. Заданные ширина и высота мира являются фиксированными и не могут быть изменены впоследствии. Типичный размер — 20x13 для одноэкранного уровня. Но здесь нет никаких ограничений. Можно создавать уровни даже меньшие, чем размер экрана. Обратите внимание, что при создании больших уровней необходимо учитывать тот факт, что один ряд или колонка клеток при прокрутке обычно делится между двумя экранами. Таким образом, уровень размером 2x2 экрана имеет размер 39x25 секций, 3x4 экрана — 58x49 секций и т. д.
Края секций образуют решётку, охватывающую мир уровня. Координаты левого верхнего угла мира уровня мы определяем как {0, 0}. Первая координата показывает смещение по горизонтальной оси вправо от начала координат, вторая — смещение по вертикальной оси вниз. В одноэкранном уровне координаты секции в нижнем правом углу — {19, 12}, в то время как координаты самого угла — {20, 13} (обратите внимание, что данная точка уже не относится к уровню).
Позицию актёра, например чёрного шарика, следует задавать двумя числами с плавающей запятой, например {1.5, 2.5} для актёра, расположенного в центре секции, находящейся на пересечении третьего ряда и второй колонки, считая от левого верхнего угла.
Но большинство объектов, таких как камни, можно разместить только в соответствии с фиксированными координатами, выраженными целыми числами. Даже если попытаться поместить камень в точку {1.5, 2.5}, он окажется в точке {1, 2}. Таким образом, если при задании координат учитываются только целые числа, мы говорим об участке решётки. Легко заметить, что позиция секции определяется по её верхнему левому углу. Верхняя и левая стороны и являются принадлежащими непосредственно данной секции, правая же и нижняя сторона принадлежат соседним секциям.
Наконец, рассмотрим более детально саму секцию. На одном участке решётки можно разместить покрытие, предмет, камень и даже несколько актёров. Комбинация всех объектов на одном участке решётки называется секцией. Эти комбинации объектов нередко объявляются один раз в так называемых определениях секции. Поскольку на многих участках решётки размещаются одинаковые комбинации объектов, эти секции можно использовать многократно.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
На каждом участке решётки можно поместить покрытие, предмет и камень. Но только по одному. Если поместить второй камень, он заменит первый. Покрытие, предмет и камень расположены определённым образом по отношению друг к другу: покрытие всегда располагается под предметом, а камень над ними. Таким образом, можно говорить о трёх слоях объектов: слой покрытий, слой предметов и слой камней.
К слою покрытий предъявляется уникальное требование. Каждый участок решётки должен иметь покрытие. Существует возможность задать секцию по умолчанию с покрытием по умолчанию, которое автоматически размещается на каждом участке решётки, на котором нет другого покрытия. Даже если разрушить покрытие, то есть убрать его, не указав ему замену, оно будет заменено покрытием по умолчанию.
У покрытий есть две элементарные характеристики: трение и сцепление. Трение замедляет движения актёров, а сцепление даёт возможность ускорять актеров с помощью мыши. Покрытие может также вызывать направленную силу, которая даёт игроку ощущение наклона. Следует также упомянуть, что покрытия могут гореть. Имеется целый набор атрибутов, с помощью которых можно управлять поведением огня.
На слое предметов находятся предметы, которые актёр может подобрать, и статичные предметы. К первой группе относятся такие предметы, как ключи, бананы и т. д. К статичным предметам относятся бомбы, фугасы, триггеры, отверстия, а также предметы, которые могут быть размещены только самой системой, такие как лазеры, огонь, пепел и т. д. Поскольку на каждом участке решетки может находиться только один предмет, шарик не может поместить другой предмет на статичном предмете. По этой причине нельзя преградить путь включенному лазеру, поместив на его пути предмет. Но при написании уровня можно добавить любой предмет по своему усмотрению на первоначальную секцию решётки.
Слой камней не требует долгих объяснений. На каждом участке решётки автор может разместить по одному камню из имеющегося набора камней. Естественно, большинство участков следует оставить незанятыми, чтобы по ним могли передвигаться актёры. И хотя границы большинства уровней окружены каменной стеной, делать это не обязательно. При отсутствии стены шарики будут отскакивать от физической границы мира.
Актёры располагаются в другом слое, который не привязан к решётке. Они могут быть размещены в любом месте. Актёры, проходящие сквозь камень, будут отображаться под камнем.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Трение, ломкость, режимы и прочее, режимы прокрутки.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Похоже, упущен по крайней мере один объект, который нельзя связать ни с конкретным местом на уровне, ни с одним из лежащих сверху слоев — резиновые ленты! На самом деле, кроме покрытий, предметов, камней и актеров в большом количестве существуют Остальные объекты, которые не требуется размещать на карте уровня. Кроме видимых резиновых лент и проволоки в мир можно добавить полезные гаджеты (gadgets), которые помогают составлять уровни простым добавлением новых элементов.
Все эти другие объекты полноценны, что следует из следующих глав. Но вам необходимо использовать метод мира add, чтобы добавить их, а также возможности, описанные в главах Ссылка на объект или Именование объектов, чтобы впоследствии обратиться к ним, потому что не существует способа обратиться к ним по их размещению.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
По своей концепции, Enigma — это игра для двух игроков. Тем не менее, в неё можно играть одному на единственном компьютере, переключая управление между двумя виртуальными игроками. Мы называем этих игроков Инь
и Ян
, поскольку первый игрок, как правило, управляет чёрным шариком, в то время как второй обычно управляет белым.
У каждого виртуального игрока есть собственный инвентарь, в котором можно хранить до 13 предметов. Самый левый предмет в инвентаре называется ‘активным’, поскольку щелчок мышью активирует этот предмет, а касание актёром чего-либо этим предметом может вызвать специальные действия.
Инвентари игроков существуют вне прямоугольного мира. Поэтому у любого предмета из инвентаря игрока позиция относительно мира недействительна, что выражается в возвращаемом на запрос ‘exists()’ значения ‘false’. С помощью дополнительного метода мира add можно добавлять предметы непосредственно в инвентари.
Хотя актёры закреплены за игроками, они достаточно самостоятельны и живут в одном из слоёв объектов (см. раздел Слои объектов). С игроками они связаны следующим образом:
каждый виртуальный игрок может управлять одним или несколькими принадлежащими ему актёрами любого типа. Это значит, что игрок Инь
не обязан всегда управлять чёрным шариком ac_marble, а с тем же успехом может управлять и белым шариком ac_pearl, лошадью ac_horse или любым другим произвольным набором объектов.
Владение и управление актёрами — это две большие разницы. Владение актёром означает, что каждый предмет, подобранный актёром, оказывается в инвентаре игрока, а предметы игрока могут использоваться всеми актёрами, находящимися в подчинении игрока. Управление актёром просто приводит к передвижению актёра под воздействием сил, которые регулирует игрок. Игрок может управлять актёром при этом не владея им. Другой актёр может находиться во владении игрока, но не управляться им, являясь, таким образом, пассивным актёром, которого могут передвигать толчки других актёров. Актёр может даже управляться обоими игроками, но владеть им может максимум один игрок.
Назначение игрокам актёров однозначно регулируется атрибутами актёров (см. раздел Атрибуты актёров).
Если в Enigma играет один пользователь, то он начинает игру, управляя игроком Инь
. Используя объекты Инь-Ян, он может переключиться на управление игроком Ян
и обратно. При игре одним игроком на сетевых уровнях автоматически добавляются предметы it_yinyang. Они позволяют произвольно переключаться между игроками. Камни st_yinyang ограничивают способность пользователей переключаться между управлением различными игроками.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Помимо объектов, принадлежащих игроку и входящих в его инвентарь, объекты могут временно входить в состав другого объекта. Наиболее очевидный пример — предмет, хранящийся в сумке it_bag. В качестве других примеров можно привести два камня сёгун st_shogun, расположенные на одном участке решётки, или, в течении короткого промежутка времени, камень, меняющийся местами с st_swap или st_pull.
В любом случае подконтрольный объект возвращает ту же позицию, что и его владелец. Даже если несколько объектов хранятся в сумке, которая в свою очередь хранится в другой сумке, все предметы вернут ту же позицию, что и самая внешняя сумка. Если эта сумка находится в инвентаре игрока, то все предметы вернут недействительную позицию.
Нельзя напрямую заставить объект входить в состав другого, разместив объекты в одном и том же месте, поскольку эта стандартная операция уничтожит старый объект и заменит его новым. По возможности, как в случае с сумкой, добавлять объекты в контейнер можно используя дополнительный метод мира add.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Теперь, после ознакомления с размещением объектов, пришло время разобраться, как выбрать тип объекта, указать его атрибуты и впоследствии ссылаться на него.
4.2.1 Тип объекта | ||
4.2.2 Ссылка на объект | ||
4.2.3 Именование объектов | ||
4.2.4 Атрибуты объектов |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
До настоящего времени мы говорили о таких типах объектов как покрытие ‘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".
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
После того, как автор распределит объекты по слоям, у него может появиться необходимость впоследствии к ним обратиться. При обратном вызове функций движок предоставляет ссылки на объекты-отправители. Но автор может обратиться к любому объекту решётки когда угодно, указав его позицию.
С помощью ссылки на объект особого типа Lua ‘object’ можно узнать текущее состояние и атрибуты объекта, модифицировать объект, посылать сообщения либо пользоваться любыми поддерживаемыми методами объекта.
Объекты можно группировать для эффективного осуществления типичных операций со всеми задействованными объектами. Например, если попробовать послать сообщение группе объектов, все объекты получат сообщение по очереди. Последовательность ряда объектов в группе является постоянной и гарантированно соблюдается при выполнении типичных операций.
Поскольку объекты могут прекратить своё существование, то следует иметь в виду, что ссылки также не являются постоянными. Есть возможность проверить существование ссылки на любой объект. Но во многих случаях действительность ссылки не имеет значения, поскольку Enigma 1.10 очень терпима по отношению к доступу к недействительным объектным ссылкам. Операции просто будут проигнорированы, а в ответ на запросы будут выданы величины по умолчанию.
На практике целесообразно запрашивать и хранить ссылки на объекты только на время локального вызова функции. Пока ваш код уровня обрабатывается по очереди без симуляции мира, которая даёт игроку возможность разрушить объекты действиями шарика, объекты будут прекращать своё существование только в соответствии с вашими прямыми указаниями.
Чтобы получить доступ к объекту при последующем вызове, к нему можно обратиться двумя способами. Во-первых, к объекту можно обратиться по его размещению. Но, поскольку многие объекты подвижны, их размещение не является постоянным. Следовательно, можно обратиться к объекту по имени (см. раздел Именование объектов).
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Чтобы длительное время иметь возможность для обращений к объекту, каждому отдельному объекту можно назначить имя. Для этого необходимо только установить для объекта атрибут ‘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#*"} |
Если разместить на карте три волшебные монеты и один телепорт, то шарик будет телепортирован к ячейке, на которой размещается ближайшая (на момент телепортации) к телепорту монета.
Во избежание непредвиденных проблем с недействительными ссылками на объект, некоторые наиболее важные объекты автоматически именуются системой, если автор уровня не предлагает свой вариант имени. Но эти уникальные имена не создают помех пользовательскому именованию объектов.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Один из ключевых факторов в обеспечении гибкости и разнообразия игры — это возможность тонкой настройки поведения объектов при помощи атрибутов. Автор уровня не ограничен фиксированным набором объектов определённого вида с предварительно заданными характеристиками.
Атрибут — это имя, строка с приписанным ей значением. Например, ‘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.1 Сообщения | Как попросить объект что-то сделать | |
4.3.2 Цель-действие | Автоматическая реакция на событие | |
4.3.3 Функция обратного вызова | Предусмотренные в Lua способы реакции на событие | |
4.3.4 Состояние объекта |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Открытую дверь можно сгенерировать, установив её атрибуты. Но как же сделать так, чтобы переключатель открывал дверь, когда до него дотронешься шариком? Он просто посылает двери сообщение ‘open’. Другие переключатели могут посылать сообщение ‘on’ ("включиться") лазеру, или ‘ignite’ ("загореться") объекту it_dynamite. При взрыве динамит в свою очередь пошлёт сообщение ‘ignite’ соседним участкам решётки.
Сообщение — это простая универсальная функция или, с точки зрения объекта-получателя и автора уровня, "метод". Он может принимать два аргумента — имя сообщения, строка и необязательное значение, Например:
mydoor:message("open") myboulder:message("orientate", NORTH) mydoor:open() myboulder:orientate(NORTH) |
Последние два примера — это стандартное сокращение первых двух.
Сообщения могут возвращать значения. Но большинство сообщений возвращают просто ‘nil’.
Сообщение можно послать любому объекту. Неподдерживаемые сообщения игнорируются без видимых результатов. В силу этого взрывающийся динамит может посылать сообщение ‘ignite’ соседним секциям, не имея сведений о том, могут ли объекты вообще загораться. Так что динамиту совершенно не важны получатели сообщения. В силу специфики сообщений их отправители и получатели полностью разъединены в том, что касается кода. Таким образом, автору уровня необходим только один метод, позволяющий посылать произвольные сообщения произвольным объектам.
Не посылайте сообщений при инициализации уровня. Переключатель можно запрограммировать на отправление двери сообщения ‘open’ при помощи цели-действия (см. раздел Цель-действие). С помощью функции обратного вызова Lua (см. раздел Функция обратного вызова) можно послать сообщение любому объекту во время его функционирования.
В главе Общие сообщения и последующих главах перечислены и описаны все сообщения.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
"Механизм цели-действия" — это классический объектно-ориентированный метод, позволяющий легко связывать объекты. Один объект активируется вызовом функции или событием (например, актёр, дотрагивающийся до камня, проходящий над предметом либо задействующий его). Вам необходимо просто связать этот объект с другим целевым объектом и приказать ему отправить сообщение о действии. Каждый раз, когда первый объект активируется, он будет посылать сообщение своей цели.
Задать "цель-действие" можно с помощью атрибутов ‘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’. С этого момента действия будут производиться не мгновенно, а с минимальной задержкой, таким образом, чтобы никогда не обрушить приложение. Но помните, что даже минимальная задержка, не выходящая за пределы соответствующего отрезка времени, может нарушить последовательность действий, приведя к неожиданным результатам.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Функции обратного вызова — самое мощное расширение парадигмы Цель-действие, которое только можно себе представить. Вместо целевого объекта в качестве получателя сообщения о действии можно использовать встроенную в 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 будут приведены примеры по-настоящему мощных функций обратного вызова с комментариями к каждой строке.
Описание дальнейшего использования и других аспектов обратных вызовов приведены в разделе Обратные вызовы и таймеры.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Ключевым понятием в объединении объектов, таких как переключатели и двери, являются простейшие механизмы состояния этих объектов. Большинство объектов описываются при помощи простых механизмов состояния, включающих только два состояния, такие как ‘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.1 Предварительная загрузка библиотек | ||
4.4.2 Принцип снимка | ||
4.4.3 Инициализация уровня | ||
4.4.4 Преобразование объекта | Передача сущности при трансформациях | |
4.4.5 Именованные позиции | Позиции как потомки уничтоженных покрытий | |
4.4.6 Обратные вызовы и таймеры | ||
4.4.7 Перезапуск уровня | ||
4.4.8 Условия завершения | Медитация и пары оксидов |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Большинство уровней содержит объекты, влияющие друг на друга. Переключатель может открывать и закрывать дверь при помощи "цели-действия" (см. раздел Цель-действие), шарик может нажать на триггер, а лазер может активировать лазерный переключатель или превратить молоток в меч. Естественно, вам необходимо знать, как можно получить желаемую конфигурацию объектов, чтобы они не претерпевали непредусмотренные изменения при инициализации уровня.
Принцип снимка — это простое практическое правило, на которое можно положиться, описывая уровень как снимок объектов в данный момент времени. Каждый объект необходимо настраивать так, как он должен быть настроен в данный момент времени. Все взаимодействия, имеющие место во время игры, не осуществляются при размещении объектов во время инициализации.
Например, если переключатель должен открывать и закрывать дверь и переключатель в начале уровня должен быть включен, а дверь открыта, объект необходимо описывать именно с такими атрибутами:
{"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. Обратите внимание, что такие объекты никогда не возвратят свой первоначальный подтип при запросе типа, поскольку они начинают своё существование как стандартные объекты.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Определившись с тем, что должно быть предварительно загружено и в каком состоянии должны появиться определённые объекты, наступило время взглянуть на то, как обрабатывается код уровня. Основная проблема состоит в том, чтобы гарантировать, что все связанные части заранее будут правильно настроены.
Перед выполнением первой строки кода мир существует просто как абстрактное вместилище, а не как массив-решётка, способный принимать объекты. Поэтому первые строки кода должны настроить все Глобальные атрибуты, необходимые значения которых отличаются от значений по умолчанию. Не смотря на то, что многие атрибуты впоследствии можно настроить и изменить даже во время выполнения, существуют такие атрибуты как ProvideExtralifes, который имеет смысл устанавливать, только перед созданием мира, или MaxOxydColor, который следует устанавливать перед его первым использованием. Мы рекомендуем выносить настройку всех глобальных атрибутов в самое начало уровня.
Вторым участком в коде уровня должно стать описание секций. Поскольку эти строки кода просто описание, они не размещают объекты в мире, зависят только от глобальных атрибутов и могут ссылаться друга на друга. Их перечисление в одном участке кода упрощает обзор кода и помогает избежать конфликтов зависимостей.
При использовании преобразований (см. разд. Преобразования) их описания следует поместить в следующий участок кода, поскольку они могут обращаться к секциям и глобальным атрибутам, а значит должны быть созданы перед созданием мира.
Главное выражение в любом уровне — это Создание мира. Оно задаёт размеры мира и располагает предметы в пределах решётки мира, согласно заданным описаниям секций. Хотя позже можно свободно добавить или изменить любой из объектов, размеры мира фиксированы и неизменны.
Поэтому следующие строки кода должны добавлять (add) другие объекты, рисовать дополнительные карты объектов или дополнять некоторые объекты. Общеупотребительным выражением подобного дополнения является метод shuffleOxyd. Для того, чтобы перекрасить и перемешать оксиды (st_oxyd), ему необходимо знать о всех оксидах на уровне. Ещё одним примером дополнения можно считать генерирование требуемого лабиринта, которое придаёт карте уровня форму лабиринта (см. раздел res.maze).
Код, следующий после создания мира, также может включать циклы или даже несколько локальных функций, которые должны быть объявлены перед включением в код. Пожалуйста, помещайте такие функции недалеко от места их использования и внутри того же участка кода.
Другой набор функций, которые могут быть добавлены — Функции обратного вызова. Мы советуем добавлять эти функции в последней секции кода, потому что они не вызываются непосредственно во время инициализации уровня.
Однако существует одно особое исключение. Функция обратного вызова postinit()
вызывается после того, как выполнился код инициализации уровня и завершились все последующие инициализации внутри движка. Если эта функция присутствует в уровне, то она выполняется непосредственно перед обработкой первого события перемещения мыши. А значит, при использовании этой функции можно быть уверенным, что все объекты находятся в своём окончательном состоянии. Перед использованием функции обратного вызова postinit()
её следует поместить после всего кода инициализации уровня, но перед остальными функциями обратного вызова, которые будут исполняться при последующих событиях.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Во время игры некоторые объекты Enigma преобразуются в другие объекты-потомки, такие как st_blocker/it_blocker, st_brake/it_brake, it_rubberband/ot_rubberband, it_hammer/it_sword и т. д.
Не смотря на то, что у объекта-потомка могут быть другие атрибуты, некоторые атрибуты, и в частности пользовательские атрибуты, необходимо сохранить. Имена объектов, их атрибуты цели и действия и все атрибуты, начинающиеся со знака подчеркивания (‘_’) — пользовательские атрибуты — наследуются объектом-потомком. Поэтому потомок объекта может точно так же получать сообщения в качестве цели, как и ранее существовавший на его месте объект, и к нему можно обратиться, используя его старое имя.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Многие камни можно перемещать и, даже если пользователь не может оттолкнуть их, большинство может поменяться местами с рядом стоящим обменным камнем. Актёры могут поднимать предметы, которые могут быть уничтожены во всепоглощающем пламени. Поэтому в большинстве случаев, предпочтительно помечать на полу якоря или формы. В каждой ячейке решётки существует объект покрытия, который намного стабильнее всех остальных объектов. Тем не менее, пользователь может сбросить в воду (fl_water) или бездну (fl_abyss) ящик (st_box), камень-головоломку (st_puzzle) или другой строительный камень. Более того, пользователь может оставить зажжённую бомбу (it_bomb), которая разрушит покрытие и оставит на его месте бездну (fl_abyss). В любом из этих случаев можно лишиться именованного якоря или важной части именованной области решётки, доступной в виде группы объектов.
Поэтому позиция каждого уничтоженного именованного покрытия остаётся в хранилище под своим именем. Чтобы получить позицию любого возможного покрытия, вместо именованных объектов следует запрашивать именованные позиции.
ti["~"] = {"fl_water", "water#"} ... function sweet() wo[po["water#*"]] = {"it_cherry"} end |
Следует отметить, что наряду со всеми позициями именованных объектов, удовлетворяющих шаблону на запрос именованной позиции будут выданы именованные позиции уничтоженных покрытий.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Наиболее гибким свойством, позволяющим автору обеспечить уникальное поведение уровня, являются функции обратного вызова.
Цели-действия убивают предупреждающий ot_timer.
Глобальные функции и функция res.*.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Теперь нам необходимо ответить на два важных вопроса: когда и при каких условиях уровень будет являться успешно завершённым? Как можно объявить медитативный уровень в противоположность стандартному уровню, основанному на открытии пар оксидов.
Флаг "медитации" по сути отсутствует как в информации о метаданных , так и среди глобальных атрибутов. Это означает, что формальное разграничение между двумя видами уровней отсутствует. Но есть два вида условий завершения. Оба проверяются постоянно и первое выполненное условие приводит к завершению уровня. Таким образом, 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
, не могут разбиться заранее, поскольку после того, как такой актёр будет уничтожен, в случае, если его нельзя воскресить, уровень будет перезапущен.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Теперь, после изучения основных принципов мира уровней Enigma, осталось лишь разобраться языком программирования, чтобы приступить к написанию своего первого уровня. Уровни Enigma написаны на языке Lua 5.1.4. Этот мощный язык позволяет вам писать самые сложные, динамические уровни и в то же время он достаточно понятен для того, чтобы написать простые стандартные уровни. Действительно, зачем в самом начале копаться в тонкостях языка?
Для второй версии Lua API (интерфейс программирования приложений Lua), используемой в Enigma 1.10, мы разработали более оптимальный подход к описанию уровней, который позволяет сделать его кратким и легко читаемым. Поэтому мы познакомим вас с этим API, разобрав несколько примеров, начиная с простого уровня и заканчивая самыми настоящими захватывающими динамическими уровнями Enigma. Свои первые эксперименты можно начать уже после ознакомления с первым примером и пояснениями к нему.
Чтобы вам было удобно, код Lua мы будем выделять цветом. Предопределённые (или встроенные) переменные и функции Lua мы будем выделять зелёным цветом. Встроенные строковые константы Enigma, такие как типы объектов или названия атрибутов и сообщений, выделяются синим. Названия переменных и их значения, относящиеся только к какому-то конкретному уровню, окрашены в пурпурный цвет.
После разбора примеров и краткого обзора мы подробно рассмотрим особенности API языка, ведь это как раз то, что каждый ожидает увидеть в справочном руководстве. Пожалуйста, обратите внимание на то, что Дополнительные возможности описаны в отдельной главе.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Давайте рассмотрим два простых одноэкранных уровня, в которых используются все основополагающие принципы. В то время как первый уровень, созданный только для примера, немного искусственный, второй — довольно динамичный уровень, который можно найти в пакетах уровней Enigma.
5.1.1 Простой пример | ||
5.1.2 Colored Turnstiles | Пакет уровней Enigma VIII, уровень #? |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Давайте взглянем на исходный код. В первые две колонки мы добавили номера строк, чтобы на них можно было ссылаться в этом разделе. Эти номера строк не являются частью самого исходного кода!
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> |
Получившийся уровень выглядит в игре следующим образом:
Теперь давайте рассмотрим код строка за строкой.
В строках с 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 для нашего громадного объекта мира.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Так как этот уровень входит в состав пакетов уровней 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 понятна. Теперь можно начать свои первые эксперименты с уровнями. Но чтобы создавать более интересные уровни, следует вернуться и прочесть следующие разделы с обзором и более сложными примерами.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
После того, как мы проанализировали первый уровень, настало время рассмотреть возможности API 2. Сделаем это, перечислив различные возможности и их использование на примерах.
5.2.1 Обзор типов | ||
5.2.2 Работа с позициями | ||
5.2.3 Работа с атрибутами | ||
5.2.4 Работа с объектами | ||
5.2.5 Работа с группами | ||
5.2.6 Работа с секциями и миром | ||
5.2.7 Работа с именованными позициями |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Для начала мы должны рассказать о специальных типах значений Enigma, отличных от стандартных типов Lua ‘nil’, ‘boolean’, ‘string’, ‘function’ и ‘table’:
Позиция в мире, которую можно задать координатами x и y.
po
; см. раздел Хранилище позицийЕдиный тип хранилища всех именованных позиций.
Объект Enigma, такой как камень, предмет, покрытие и т.п. Каждому объекту также соответствует позиция.
Список объектов.
no
; см. раздел NamedObjectsЕдиный тип хранилища всех именованных объектов.
DEFAULT
; см. раздел Атрибуты объектовЕдиный тип значений по умолчанию, который может использоваться вместо типа Lua ‘nil’ в определениях анонимных списков секций.
Описание для одной секции одного или нескольких объектов (покрытие, предмет, камень, актёр).
ti
; см. раздел Хранилище секцийЕдиный тип хранилища всех экземпляров секций.
wo
; см. раздел МирЕдиный тип мира, содержащий все объекты.
Список позиций.
Пожалуйста, обратите внимание на четыре представления: ‘po’, ‘no’, ‘ti’ и ‘wo’. Два из них уже можно было заметить в предыдущем разделе Примеры простых уровней на языке Lua. Это четыре переменные, которые должны быть настроены перед тем, как будет выполнен код уровня.
Обычно, для часто используемых переменных и функций API 2 использует двухсимволные имена, чтобы сократить код уровня и сделать его более читаемым. Авторам рекомендуется использовать либо одиночные символы, либо имена из трёх и более символов для имён локальных переменных.
В оставшейся части этого раздела мы предполагаем, что ‘obj’ — это объектная ссылка на камень, предмет или покрытие; это значит, что его тип — ‘object’. И предположим, что ‘pos’ — допустимая переменная типа ‘position’.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Подробности можно узнать в разделе Позиция.
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’.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Подробности можно узнать в разделе Объект.
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 — длина по умолчанию, например ‘1’ obj["color"] = nil — удаляет атрибут цвета — убирает цвет obj["length"] = DEFAULT — длина по умолчанию, например ‘1’ |
Любой атрибут объекта может быть сброшен к своему значению по умолчанию, что представляет собой операцию "удаления" атрибутов, присвоением ему значения ‘nil’ из Lua или значения ‘DEFAULT’ из Enigma.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Подробности можно узнать в разделе Объект.
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’, таким образом можно проверить, является ли объект камнем. Проверка особых подтипов может даже определить текущее состояние или другие атрибуты объекта, чтобы сообщить о его текущей классификации.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Подробности можно узнать в разделе Группа.
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) |
Ищет в группе объект, ближайший к указанному.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Подробности можно узнать в разделах Описание секций и объектов, Хранилище секций и Мир.
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’. Второй аргумент — код определения секции по умолчанию, который задаёт покрытие по умолчанию, которое будет установлено, если в секции нет другого объекта покрытия. Кроме того, своим размером этот код задаёт стандартную длину кода, используемого в последующей карте. Третий аргумент — карта, задаваемая анонимными списками строк. Размер мира задаётся максимальной длиной передаваемых строк и их количеством. Эти значения возвращаются вызовом.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Подробности можно узнать в разделе 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). Оператор "решётка" (‘#’) возвращает количество позиций в списке.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Пришло время показать настоящую мощь нового API. Давайте опять посмотрим на два существующих уровня. Сначала поиграйте на уровнях, чтобы исследовать их, а потом приступим к комментированию каждой строки исходного кода, чтобы понять, как реализовать собственные идеи уровней.
5.3.1 Color Maze | Пакет уровней Enigma VIII, уровень #19 | |
5.3.2 Weirdly Wired | Пакет уровней Enigma VIII, уровень #22 |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Давайте посмотрим на часть исходного кода уровня, написанную на 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".
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Чтобы заметить изменения в оформлении узора на покрытии и граничных камней панели, не говоря уже о различных способах динамической связи камней с помошью 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.1 Синтаксис и соглашения | Сокращения в описаниях синтаксиса | |
5.4.2 Значение и ссылка | Основные отличия между типами данных | |
5.4.3 Полиморфизм и перегрузка | Способы размещения объектов | |
5.4.4 Мнимые типы данных | Переосмысление типов данных |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
В следующих подразделах будут подробно описаны типы данных, операторы и методы для работы с ними, а также глобальные функции. Для эффективного описания будут использоваться определённый синтаксис и соглашения.
Следующие сокращения и полученные из них добавлением числа всегда представляют собой значение определённого типа:
Для описания синтаксиса операторов и методов для типов данных необходимо перечислить допустимые типы данных аргументов. Зачастую можно использовать любой из списка нескольких допустимых типов. В таких случаях эти типы будут заключены в угловые скобки (‘<’, ‘>’) и отделены друг от друга чертой (‘|’). Эти символы не входят в состав самих операторов или методов и их не следует помещать в код уровня. В то же время квадратные (‘[’, ‘]’) и фигурные (‘{’, ‘}’) скобки сохраняют своё символьное значение в Lua. Когда эти скобки появляются в синтаксисе, их следует включать в код. Например, следующая синтаксическая конструкция:
result = pos + <pos | obj | cpos | polist> |
позволяет записать в уровне любую из следующих строк:
result = pos + pos result = pos + obj result = pos + cpos result = pos + polist |
Однако такая синтаксическая конструкция как:
x = pos["x"] |
требует, чтобы в код включались квадратные скобки Lua. Конечно, имя переменной позиции и её значение могут быть какими угодно.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Одной из важнейших сторон типов данных Lua является различия между фактическим значением и ссылкой на него. Значения — это числа, булевы значения ‘true’ и ‘false’, строки и ‘nil’. Единственный ссылочный тип данных — список Lua.
Значения всегда постоянны. Их нельзя изменять. Значения назначаются переменным. Во время вычислений можно назначить переменной другое значение. Но первоначальное значение никогда не изменяется. Это должно быть понятно, если представить себе значение ‘true’ или, например, число ‘7’. Это справедливо и для строк, например "hello"
. Результатом объединения двух строк становится новая строка. Однако сами компоненты не меняются. Все методы, "изменяющие строку", в качестве возвращаемого значения выдают новую строку. Поэтому переменная, содержащая значение изначальной строки, всё ещё будет хранить неизменённое значение.
Природа списков совершенно другая. Они представляют собой вместилища данных. В Lua пользователю доступны только ссылки на эти вместилища. При добавлении или изменении значения внутри вместилища меняется не ссылка на список, а содержимое списка. Поэтому в двух переменных, содержащих ссылки на один и тот же список, после его изменения будут храниться ссылки на тот же изменённый список.
Для каждого нового типа данных мы будем указывать, является ли он значением или ссылкой. О влиянии такой особенности языка на работу уровня см. в Предупреждении.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Как вы, наверное, заметили, в качестве позиции (Позиция) во многих действиях можно использовать Объект. Это возможно благодаря тому, что объекты поддерживают большинство возможностей позиций. Объекты — это не позиции, но с ними можно обращаться подобным образом. Эта возможность называется ‘полиморфизм’ и помогает значительно упростить код. Чтобы понять, какие типы аргументов подходят в конкретном случае, в следующих подразделах нужно обратить особое внимание на синтаксис.
Lua ограничивает набор используемых операторов. Поэтому оператор сложения двух типов данных ‘+’ в зависимости от используемых данных предпримет различные действия. Результатом сложения двух позиций станет векторная сумма, а результатом сложения двух групп будет объединение групп. Такое различное использование одного и того же оператора называется ‘перегрузка’.
Перегрузка в сочетании с полиморфизмом может привести к неоднозначным ситуациям. Например, мы решили разрешить складывать позицию с объектом, результатом чего станет векторное добавление позиции объекта к указанной позиции. В то же время мы хотим, используя оператор ‘+’, объединить объект с существующей группой. Но, что получится в результате сложения двух объектов? Векторная сумма их позиций или объединение объектов в новую группу? Оба варианта имеют смысл и могли бы пригодиться. В таком случае мы отдали предпочтение первой возможности, поскольку операция вычитания, возвращающая векторное расстояние между объектами — очень полезная возможность. В любом случае, всегда можно принудительно использовать нужную операцию, преобразовав объект в позицию или группу. Чтобы чётко понять, что получится, внимательно прочтите приведённые правила синтаксиса.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Хотя мы добавили всего десять основных типов данных, которые описаны в последующих подразделах, API по разному обращается с одними и теми же типами данных в зависимости от их использования. Например, стандартное число Lua используется для описания состояния (state) объекта. В очень редких случаях состояние может отражать реальное число, как например состояние ot_counter. Для большинства объектов состояние — это одно значение из допустимого диапазона, которое в API представлено числом. Поэтому, говоря о значениях состояния (state), мы будем подразумевать мнимые типы данных.
Для мнимых типов данных API предоставляет Общие константы, которые записываются в верхнем регистре. Всегда используйте только эти константы, а не их эквиваленты в виде чисел или значений других типов. Использование констант делает код уровня более удобочитаемым и совместимым с последующими версиями игры, если вдруг нам понадобится изменить заданные значения или преобразовать мнимый тип данных к другому типу.
Стоит упомянуть один абстрактный тип данных, поскольку он может одновременно использовать два различных мнимых типа данных. Этот особый тип данных используется для описания направления (‘direction’) или ориентации (‘orientation’). Они мало отличаются друг от друга, но мы будем говорить об ориентации, если нас интересует просто список основных направлений в виде числовых значений, чтобы определиться с их выбором. Эти постоянные значения выдаются на запрос ориентаций (orientations).
Иногда значения нужны для вычисления смещений позиции. В этом случае мы подразумеваем направление (‘direction’) и используем значения позиции (Позиция) в качестве векторов смещения. Самые общеупотребительные значения в виде констант можно получить так, как это описано в подразделе Смещения направлений. Заметьте, что нет нужды приводить значения наших направлений к 1.
Заданная ориентация может быть приведена к значению направления с помощью списка преобразования ORI2DIR.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Позиция — это пользовательский тип данных Lua, добавленный в Enigma, чтобы работать с позициями мира, как описано в разделе Форма и координаты мира. Позиция представляет собой значение, а значит это константа. После того, как позиция была создана, её нельзя изменить, но с позициями можно работать, используя операторы. Если сложить значения двух позиций, то в результате будет получена позиция с новым значением.
В отличие от Объектов, позиции бессмертны и никогда не исчезают. Поэтому значения позиций можно хранить в глобальных переменных сколько понадобится. Значения постоянны и не изменяются, даже когда объекты, из которых они были получены, успели переместиться на другой участок решётки или были уничтожены.
Значения позиций не ограничиваются размерами мира. Каждая координата может принимать положительное, отрицательное и нулевое значение. Поэтому позиции можно использовать в вычислениях, чтобы, например, определить смещение между двумя другими позициями.
Позиции создаются посредством единого дескриптора хранилища позиций (см. раздел Хранилище позиций), который позволяет преобразовывать в позиции координаты, объекты, постоянные значения позиций. Этот дескриптор позволяет также получать существующие именованные позиции. Более того, позиции создаются косвенно, в виде возвращаемых значений многих операторов.
Примеры работы с позициями можно посмотреть в разделе Работа с позициями.
Ниже мы рассмотрим поддерживаемые операторы:
5.5.1 Сложение и вычитание позиций | Операторы ‘+’ и ‘-’ | |
5.5.2 Умножение и деление позиций | Операторы ‘*’ и ‘/’ | |
5.5.3 Смена знака позиции | Унарный оператор ‘-’ | |
5.5.4 Округление позиции до центра решётки | Оператор ‘#’ | |
5.5.5 Сравнение позиций | Равенство ‘==’ и неравенство ‘~=’ | |
5.5.6 Объединение позиций | Оператор ‘..’ | |
5.5.7 Доступ к координатам позиций | Операторы индексирования | |
5.5.8 Округление позиции до границ решётки | Метод ‘grid()’ | |
5.5.9 Существование позиции | Метод ‘exists()’ |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
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) |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
result = pos <*|/> number
result = number * pos
Скалярное произведение или частное вектора позиций. Возвращается значение позиции с обоими координатами, умноженными или поделенными на указанное число.
newpos = 3 * po(3, 4) -- = po(9, 12) newpos = po(2, 3) / 2 -- = po(1, 1.5) |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
result = -pos
Унарное скалярное умножение вектора позиции на ‘-1’. Возвращается новое значение позиции, обе координаты которого умножены на ‘-1’.
newpos = -po(3, 4) -- = po(-3, -4) |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
result = #pos
Округление вектора позиции до координат центра участка решётки. Возвращается новое значение позиции с координатами центра участка решётки с данной позицией.
newpos = #po(3, 4) -- = po(3.5, 4.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 |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
result = pos1 .. <pos2 | polist>
result = <pos1 | polist> .. pos2
Объединяет две позиции или позицию с существующим списком позиций (PositionList) в новый список позиций, содержащий все позиции в указанном порядке. Следует отметить, что эта операция ассоциативна, то есть не имеет значения, используются ли при множественном объединении скобки.
newpolist = po(3, 4) .. po(4, 4) |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
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 |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
result = pos:grid()
Возвращает новое значение позиции, которое указывает на координаты верхнего левого угла участка решётки, включающего позицию.
newpos = po(3.2, 4.7):grid() -- = 3, 4 newpos = po(-2.4, -5.0):grid() -- = -3, -5 |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
result = pos:exists()
Проверяет, входит ли позиция в состав мира, и возвращает ‘true’, если это так. В противном случае возвращается ‘false’.
Следует отметить, что метод ‘exists’ для Объекта сообщает о существовании объекта. Результат выполнения ‘po(obj):exists()’ для существующих объектов может быть и ‘false’. Например, это может произойти с предметами (см. раздел Предметы), в данный момент находящимися в инвентаре игрока. Предмет существует, но он не входит в состав мира. В то же время предметы, хранящиеся в сумке, которая лежит в пределах мира, вернут в качестве своих позиций позицию сумки.
boolean = po(3.2, 4.7):exists() |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Как описано в разделе Слои объектов, этот тип данных позволяет работать со всеми объектами мира. При обращении к объекту вы получаете не сам объект, а ссылку на него. Объект может изменять не только создатель уровня, но и игрок. Присваивание другого значения переменной удаляет ссылку, но не сам объект.
С другой стороны, из-за действий игрока объект может быть уничтожен, хотя ссылка на него всё ещё будет доступна через переменную Lua. Конечно, эта ссылка становится недействительной как только объект, на который она ссылается, будет удалён. Но в новом API такая недействительная ссылка, которая называется ‘нулевой’ (‘NULL’ reference), больше не становится фатальной. Любые попытки что-то записать по этой ссылке просто игнорируются. Поэтому ссылкам на объекты можно посылать сообщения, независимо от их действительности. Только перед записью, возможно, придётся предварительно проверить, существует ли объект, потому что при обращении к нулевым ссылкам будет получено значение ‘nil’.
У объектов есть атрибуты, к которым можно обратиться с помощью индексных методов Lua. В дополнение к отличительным атрибутам объекта можно добавлять свои собственные. Собственные атрибуты начинаются с приставки ‘_’ перед их именем.
Объекты мира создаются назначением позиции мира (см. раздел Мир) описания секции. Получить ссылку на соответствующий объект можно либо с помощью хранилища именованных объектов (см. раздел NamedObjects), функций (см. раздел Функции) либо другими методами, которые возвращают ссылку на отдельный объект.
Объекты поддерживают большую часть методов позиции (см. раздел Позиция) и в большинстве случаев могут напрямую использоваться в качестве позиций без явного преобразования. Для обоих типов отличаются только специализированные методы, такие как проверка на существование. Конечно расположение всех объектов мира ограничено пределами мира. Но помните, что переносные Предметы могут находиться в инвентаре игрока и возвращать позицию за пределами мира. Актёры всегда возвращают позиции, округлённые до координат содержащей их ячейки решётки. Это унаследованная возможность. Поскольку код Lua в любом случае не очень подходит для работы с перемещениями актёров, мы сохранили эту возможность округления.
Объекты поддерживают и стандартный набор операторов групп (см. раздел Группа), если одним из аргументов является группа, а другим объект.
Примеры работы можно посмотреть в разделах Работа с объектами и Работа с атрибутами.
5.6.1 Доступ к атрибутам объекта | Операторы индексирования ‘[]’ | |
5.6.2 Отправка сообщений объектам | Метод ‘:’ | |
5.6.3 Сравнение объектов | Равенство ‘==’ и неравенство ‘~=’ | |
5.6.4 Существование объекта | Метод ‘exists()’ | |
5.6.5 Уничтожение объекта | Метод ‘kill()’ | |
5.6.6 Определение типа объекта | Методы ‘kind()’ и ‘is()’ | |
5.6.7 Доступ к координатам объекта | Операторы ‘["x"]’, ‘["y"]’ | |
5.6.8 Сложение и вычитание объектов | Операторы ‘+’, ‘-’ | |
5.6.9 Округление объекта до центра решётки | Оператор ‘#’ | |
5.6.10 Объединение объектов | Оператор ‘+’ | |
5.6.11 Пересечение объектов | Оператор ‘*’ | |
5.6.12 Разность объектов | Оператор ‘-’ | |
5.6.13 Звук объекта | Метод ‘sound()’ |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
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"}) |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
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) |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
result = obj1 <==|~=> obj2
Сравнение двух значений объектов. Два значения объектов равны, если оба ссылаются на один и тот же существующий объект мира В противном случае они не эквивалентны.
bool = obj1 == obj1 -- = true bool = obj1 == obj2 -- = false, if two different objects bool = obj1 ~= obj2 -- = true, if two different objects |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
result = -obj
result = obj:exists()
Проверяет действительность ссылки на объект. Возвращает истину, если объект всё ещё существует, в противном случае, для нулевых ссылок на объект (‘NULL’), возвращает ложь.
bool = -obj bool = obj:exists() |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
obj:kill()
Сразу же уничтожает объект. Стоит заметить, что ни в коем случае нельзя уничтожать объект-отправитель для действия обратного вызова. Если уничтожить отправителя всё же необходимо, то, согласно описанию в разделе Цель-действие, добавьте атрибут safeaction.
obj:kill() |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
result = obj:is("kind")
result = obj:kind()
Эти методы позволяют определить и получить Тип объекта.
bool = obj:is("st_chess") string = obj:kind() |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
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() |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
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 |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
result = #obj
Округление вектора позиции объектов до координат центра участка решётки. Возвращается новое значение позиции, с координатами центра участка решётки с данной позицией.
newpos = #obj -- e.g. po(3.5, 4.5) |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
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) |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
result = obj * group
result = group * obj
Если объект входит в группу, то будет возвращён набор, состоящий только из него, в противном случае будет возвращена пустая группа.
newgroup = obj1 * grp(obj1, obj2) -- = grp(obj1) newgroup = grp(obj2) * obj1 -- = grp() |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
result = obj - group
result = group - obj
В первом случае, если объект не входит в группу, то будет возвращён набор, содержащий только сам объект, или же будет возвращена пустая группа. Во втором случае новая группа содержит все элементы старой без самого объекта. Порядок элементов не меняется.
newgroup = obj1 - grp(obj2, obj1) -- = grp() newgroup = grp(obj1, obj2) - obj1 -- = grp(obj2) |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
result = obj:sound("name", volume)
С позиции объекта проигрывается звук с указанным именем. По умолчанию громкость устанавливается в значение ‘1’.
obj:sound("quake") obj:sound("quake", 2) |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Группа — это сортированные набор Объектов. Каждый объект может содержаться в группе только в одном экземпляре. В отличие от списка Lua, группа — тип данных с постоянным значением. Группу нельзя изменить, после того как она была получена. Но группы могут участвовать в вычислениях и к ним могут применяться такие операторы, как объединение, пересечение и разность, в результате которых будут получены другие значения групп.
Хотя группы, как значения, могут храниться длительное время, при хранении группы дольше, чем для использования в выражении обратного вызова, следует быть внимательным. Группы, содержат ссылки на объекты, а объекты могут быть уничтожены. Поэтому у ранее полученной группы, при последующем её использовании в выражении обратного вызова могут быть недействительные ссылки на объекты. Если группе просто отсылается сообщение, то это не так критично, но в других случаях из группы следует удалять недействительные элементы (см ниже).
Группа создаётся перечислением входящих в неё объектов в качестве аргументов Функции ‘grp()’, получением объектов из хранилища NamedObjects или в результате других методов и вычислений.
Поскольку группы постоянны, то порядок входящих в них объектов тоже постоянен. Все операции, которые генерируют новые группы, по мере возможностей стараются сохранять этот порядок. Например, объединение двух групп берёт в заданной последовательности объекты первой группы и добавляет к ним объекты из второй группы, отсутствующие в первой, в порядке их следования.
Все операции с группами, которые возвращают новые группы, убирают из своих результатов недействительные на момент выполнения операции нулевые ссылки на объект. Эту возможность можно использовать, чтобы очистить группу при помощи функции ‘grp()’.
Любое сообщение, посланное группе, перенаправится всем её элементам в порядке их следования. Запись атрибутов группы представляет собой последовательность циклов записи атрибутов для каждого элемента группы.
Но можно пройти по элементам группы и обратиться к каждому из них при помощи операции доступа по индексу. Поддерживаются и несколько особых методов для управление группой на низком уровне, такие как перемешивание, выделение подгруппы, сортировка и т.п.
Примеры работы можно посмотреть в разделе Работа с группами.
5.7.1 Отправка сообщений группам | Метод ‘:’ | |
5.7.2 Запись атрибутов в группу | Операторы индексирования ‘[]’ | |
5.7.3 Сравнение групп | Равенство ‘==’ и неравенство ‘~=’ | |
5.7.4 Размер группы | Оператор ‘#’ | |
5.7.5 Доступ к элементам группы | Операторы индексирования ‘[]’ | |
5.7.6 Обход элементов группы в цикле | ‘for obj in group do ... end’ | |
5.7.7 Объединение групп | Оператор ‘+’ | |
5.7.8 Пересечение групп | Оператор ‘*’ | |
5.7.9 Разность групп | Оператор ‘-’ | |
5.7.10 Перемешивание группы | Метод ‘shuffle()’ | |
5.7.11 Сортировка группы | Метод ‘sort()’ | |
5.7.12 Выделение подгруппы | Метод ‘sub()’ | |
5.7.13 Ближайший объект из группы | Метод ‘nearest()’ |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
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() |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
group["attributename"] = value
group:set({attributename1=value1, attributename2=value2,...})
Задаёт значения атрибутов, описанных в следующих разделах, или пользовательских атрибутов для всех объектов группы. Метод ‘set’ позволяет изменить одновременно несколько атрибутов. Если ссылка на объект недействительна, то запись атрибутов не производится. Групповое чтение атрибута невозможно — доступ на чтение к элементу с указанным индексом — это перегруженная операция, доступная только для элементов группы.
group["color"] = BLACK group.color = BLACK group:set({target=mydoor, action="open"}) |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
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, если содержимое объектов отличается |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
result = #group
Количество элементов в группе. Недействительные нулевые ссылки на объект тоже учитываются.
number = #grp(obj1, obj2) -- = 2 for i = 1, #group do obj = group[i] ... end |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
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 |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
for obj in group do ... end
Обход всех объектов группы в цикле. Тело цикла выполняется по порядку для всех объектов, включая недействительные нулевые ссылки на объект.
for obj in group do obj:toggle() end |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
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) |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
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) |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
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) |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
result = group:shuffle()
Возвращает новую группу с теми же элементами, но в другой последовательности, выбранной случайным образом. Стоит отметить, что после вызова этого метода из полученной группы удаляются все недействительные нулевые ссылки на объект.
newgroup = grp(obj1, obj2) |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
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() |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
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) |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
result = group:nearest(obj)
Возвращает объект группы, который находится ближе всех к позиции объекта, переданного по ссылке. При расчёте расстояний координаты позиции актёра не округляются. Если два объекта находятся на одном и том же расстоянии от объекта, переданного по ссылке из них случайным образом выбирается один.
newobject = grp(obj1, obj2, obj3):nearest(obj4) |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Тип данных NamedObjects используется только одним объектом, единым хранилищем именованных объектов. При именовании объектов (см. раздел Именование объектов), это хранилище записывает его имя и позволяет позже обратиться к нему по имени.
Поскольку NamedObjects един, то нельзя создавать другие его экземпляры. При загрузке уровня экземпляр данного объекта хранится в глобальной переменной ‘no’.
5.8.1 Запрос к хранилищу NamedObjects | Оператор индексирования ‘[]’ для чтения | |
5.8.2 Именование объектов NamedObjects | Оператор индексирования ‘[]’ для записи |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
result = no["name"]
Запрос одного или нескольких объектов из хранилища. Если в имени не задан шаблон, то возвращается значение типа Объект. Это либо уникальный объект с указанным именем, либо, если объекта с заданным именем нет, — недействительный нулевой (‘NULL’) объект.
Если в запрашиваемом имени есть символы шаблона, такие как звёздочка ‘*’ или знак вопроса ‘?’, то возвращается Группа, содержащая все объекты с подходящими именами. Звёздочке соответствует ни одного, один или несколько произвольных символов. Знаку вопроса соответствует один произвольный символ. Оба символа шаблона могут использоваться в любом месте строки и в любом количестве. В любом случае возвращаемым значением всегда будет Группа. В группе может содержаться несколько объектов, один объект или вообще не быть ни одного объекта, если не существует объектов, удовлетворяющих шаблону.
obj = no["mydoor"] — точное совпадение имени group = no["mydoors#*"] — любой суффикс group = no["mydoor?"] — суффикс из одного символа group = no["mydoors?#*"] — соответствует, например, "mydoorsA#123435", "mydoorsB#1213" |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
no["name"] = obj
Доступ по индексу для записи в хранилище позволяет назначить объекту имя или переименовать его. Заметьте, что назначить объекту имя или переименовать его также можно с помощью записи атрибута Объекта. Имя объекта хранится в атрибуте "name"
. Оба способа именования объекта абсолютно эквивалентны.
no["myobject"] = obj |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
PositionList — это упорядоченный набор, или список, позиций (см. раздел Позиция). Как и Группа, это вместилище — постоянный тип данных, который нельзя изменить. Но, объединяя существующие списки и отдельные позиции, можно легко создавать новые списки позиций.
Важное отличие от группы состоит в способности списка позиций хранить позицию одновременно в нескольких местах. Благодаря этому список позиций подходит для описания путей, даже замкнутых или с пересекающимися сегментами.
Поскольку позиции (см. раздел Позиция) это значения, которые никогда не становятся недействительными, однажды созданный список позиций никогда не меняется и не становится недействительным. По своей сути их значения всегда существуют. Поэтому они лучший выбор для вместилища долгосрочного хранения.
Группу можно легко преобразовать в список позиций, воспользовавшись единым хранилищем позиций, с помощью простого выражения ‘po(group)’. А все объекты (см. раздел Объект) общего типа, расположенные по контуру, описываемому списком позиций, можно получить с помощью таких функций (см. раздел Функции), как ‘st(polist)’, ‘it(polist)’ и ‘fl(polist)’.
Поскольку в каждой ячейке решётки должен быть один объект покрытия, то заданный список позиций можно преобразовать в группу покрытий без потери информации. Теперь к покрытиям можно применять любые методы группы, например перемешивание, сортировка, выделение подгруппы и т.п. Наконец, полученную группу можно преобразовать обратно к постоянному списку позиций. Естественно, при преобразовании сохраняется порядок элементов.
Для упрощения преобразования и дополнения списка позиций поддерживается несколько дополнительных операторов, работающих с позициями.
Следует отметить, что в отличии от группы (см. раздел Группа), этот тип данных не может храниться прямо в атрибуте объекта (см. раздел Объект). Однако всегда можно хранить в атрибутах группу покрытий. Если есть вероятность, что покрытия будут уничтожены, то, возможно, им следует присвоить имена, как описано в разделе Именованные позиции.
Примеры работы можно посмотреть в разделе Работа с именованными позициями.
5.9.1 Сравнение PositionList | Равенство ‘==’ и неравенство ‘~=’ | |
5.9.2 Размер PositionList | Оператор ‘#’ | |
5.9.3 Доступ к элементам PositionList | Операторы индексирования ‘[]’ | |
5.9.4 Объединение PositionList | Оператор ‘..’ | |
5.9.5 Смещение PositionList | Операторы ‘+’ и ‘-’ | |
5.9.6 Масштабирование 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, различный порядок |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
result = #polist
Количество позиций в списке.
number = #(po(2,3) .. po(5,7)) -- = 2 for i = 1, #polist do pos = polist[i] ... end |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
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 |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
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) |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
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) |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
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) |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Тип данных ‘позиция’ используется только в одном случае — единым хранилищем именованных позиций. Кроме работы с именованными позициями он предоставляет полезные преобразования других типов данных к типам, основанным на позиции.
Поскольку хранилище позиций едино, то нельзя создать его новый экземпляр. Хранилище становится доступным при загрузке уровня, а его элементы хранятся в глобальной переменной ‘po’.
Хранилище позиций — это расширение хранилища именованных объектов (см. раздел NamedObjects). При любом присвоении объекту имени (см. раздел Именование объектов) в этом хранилище сохраняется имя объекта и позже к текущей позиции объекта можно обратиться по имени. Но даже если объект покрытия будет уничтожен, его позиция будет храниться как именованная позиция (см. раздел Именованные позиции). Конечно, можно самостоятельно присваивать позициям имена, но у позиций существующих именованных объектов при определении конфликтов имён всегда будет преимущество.
Примеры работы можно посмотреть в разделе Работа с именованными позициями.
5.10.1 Запрос к хранилищу позиций | Оператор индексирования ‘[]’ для чтения | |
5.10.2 Доступ к элементам хранилища позиций | Оператор индексирования ‘[]’ для записи | |
5.10.3 Преобразование позиций | Функция ‘po()’ | |
5.10.4 Преобразование PositionList | Функция ‘po()’ |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
result = po["name"]
Запрос из хранилища одной или нескольких позиций. Если в имени не используются символы шаблона, будет возвращено значение позиции (см. раздел Позиция) конкретного объекта с заданным именем, если тот существует. Если объект с таким именем не существует, то будет возвращена последняя позиция, хранившаяся под этим именем. Если позиция не существует, то будет возвращено значение ‘nil’.
Если в запрашиваемом имени есть символы шаблона, такие как звёздочка ‘*’ или знак вопроса ‘?’, то возвращается PositionList, содержащий все позиции с подходящими именами. Звёздочке соответствует ни одного, один или несколько произвольных символов. Знаку вопроса соответствует один произвольный символ. Оба символа шаблона могут использоваться в любом месте строки и в любом количестве. В любом случае возвращаемым значением всегда будет PositionList. В списке может содержаться несколько позиций, одна позиция или вообще не быть ни одной позиции, если не существует позиций, удовлетворяющих шаблону.
pos = po["mydoor"] — точное совпадение имени polist = po["mydoors#*"] — любой суффикс polist = po["mydoor?"] — суффикс из одного символа polist = po["mydoors?#*"] — соответствует, например, "mydoorsA#123435", "mydoorsB#1213" |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
po["name"] = obj
Доступ к хранилищу по индексу для записи позволяет присвоить имя или переименовать позиции. Следует отметить, что имени, уже ссылающемуся на существующий Объект, нельзя присвоить новую позицию. Попытка такого доступа на запись будет просто проигнорирована.
po["mypos"] = pos |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
result = po(<obj | pos | {x, y} | x, y >)
Преобразовывает свой параметр в новое значение позиции.
pos = po(pos2) pos = po(obj) pos = po({2, 4}) pos = po(3, 7) |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
result = po(group | {pos1, pos2, pos3})
Преобразовывает указанную группу или список в новый PositionList, который содержит позиции всех действительных Объектов группы или списка в той же последовательности.
polist = po(group) polist = po({po(3, 7), po(2, 6)}) polist = po({}) — пустой список позиций |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Секция — это описание одного или нескольких объектов, которые должны располагаться в одной и той же ячейке решётки. Отдельный объект можно описать простым описанием объекта, анонимным списком 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"} — одно из двух значимых описаний преобразовано к типу секции |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Тип данных ‘секция’ используется только в одном случае — единым хранилищем описаний секций и объектов (см. раздел Описание секций и объектов). Кроме работы с секциями он предоставляет полезные преобразования основанных на списках описаний объектов к секциям.
Поскольку хранилище секций едино, то нельзя создать его новый экземпляр. Хранилище становится доступным при загрузке уровня, а его элементы хранятся в глобальной переменной ‘ti’.
В хранилище секции хранятся по заданной кодовой строке. Кодовые строки могут быть любого размера. Из-за ограничений Lua они должны состоять только из печатных 7-битных символов кодовой таблицы ASCII.
Каждому коду можно назначить секцию только один раз. Повторное назначение приведёт к ошибке. С одной стороны это позволяет оптимизировать внутреннюю реализацию, а с другой стороны попытка переназначить уже назначенный код — самая распространённая ошибка при написании уровня, о которой необходимо сообщить автору.
Примеры работы можно увидеть в разделе Работа с секциями и миром.
5.12.1 Запись секций | Присвоение секции связанному с ней коду | |
5.12.2 Запрос секции | Получение секции по ключу | |
5.12.3 Преобразование секции | Преобразование описания объекта к секции |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
ti["key"] = <tile|odecl>
Основанный на индексе доступ к хранилищу на запись, который позволяет назначить указанному ключу секцию или основанное на списке описание объекта, которое автоматически преобразуется к секции. Ключ должен представлять собой уникальную строку. Уникальную в том смысле, что ключу, которому уже была назначена другая секция, больше нельзя будет назначить новую секцию.
ti["#"] = tile ti["$"] = {"st_chess"} ti["$"] = {"st_switch"} — ошибка переназначения ключа ti["anykey"] = {"st_chess"} |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
result = ti["key"]
Запрос секции, которая была назначена указанному ключу. Если в ключе ещё не хранится секция, то будет возвращено значение Lua ‘nil’. Следует отметить, что хранилище секций, в отличие от хранилищ именованных объектов и позиций, не работает с шаблонами. Звёздочка ‘*’ и знак вопроса ‘?’ — такие же ключи, как и остальные символы.
tile = ti["#"] |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
result = ti(odecl)
Преобразовывает основанное на списке описание объекта к новому значению секции.
tile = ti({"st_chess"}) tile = ti{"st_chess"} — эквивалент в синтаксисе Lua |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Тип данных ‘мир’ существует только в одном экземпляре, это ещё один единый объект. После загрузки уровня ссылка на этот единый объект хранится в глобальной переменной Lua ‘wo’. Поскольку данный объект един, то нельзя создать его новый экземпляр.
Однако хотя единый объект ‘wo’ уже существует после загрузки уровня, мир всё ещё окончательно не определён. Уже в первой строке кода Lua можно использовать Глобальные атрибуты. Но мир фактически становится завершённым после создания мира (см. раздел Создание мира). После этого вызова у мира появляются вполне определённые размеры и он заполняется начальным набором объектов, с которыми, начиная с этого момента, уже можно работать.
5.13.1 Создание мира | Наполнение мира объектами | |
5.13.2 Установка секций мира | Оператор индексирования ‘[]’ для записи | |
5.13.3 Установка глобальных атрибутов | Оператор индексирования ‘[]’ для записи | |
5.13.4 Чтение глобальных атрибутов | Оператор индексирования ‘[]’ для чтения | |
5.13.5 add | Добавляет к миру или в инвентарь отдельные объекты | |
5.13.6 drawBorder | Рисует прямоугольную границу мира, используя указанную секцию | |
5.13.7 drawMap | Располагает в мире объекты, заданные списком кодов секций | |
5.13.8 drawRect | Заполняет прямоугольную область указанной секцией | |
5.13.9 world floor | Получение поверхностей | |
5.13.10 world item | Получение предметов | |
5.13.11 shuffleOxyd | Правила перемешивания оксидов | |
5.13.12 world stone | Получение камней |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Как только будут установлены все параметры и объявлены все секции, наступает время создать мир уровня со всеми его объектами. Это делается с помощью следующего конструктора, у которого есть две разновидности.
width, height = wo(topresolver, defaultkey, map)
width, height = wo(topresolver, libmap)
width, height = wo(topresolver, defaultkey, width, height)
ti
| resolver | localresolverУ любой секции есть код, который необходимо преобразовать к её описанию. Это можно сделать либо с помощью хранилища секций ‘ti’, либо с помощью библиотеки Преобразования или используя функцию локального настраиваемого преобразования (см. раздел Настраиваемое преобразование). Этот аргумент задаёт преобразование наивысшего уровня, которое будет вызываться раньше остальных.
Строка, которая определяет код, присваиваемый по умолчанию. Он используется, если не задан ни один код, и присваивается секции при отсутствии покрытия. Количество символов этого кода определяет размер кода на карте.
Список строк. Каждая строка описывает ряд секций, используя их коды. Если задана карта, размеры мира определяются по самой длинной строке и количеству этих строк.
Карта, полученная с помощью библиотеки libmap.
Полученный вместо карты, этот аргумент задаёт желаемую ширину мира.
Полученный вместо карты, этот аргумент задаёт желаемую высоту мира.
w, h = wo(ti, " ", 20, 13) w, h = wo(resolver, " ", { " ", ... " "}) w, h = wo(ti, mylibmap) |
Этот конструктор мира может быть вызван лишь единожды. Каждый последующий вызов приводит к ошибке. Этот вызов устанавливает размеры мира согласно заданным значениям, которые можно узнать с помощью его двух возвращаемых значений. Также размер мира позже может быть получен через атрибуты мира Width и Height.
Мир без карты заполняется секциями по умолчанию. Секциями по умолчанию также заполняются строки полученной карты, которые короче остальных. Каждой секции, для которой не задан объект покрытия, будет предоставлена секция покрытия по умолчанию.
Каждый код преобразуется к описанию его секции после прохождения цепи преобразований. В качестве параметра этого вызова выступает преобразование высшего уровня. Если это ‘ti’, то цепь состоит только из одного элемента и берётся описание секции, хранящееся в хранилище секций в указанном ключе. В противном случае будут применяться преобразования, которые описаны в разделе Цепь преобразования.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
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"] |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
wo["attritbutename"] = value
Доступ для записи к строковым индексам, позволяющий изменить глобальные атрибуты. Изменить можно только существующие атрибуты, для которых предусмотрена возможность записи. Обратите внимание, что для получения необходимого эффекта некоторые атрибуты необходимо установить перед созданием мира.
wo["ConserveLevel"] = true |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
var = wo["attritbutename"]
Доступ для чтения к строковым индексам, позволяющий получить значения глобальных атрибутов. Прочесть можно только существующие атрибуты, для которых предусмотрена возможность чтения. Обратите внимания, что некоторые атрибуты возвращают правильные значения лишь сразу после создания мира.
var = wo["IsDifficult"] |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Добавляет к миру Остальные объекты, либо добавляет переносные объекты в инвентарь или другой объект-контейнер.
wo:add(tile_declarations)
wo:add(target, tile_declarations)
Одно или несколько описаний объектов, представленных в виде секций или анонимных списков.
‘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) могут быть добавлены только портативные Предметы. Никакая другая цель, кроме перечисленных выше, на данный момент не добавляет объекты этим методом.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Рисует границу из заданных секций вокруг заданного прямоугольника.
wo:drawBorder(upperleft_edge, lowerright_edge, <tile | key, resolver>)
wo:drawBorder(upperleft_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) |
Рисует четыре линии толщиной в одну секцию с использованием заданной секции. Это значит, что в каждой ячейке прямоугольника создаётся по одному экземпляру каждого объекта из описания секции. В вырожденном случае прямоугольник может превратиться в линию.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Даже если мир инициализирован картой при создании мира (см. раздел Создание мира), иногда удобно иметь возможность рисовать карты внутри существующей либо во время инициализации, либо внося динамические изменения уровня с помощью функции обратного вызова (см. раздел Функция обратного вызова). Безусловно, основное назначение ‘drawMap’ — рисование повторяющихся шаблонов.
wo:drawMap(resolver, anchor, ignore, map, [readdir])
wo:drawMap(resolver, anchor, libmap-map, [readdir])
Преобразование, которому должны быть перенаправлены необработанные запросы. В качестве последнего преобразования цепи преобразований может выступать ‘ti’.
Позиция точки привязки, где должна располагаться верхняя левая секция карты.
Кодовая строка секции, которая должна быть проигнорирована. Эта кодовая строка обязательна, даже если она не используется на карте.
Список строк. Каждая строка описывает ряд секций, используя их коды.
Если используемая карта была создана с помощью libmap, строку ‘ignore’ можно опустить. Тогда вместо неё будет игнорироваться код по умолчанию.
Необязательный аргумент, который изменяет направление карты по отношению к миру. Значением этого аргумента может быть любая из констант, описанных в разделе Вращение и отражение карт.
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() |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Заполняет прямоугольник указанной секцией.
wo:drawRect(upperleft_edge, lowerright_edge, <tile | key, resolver>)
wo:drawRect(upperleft_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) |
Замкнутый прямоугольник заполняется указанной секцией. Это значит, что в каждой точке прямоугольника, в том числе и внутри контура, помещается каждый объект указанной секции. В вырожденном случае прямоугольник может превратиться в линию.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Возвращает покрытия в указанной позиции или позициях.
result = wo:fl(<pos| {x, y}|x, y| obj | group| polist>)
Этот метод мира идентичен глобальной функции fl.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Возвращает предметы в указанной позиции или позициях.
result = wo:it(<pos| {x, y}|x, y| obj | group| polist>)
Этот метод мира идентичен глобальной функции it.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Изменение распределения цветов оксидов (st_oxyd) делает каждый уровень, кроме, естественно, уровней для медитации, немного более непредсказуемым. При каждом начале уровня он выглядит немного по-разному и пользователю приходится решать изменившийся уровень. Это продлевает время, в течение которого уровень доставляет развлечение игроку. Поэтому вызов этого метода можно найти в большинстве уровней.
Многие уровни просто вызывают этот метод, не передавая ему аргументов. Это приводит к перераспределению цветов всех оксидов (st_oxyd), которые не исключены атрибутом ‘noshuffle’.
Но иногда уровням нужно контролировать перераспределение, то ли для того, чтобы гарантировать решаемость уровня, то ли чтобы просто убедиться в справедливом распределении оксидов. Представьте уровень, в котором в каждом углу находятся по два камня st_oxyd. Если игроку повезёт и он добьётся распределения, при котором в каждом углу будет по два оксида одинакового цвета, решение уровня становится банальным. В другом уровне может быть проход, через который шарик может пройти лишь несколько раз. При 5 и более оксидах с каждой стороны прохода вам нужно быть уверенным в том, что шарику не потребуется пересекать проход больше раз, чем возможно. Обе ситуации можно проконтролировать, используя в качестве аргументов к этому методу подходящие правила.
wo:shuffleOxyd(rules)
Либо ни одного правила, либо столько, сколько пожелаете, разделённых запятыми.
Каждое правило — список с подмножеством перечисленных элементов. Элемент group1 обязателен. Все остальные необязательны и могут добавляться в любых сочетаниях.
Описание объектов-оксидов, которые входят в первую группу. Допустимыми дескрипторами считаются: либо группа, либо отдельная ссылка на объект, либо строковый спецификатор, преобразующий к одному или, с помощью маски, к нескольким объектам-оксидам.
Описание объектов-оксидов, которые входят во вторую группу. Допустимыми дескрипторами считаются: либо группа, либо отдельная ссылка на объект, либо строковый спецификатор, преобразующий к одному или, с помощью маски, к нескольким объектам-оксидам.
max =
numberМаксимальное количество пар оксидов.
min =
numberМинимальное количество пар оксидов.
circular = true
Избегать появление любых пар смежных оксидов из ‘group1’. Также избегать появления пары, состоящей из первого и последнего оксидов из ‘group1’.
linear = true
Избегать появление любых пар смежных оксидов из ‘group1’.
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. Во избежание ситуации, когда с левой и правой стороны появится по две пары оксидов, мы добавляем правило минимума, которое заставляет появляться две разнесённые в пространстве пары оксидов.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Возвращает камни с указанной позиции или позиций.
result = wo:st(<pos| {x, y}|x, y| obj | group| polist>)
Этот метод мира идентичен глобальной функции st.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Помимо всех возможностей, зависящих непосредственно от контекста, представляющего собой значения, и реализованных в виде операторов и методов определённых типов данных, существует и немало других. Они не зависят от контекста или хотя бы в одной разновидности принимают в качестве входных параметров только стандартные контекстнонезависимые типы данных Lua. Поэтому такие задачи реализуются в виде простых функций.
5.14.1 assert_bool | Если условие не выполняется, то выдаётся ошибка. | |
5.14.2 assert_type | Если переменная не принадлежит указанному типу, то выдаётся ошибка. | |
5.14.3 cond | Тернарный оператор, обёртка для if-then-else. | |
5.14.4 etype | Расширенная функция, возвращающая обычные и пользовательские типы данных. | |
5.14.5 fl | Получение поверхности | |
5.14.6 grp | Создание группы из объектов-параметров | |
5.14.7 it | Получение предмета | |
5.14.8 ORI2DIR | Преобразование положения к направлению | |
5.14.9 random | Генератор случайных чисел | |
5.14.10 st | Получение камня | |
5.14.11 usertype | Информация о пользовательских типах данных Enigma |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Если условие не выполняется, то функция ‘assert_bool’ выдаёт ошибку.
assert_bool(condition, message, level)
Булево (или приводимое к нему) выражение. Если его значение false
или nil
, то выдаётся ошибка.
Строка с сообщением об ошибке. Если message
пусто или представляет собой nil, то выдаётся "анонимное утверждение", но зачастую лучше предоставлять осмысленное сообщение об ошибке.
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.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Если тип первого параметра не совпадает ни с одним из указанных, то функция ‘assert_type’ выдаёт ошибку.
assert_type(var, vardescription, level, type1, type2, ...)
Переменная любого типа.
Если тип ‘var’ не совпадает с типами ‘type1’, ‘type2’ ..., то выдаётся сообщение об ошибке, которое включает фактический тип переменной ‘var’ и желаемые типы. ‘vardescription’ — это строка с информацией, дополняющей сообщение об ошибке. Это должно быть довольно подробное описание переменной ‘var’ (фактически полное название), состоящее из символов в нижнем регистре, дополнительная информация приводится в скобках.
level
указывает место обнаружения ошибки, так же как и функция Lua ‘error’. Указывается в обязательном порядке, если сомневаетесь, то выбирайте ‘1’.
Последовательность строк. Если тип ‘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. |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
‘cond’ — это условное выражение, замена тернарного оператора ‘?:’ из C-подобных языков. Однако стоит заметить, что это не точная замена, а просто обходной манёвр с некоторыми побочными эффектами замены.
cond(condition, iftrue, iffalse)
Булево выражение.
Выражение, которое выдаётся, если ‘condition’ истинно.
Выражение, которое выдаётся, если ‘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’ (например строками или переменными) и никаких побочных эффектов не возникнет.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Функция ‘etype()’ возвращает дополнительный тип своего параметра.
etype(var)
Переменная любого типа.
argtype = etype(value) |
Типами Lua являются "nil"
, "number"
, "boolean"
, "string"
, "table"
, "function"
, "userdata"
и "thread"
. Чтобы запросить тип любой переменной, можно воспользоваться функцией Lua ‘type’. Однако Enigma различными способами определяет целый ряд дополнительных типов и такие типы можно запросить с помощью ‘etype’. ‘etype’, как правило, возвращает тип Lua своего параметра, кроме двух случаев, рассмотренных ниже:
Вместо "userdata"
будут возвращены спеыиальные типы Enigma. Такими особыми типами являются "object"
, "position"
, "tile"
, "tiles"
, "group"
, "world"
, "polist"
и "default"
. Если встретится неизвестный пользовательский тип данных, то будет возвращено значение "unknown"
.
Если var
— это список, то будет возвращён его метасписок. Если присутствует элемент ‘_type’, то он будет использован в качестве etype
. Самыми характерными примерами являются карты libmap, Преобразования и res.maze с лабиринтами и клетками. Поэтому ‘etype’ будет возвращать ещё и типы "map"
, "resolver"
, "maze"
и "cell"
. К системе etype
можно обратиться через ‘_type’ из любого места, где используются метасписки.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Функция ‘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)) |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Функция ‘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 |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Функция ‘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)) |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Список ‘ORI2DIR[]’ преобразует значения положения в значения направления.
result = ORI2DIR[orientation]
В элементах с соответствующим индексом списка хранятся значения направлений соответствующих положений.
direction = ORI2DIR[NORTH] -- N = po(0, -1) direction = ORI2DIR[SOUTHEAST] -- SE = po(1, 1) direction = ORI2DIR[NODIR] -- po(0, 0) |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Функция ‘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 |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Функция ‘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)) |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Функия ‘usertype()’ возвращает для типов данных Enigma информацию о типе.
usertype(var)
Переменная любого типа.
argtype = usertype(value) |
Особая информация о типе возвращается только для используемого в Enigma типа данных Lua "userdata"
. Такими особыми типами являются "object"
, "position"
, "tile"
, "tiles"
, "group"
, "world"
, "polist"
и "default"
. Для других типов данных будет возвращено значение "unknown"
.
Функция etype предоставляет основные возможности по определению типов данных и частично основана на этой функции.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Некоторые атрибуты и сообщения — общие для многих объектов или даже поддерживаются всеми объектами. Здесь мы подробно их рассмотрим. Дальнейшие главы просто ссылаются на них или даже пропускают их, когда они полностью поддерживаются и используются как обычно.
6.1 Общие атрибуты | ||
6.2 Общие сообщения | ||
6.3 Общие константы | ||
6.4 Глобальные атрибуты |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Атрибут именования объектов (см. раздел Именование объектов), который позволяет присвоить имя любому объекту. Чтобы ссылаться на него, следует проверять уникальность имён. Но движок помогает вам, автоматически добавляя уникальный номер к именам, оканчивающимся на знак ‘#’ (см. раздел Именование объектов). Если вы повторно используете уже использовавшееся имя, первый объект будет разименован и все ссылки на имя будут указывать на новый именованный объект. Если вам нужно присвоить объекту имя, вы должны сделать это при создании объекта, так как некоторым объектам имена необходимы и в случае их отсутствия уникальные имена им присвоит движок.
Следует заметить, что этот атрибут не перечислен в описаниях отдельных объектов.
Последовательность символов, состоящая из букв и специальных символов, указанных выше.
nil
Если имя не задано, то некоторым объектам имена будут присвоены автоматически.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Ключевой атрибут любого объекта, описывающий текущее состояние объекта в его стандартном жизненном цикле. Это Состояние объекта описывается простым числом. Большинство динамических объектов могут находиться только в двух состояниях. Для других объектов характерно большее количество состояний. Возможные состояния перечисляются в описании каждого объекта. Этот универсальный атрибут допускает общие сообщения, такие как toggle, signal, on, off, open, close.
Пожалуйста, используйте указанные в описаниях объектов константы, набранные в верхнем регистре.
0
Хотя в большинстве случаев значение атрибута задаётся при создании объекта, предпочтительно впоследствии менять его посредством сообщений.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Все активные объекты при срабатывании выполняют некое действие по отношению к своей цели. Данный атрибут является частью парадигмы Цель-действие, которая гарантирует возможность связывать объекты. Вы можете задать общий атрибут ‘target’ для объекта либо зависимые от состояния (атрибут state) атрибуты ‘target_0’, ‘target_1’ и т. д. (см. раздел Состояние объекта). Для всех них характерен одинаковый синтаксис:
Отдельные цели могут быть объявлены через имя объекта либо ссылку на него. Несколько целей можно объявить с помощью групп и меток.
target = "myDoor" target = myObject target = {"myDoor", myObject} target = {grp(obj1, obj2), "myDoor", myObject} |
nil
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Все активные объекты при срабатывании выполняют некое действие по отношению к своей цели. Данный атрибут является частью парадигмы Цель-действие, которая гарантирует возможность связывать объекты. Вы можете задать общий атрибут ‘action’ для объекта либо зависимые от состояния (атрибут state) атрибуты ‘action_0’, ‘action_1’ и т. д. (см. раздел Состояние объекта). Для всех них характерен одинаковый синтаксис:
Отдельное действие можно объявить, используя строку сообщения. Несколько действий, относящихся к различным целям, можно объявить, используя метки строк.
action = "open" action = {"open", "turn", "toggle"} |
nil
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Очень специфическое дополнение к парадигме Цель-действие, позволяющее в случае действий, зависимых от состояния, отказать в отправке сообщений по умолчанию (см. раздел Состояние объекта).
true
, false
false
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Довольно специфическое дополнение к парадигме Цель-действие, которое позволяет уничтожить отправителя при выполнении кода действия.
true
, false
false
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Атрибут, который пытается изменить значение действия на противоположное. Он поддерживается всеми объектами с булевым значением действия.
Следует отметить, что этот атрибут не перечислен в описаниях отдельных объектов, если у объекта есть булевы значения действия.
true
, false
false
Этот атрибут поддерживают все объекты с булевыми значениями действий. К тому же, некоторые объекты с другими обратимыми типами значений действий, такими как направления, поддерживают обратимость своих атрибутов, как указано в описаниях этих объектов.
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Атрибут, который описывает один или несколько пунктов назначения. Он используется такими объектами, как 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
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Атрибут, который описывает тормозящую силу трения, которая воздействует на актёров, находящихся на покрытии. Сила трения возрастает вместе с возрастанием скорости актёра и при положительных значениях атрибута является тормозящей. Но значение атрибута также может быть и отрицательным, что порождает силу ускорения, которую игроку чрезвычайно сложно контролировать.
Помимо всех видов покрытий, для некоторых предметов, по функции схожих с покрытиями, таких как it_strip, it_meditation, могут существовать значения силы трения, отличающиеся от силы трения покрытия.
nil
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Атрибут, который описывает сцепление, позволяющее актёру ускоряться на покрытии. Большая сила сцепления означает большую силу ускорения при одинаковой скорости мыши. Значение силы сцепления может быть и отрицательным, что порождает силу ускорения, направленную в сторону, противоположную движению мыши; такую силу ускорения игроку довольно сложно контролировать.
Кроме всех видов покрытий, для некоторых предметов, по функции схожих с покрытиями, таких как it_strip, it_meditation, могут существовать значения силы сцепления, отличающиеся от силы сцепления покрытия.
nil
[ < ] | [ > ] | [ << ] | [ Вверх ] | [ >> ] | [В начало] | [Содержание] | [Предметный указатель] | [ ? ] |
Атрибут, который определяет, должно ли данное объявление объекта применяться на чётных или нечётных участках решётки. Если значение атрибута равно ‘0’, объект будет размещаться только на участках решётки с чётной суммой координат x и y, если же значение равно ‘1’, то он будет размещаться на участках решётки с нечётной суммой координат. Таким образом можно сделать два различных объявления объекта для одного участка решётки, чтобы создать карту произвольной формы, состоящую из покрытий, предметов или камней, расположенн