Назад в раздел
FAQ по скрипт-языку для Quest.
eManual.ru - электронная документация
From: Michael Borisenko, 2:5061/15.20
Subj: FAQ по скрипт-языку для Quest
В данном FAQ рассматриваются принципы работы скрипт-языка.
Рассмотрим рисованные квесты от 3-го лица. Рисованные квесты от
1-го лица - это, по сути, вырожденный вариант от 3-го лица, когда
главный персонаж не виден со стороны.
1. Что должен делать движок
---------------------------
В рисованном квесте от 3-го лица действие, как правило, проходит
на отдельных экранах ("комнатах"). Для каждой комнаты формируется
статичный фон, на который затем накладываются спрайты, которые создают
иллюзию движения.
Фон не обязательно состоит из одной картинки - его можно
формировать из нескольких "кирпичиков". Это может облегчить труд
художника, но комнаты могут стать несколько однотипными, как это
получилось в "Кирандии" (первой и второй).
Как изобразить какое-либо действие на этом фоне? Очевидно, что в
рисованном квесте всё многообразие действий изображается
последовательной сменой спрайтов.
Каким образом менять спрайты в нужной последовательности и в
нужное время? По-видимому, лучшим решением здесь будет использование
специального скрипт-языка, который будет управлять объектами на
экране. Рассмотрим сперва, какие объекты могут быть, а затем попробуем
дать общую структуру скрипт-языка.
Допустим, мы имеем полный набор всех спрайтов для игры - включая
все фазы движения персонажей, статичные рисунки, движущиеся облака и
прочее. Пусть эти спрайты пронумерованы. Нумерация может быть сквозная
или с ветвлениями. Например, номер 12.0 означает первую фазу движения
некоего объекта, 12.1 - вторую фазу и так далее. Таким образом,
любой объект в игре может состоять из одного спрайта либо из группы
спрайтов.
Учтём также, что когда мы выводим спрайты поверх фона, это надо
делать с учётом планов - чтобы ближние закрывали дальние. Далее, нужно
как-то синхронизировать переключение спрайтов - поэтому введём понятие
игрового такта. В зависимости от плавности мультипликации нужна
частота 15-25 тактов в секунду.
Таким образом, игровой движок каждый такт получает информацию:
какие спрайты из общего набора включены, какие у включённых координаты
и номера планов. Движок также учитывает переключение комнат (тогда он
меняет фон) и скроллинг, если фон не помещается на физическом
экране. В случае скроллинга удобнее ввести абсолютные координаты для
вывода спрайтов. Например, фон у нас размером 1280х400, а экран -
320х200. Тогда спрайты будут иметь координаты от 0 до 1279 по Х и от 0
до 399 по У. Либо, если спрайты могут частично выходить за край
экрана, их координаты станут отрицательными. Экран же имеет какое-то
смещение внутри фона, движок учитывает это смещение и пересчитывает
все координаты для вывода на физический экран.
2. Как работает скрипт-язык
---------------------------
Здесь будет рассматриваться только общая структура скрипт-языка. В
примерах используются команды, близкие по синтаксису к Си. Но
синтаксис каждый может подогнать под свои нужды. Нужно заметить
только, что языку желательно иметь операторы ветвления, циклов,
переходов, желательны подпрограммы и(или) функции. Необходимы
переменные(может быть, только целочисленные) и массивы. В общем, как в
любом языке.
Ограничения здесь скорее сверху, чтобы не переборщить и не сделать
ненужных операторов. Единственное - в обязательном порядке вводится
"разделительная тактовая черта" - пусть это будет "|". Когда программа
доходит до "|", выполнение её приостанавливается до следующего такта.
Именно эта тактовая черта делает необходимым скрипт-язык. Реализовать
аналог тактовой черты на том же языке, на котором написан движок,
очень сложно и система получится крайне неуклюжей. Скрипт-язык же
предоставляет неограниченную гибкость.
На скрипт-языке пишется не одна программа, а множество. Программы
будут управлять не только спрайтовыми объектами, но и звуками,
интерфейсом. В каждый момент времени часть программ выключена, часть
включена. Все они выполняются параллельно. Параллельность означает,
что сперва выполняется одна программа до "|", потом следующая и так до
тех пор, пока они не закончатся. Тогда движок получает информацию об
изменениях за последний такт и выводит изображение на экран. После
этого все включённые программы продолжают работу до следующей "|".
В самом начале игры запускается только одна, главная программа.
Остальные находятся в состоянии ожидания. Главная программа в
зависимости от разных флагов и переключения комнат запускает
остальные.
Каждой комнате соответствуют несколько программ, которые
обслуживают её объекты. Когда мы уходим из комнаты, программы в ней
выключаются. Но некоторые программы должны быть включёнными всегда.
Например, если у нас где-то стоят часы, то программа, которая
отсчитывает время, должна работать постоянно вне зависимости от того,
находимся ли мы в комнате с часами или нет. А вот программа, двигающая
стрелки, должна выключаться при выходе из комнаты.
Добавим также, что программы могут быть зациклены, т.е.
выполняться бесконечно (как счётчик времени), либо заканчивать работу
после выполнения своей функции - например, вспыхнуло где-то пламя и
погасло.
Итак, суммируем всё вышесказанное. В самом начале игры запускается
одна-единственная программа (пусть она называется main). В ней
находятся глобальные переменные, флаги. Совершив все подготовительные
действия, main запускает программу room1, которая включает фон для
первой комнаты и запускает группу программ, в которых обслуживаются
объекты этой комнаты. Когда приходит время перейти в другую комнату,
эта группа программ выключается и включается программа room2. И так
далее.
Видно, что нет нужды в каждой программе ставить признак, к какой
комнате она принадлежит. Всё определяется тем, откуда программа
запущена. Более того - одна программа может обслуживать несколько
комнат. Например, если мы находимся на морском берегу и в нескольких
комнатах слышен шум моря. Шум может выдавать одна и та же программа.
Посмотрим, как это всё работает в виде операторов.
3. Основные операторы скрипт-языка
----------------------------------
Представляется разумным ввести следующие операторы управления
программами:
* run ; - Запустить программу prog. Например, в самом
начале игры из программы main запускаем счётчик времени:
run timecount;
Программа timecount будет каждые 25 тактов прибавлять по секунде,
каждые 60 секунд - по минуте, каждые 60 минут - по часу и вести
подсчёт в некие глобальные переменные в программе main, которые
доступны из всех программ.
* run related ; - Запустить родственную программу prog. При
этом программа prog привязывается к текущей программе, и если мы
текущую остановим, то остановится и prog. Возьмём для примера
маятниковые часы - часами могут управлять несколько программ сразу.
Одна будет считать время, другая двигать стрелки, третья двигать
маятник и тикать, т.е. выдавать звуки. Первая программа работает всё
время, две последние - только если игрок находится в комнате с часами.
Как уже говорилось выше, при входе в комнату автоматически запускается
только одна программа, назовём её room25. Программа room25 запускает
программу face (циферблат) и программу pendulum (маятник):
run related face;
run related pendulum;
При выходе из комнаты room25 выключается, а вместе с ней
выключаются, как родственные, face и pendulum. Эти две, в свою
очередь, тоже могли запускать из себя другие родственные программы -
тогда остановятся все они.
Далее рассмотрим операторы:
* stop ; - Остановить программу prog и все родственные ей
программы. Программа может также остановить саму себя.
* stop related; - Останавливает все вышестоящие родственные
программы по цепочке. Этот оператор автоматически выключает и
программу, в которой находится, и вообще всё родственное дерево.
* continue ; - Продолжить программу prog. Действие этого
оператора таково: если prog ещё не запускалась, т.е. счётчик команд
стоит на 0, программа просто начинает работу. Если prog была
остановлена оператором stop, то она продолжает работу с этого места.
В этих случаях включаются также все родственные prog'у программы. Если
же prog завершила работу, т.е. счётчик её команд дошёл до конца, то
никакого действия не совершается. Чтобы гарантированно запустить
программу сначала, используется run.
* wait; - Ожидать выключения. Этот оператор полезен в конце
программы типа roomNN, когда она уже запустила всё что нужно и
подготовила комнату к работе. Если она при этом остановится, то
автоматически выключатся все родственные программы, а мы их только что
запустили. Оператор wait ждёт, когда программу выключат, и вхолостую
отсчитывает такты. wait эквивалентен следующей конструкции:
while (1) |;
либо
a: | goto a;
В обоих примерах тактовая черта обязательна, иначе всё повиснет.
Можно предложить также следующие конструкции для описания самих
программ:
#prg - начало программы prog
#end - конец prog
4. Пример работы параллельных программ
--------------------------------------
Теперь такой пример. Пусть у нас из первой комнаты имеется два
выхода - в комнату 2 и в комнату 3. Структура будет такая:
#prg main
...
run timecount; // Счётчик времени
run room1; // На этом main заканчивается, родственные программы ей не нужны
#end
#prg timecount
<считаем время, эта программа работает всегда>
#end
#prg room1
<активируем фон для первой комнаты>
...
run related clock; // запускаем часы
run related exit_1_to_2; // Программа выхода в комнату 2
run related exit_1_to_3; // Программа выхода в комнату 3
...
wait;
#end
#prg clock
run related face;
run related pendulum;
<тикают часы>
#end
#prg face
<переводим стрелки>
#end
#prg pendulum
<качаем маятником>
#end
#prg exit_1_to_2
<ожидаем, когда выполнится условие выхода из 1-й комнаты - например,
главный герой подошёл к краю экрана>
run room2;
stop related; // Останавливаем вызвавшую room1 и все родственные программы,
// в том числе clock, face, pendulum, exit_1_to_3.
#end
#prg exit_1_to_3
<ожидаем, когда выполнится условие выхода>
run room3;
stop related;
#end
#prg room2
<активируем фон для второй комнаты. При этом фон для первой автоматически
стирается движком>
...
run related exit_2_to_1; // Программа выхода в комнату 1
run related exit_2_to_4; // Программа выхода в комнату 4
<и так далее>
...
wait;
#end
<И тому подобное для 3-й комнаты и всех остальных>
...
Технически программы для каждой комнаты удобно группировать в
отдельные текстовые файлы. Общие для всех программы можно поместить в
тот же файл, где находится программа main и т.д. В "Шестом Измерении"
использован похожий язык. Компилятор собирает все тексты с программами
и компилирует в простой байт-код. В exe'шнике присутствует только
интерпретатор байт-кода.
Легко видеть, что такая система даёт АБСОЛЮТНУЮ свободу при
программировании квеста. Если абсолютной свободы покажется слишком
много, систему можно упростить. Операторы вовсе не обязаны иметь такие
длинные названия; можно упростить команды до уровня программируемого
калькулятора; можно сократить число команд (related в принципе вещь
необязательная, хотя удобная); можно формировать "болванки" для комнат
автоматически.
Несомненно, в любом случае придётся делать специальные редакторы
для визуального размещения спрайтов, обозначения маршрутных дорожек
для главного персонажа, размещения активных зон на экране и так далее.
Редакторы перегоняют всё это в текстовую форму - массивы чисел для
скрипт-языка.
5. Переменные и подпрограммы
----------------------------
Для удобства рассмотрения дальнейших примеров определим, как
работают переменные. Для простоты будем считать, что все переменные
глобальные. Поэтому к любой переменной любой программы можно
обратиться из другой программы таким образом: ..
Например, для обращения к переменным описанной выше программы
timecount мы напишем:
hrs=timecount.hours;
mnt=timecount.minutes;
К переменным программы main, как к "совсем" глобальным, можно
обращаться проще - просто по имени. Если в текущей программе такая
переменная не определена, будет обращение к переменной из main.
Поэтому переменные hours и minutes можно определить не внутри
timecount, а внутри main, что упростит обращение к ним.
Можно ввести также подпрограммы. Будем описывать подпрограмму как
обычную программу, но завершающуюся оператором return (их может быть
несколько). Вызывать такую программу мы будем не с помощью оператора
run, а с помощью call. При этом счётчик команд текущей программы
просто перескакивает на подпрограмму, и возвращается, когда встретит
return. Пример: пусть у нас есть подпрограмма, которая выдаёт
какой-нибудь часто используемый звук.
#prg some_program
...
call very_loud_sound; // дальше не пойдём, пока звук не отгремит
...
#end
#prg very_loud_sound
<выдаём звук>
return; // не забыть return, иначе завершится программа some_program
#end
6. Работа со спрайтами
----------------------
Рассмотрим теперь операции, которые могут понадобиться для работы
со спрайтами и переключением фона. Их можно реализовать либо с помощью
операторов скрипт-языка, либо с помощью функций. По многим
соображениям функции удобнее - они могут возвращать значения, они
независимы от языка. Один раз написав язык, можно будет впоследствии
менять только набор функций для обмена информации с движком и делать
другие проекты.
Итак, базовый набор функций:
int background(int FonNumber) - включает фон с заданным номером.
При этом все спрайты выключаются. В пределах текущего такта нужно
включить все необходимые спрайты. Для этого все программы, которые
включают спрайты, должны находится после программы с background() -
тогда они сработают в том же такте. Функция может возвращать значение.
Например, не 0, если ошибка включения фона.
int object(int Number, int Phase, int X, int Y, int Layer) -
включает спрайт (объект) с заданным номером и в заданной фазе. Здесь
мы рассматриваем несквозную нумерацию спрайтов (см. начало). Задаются
также координаты и номер плана. Функция резервирует и возвращает некий
handle, с которым движок будет работать дальше. Хэндлы позволяют
включать один и тот же спрайт в нескольких местах и дают другие
удобства.
delete(int Handle) - удаляет спрайт по хэндлу. Кроме удобств,
хэндлы могут причинить неприятности, если выключить несуществующий
хэндл. Вспомним, что при включении нового фона все спрайты
выключаются, т.е. удаляются все зарезервированные хэндлы. Поэтому
стоит сделать выдачу сообщения об ошибке в самом движке, когда ему
поступает команда удалить несуществующий хэндл.
set(int Handle, int Phase, int X, int Y, int Layer) -
устанавливает новые значения для объекта. Можно также ввести
дополнительные функции для кажого параметра: set_phase, set_xy,
set_layer. Ещё бывает удобна функция типа add_xy, которая добавляет
новые значения к старым координатам.
Могут быть также полезны функции:
off(int Handle) - выключает спрайт, но не удаляет его.
on(int Handle) - включает спрайт.
Кроме функций установок, понадобятся ещё функции для получения
установленных значений.
int get_x(int Handle) - выдаёт координату X. Аналогично действуют
следующие функции:
int get_y(int Handle)
int get_phase(int Handle)
int get_layer(int Handle)
int get_switch(int Handle) - возвращает 0 или 1, в зависимости от
того, включён спрайт или нет.
Если движок настолько продвинут, что может ещё и поворачивать
спрайты, то надо предусмотреть функции типа turn для их поворота.
Например, это может быть актуально для тех же часов - поворачивать
стрелки. Если не поворачивать, то придётся рисовать множество фаз.
7. Пример работы со спрайтами
-----------------------------
Рассмотрим простой пример. Пусть у нас горит огонь. Художник
нарисовал 5 фаз горения. В некоторый момент времени огонь гаснет -
тогда включаются ещё 10 фаз угасания. Итого огонь имеет 15 фаз.
Чтобы погасить огонь, нужно всего лишь в любой программе
установить переменную endfire в 1.
#prg some_program
...
fire.endfire=1;
...
#end
#prg fire
int h, endfire=0;
int f[20]={0,1,3,2,4,3,0,4,2,1,0,3,1,2,4,0,3,-1}; // фазы переключаются
// хаотично
int pf; // номер элемента массива с текущей фазой
h=object(212, 0, 25, 46, 0); // Спрайт номер 212, нулевая фаза,
// х=25, у=46, план=0, т.е. самый дальний.
while (1)
for (pf=0; f[pf]!=-1; pf++)
{
set_phase(h, f[pf]); // Устанавливаем новую фазу
if (endfire) goto exit; // Если какая-то другая программа установила
// endfire в 1, гасим огонь.
| // Не забываем про "|"
}
exit:
for (pf=5; pf<15; pf++) { set_phase(h, pf); | } // Цикл угасания огня
delete(h); // огонь погас, объект выключаем.
#end
Нетрудно включить таким же образом сколько угодно огней -
достаточно скопировать эту программу под именем fire2 и изменить
координаты. Программа может показаться сложной для такого простого
объекта, как огонь, но не стоит забывать, что этот же алгоритм всё
равно пришлось бы так или иначе реализовывать. Этот пример на самом
деле относительно сложный - в большинстве случаев придётся просто
переключить подготовленные фазы в цикле.
По аналогии со спрайтами можно реализовать и работу со звуками.
Здесь мы звуки, а также прочие вопросы интерфейса не рассматриваем.
(c) Michael Borisenko, 30.11.1998
FidoNet: 2:5061/15.20, E-mail: ims@jeo.ru
|
|
|
|