Советы по написанию триггеров
для МПМ «Былины»,

или «Почему оно тикает»


Предисловие

Здравствуйте! Итак, перед вами «учебник» по написанию триггеров для МПМ «Былины». Слово это взято в кавычки, ибо ни по своему объему, ни по содержательности этот текст все же не тянет на полноценное учебное пособие :). Учитывая, что это первый опыт написания подобного текста, автор просит простить ему излишнюю назидательность и прочие огрехи. Критика, дополнения и указания на неточности и ошибки, просьбы о написании дополнительных разделов и глав — приветствуются и принимаются по мад-почте на имя Горраха.

Предупреждаю заранее — учебник не содержит и не будет содержать в дальнейшем подробного описания возможностей DGScript — для этого существует спецификация, выложенная на странице МПМ «Былины» в разделе «Файлы». Впрочем, удовлетворение от хорошо проделанной работы по созданию красивой и гибкой зоны при помощи DGScript вполне компенсирует время и усилия, потраченные на его освоение. Желаю удачи и творческих успехов в работе билдера!

Часть 1
Зачем они нам?

Первый вопрос начинающего билдера: «А для чего вообще нужны триггера?»

Мир МПМ-игры достаточно гибок, и очень многие эффекты можно создать, просто проставив мобам и предметам соответствующие параметры. Тем не менее — количество возможных игровых ситуаций безгранично, и для обработки этих событий и ситуаций предусмотрен специальный язык разработки программ-«триггеров», который называется DGscript. Это «техническая» сторона дела. Если же обратиться к творчеству — то триггера могут сделать вашу зону по-настоящему оригинальной и интересной, создать необычные эффекты, образовать запутанный и увлекательный квест. Зона, не содержащая триггеров, по сути представляет собой просто некий объем пространства, по которому бродят предназначенные к убиению слепоглухонемые мобы. Хотя, разумеется, если вы в состоянии писать описания так, как это сделано в «Трех дорогах» — это может, до некоторой степени, служить компенсацией :). «Научить» же мобов, объекты и комнаты вести себя нестандартным образом, реагировать на разнообразные действия игрока можно лишь с помощью DGScript.

Итак, если вы хотите писать не сырые «полуфабрикаты», а законченные и работоспособные зоны и желаете изучить язык триггеров, первое, что вы должны сделать, это скачать с сайта «Былин» из раздела «Файлы» документацию по DGscript (DGSinfo). Последняя на момент написания версия этого документа носит номер 4. Я не стану описывать здесь подробности языка, лишь приведу несколько примеров. Кроме того, если вы владеете любым языком программирования, то нелишним будет просмотреть исходные коды интерпретатора триггеров.

Часть 2
С чего начать?

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

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

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

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

Часть 3
Основные понятия

Для написания триггеров вовсе необязательно быть программистом и знать языки программирования (хотя в этом случае освоить DG script значительно легче), но следует все же усвоить некоторые основные понятия. Итак…

1. Игровыми объектами в МПМ «Былины» являются а) мобы (или игроки — хотя к игроку (так, во всяком случае, считается :)) нельзя подключить триггера); б) предметы; в) комнаты. Каждый объект обладает некоторым количеством характеристик — свойств, количество и значение которых зависят от типа объекта, от его подтипа (например, предмет может быть оружием, а может свитком или магическим напитком) и даже конкретного экземпляра данного объекта.

2. Триггером называется прикрепленная к игровому объекту программа на языке DGscript, выполнение которой происходит при некоторых заранее определенных условиях. Существует три типа триггеров — триггера мобов, комнат и объектов. У каждого из этих типов есть подтипы, от которых зависят условия срабатывания триггера. Например, триггер может вызываться, когда вы сказали какую-либо фразу, когда вы дали мобу предмет или деньги и т.д.

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

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

5. Оператором языка называется заранее определенная разработчиками DGscript команда, введение которой в текст триггера вызывает выполнение определенных действий. Например, оператор wait 1s вызовет остановку выполнения триггера на 1 секунду (или на другое, указанное в параметре время), а оператор halt прекращает выполнение текущего триггера.

6. ID объекта. То, что вы создаете в редакторе, не есть, собственно, «мобы» и «предметы». Это описание класса объектов, исходя из которого движок игры создаст экземпляры класса. То, что какой-то моб или предмет может присутствовать в игре в единственном экземпляре, роли не играет. Приведу аналогию. Вы создаете «штамп». А каждый моб или предмет с заданными вами характеристиками — это «отпечаток» штампа. Тогда возникает вопрос — как же отличать друг от друга объекты, созданные с одного «штампа»? Для этого и существует идентификатор — ID. В мире не может быть двух объектов с одинаковыми идентификаторами. Таким образом, идентификатор создает ссылку на один-единственный, вполне определенный экземпляр-«отпечаток». ID — это не то же самое, что и vnum. Следуя аналогии, vnum — это номер «штампа» (описания класса), а ID — «номер» отпечатка. (Учтите, ID — это не число! Поэтому не стоит пытаться проводить над ним арифметические операции.)

Предположим, вы имеете указатель на некоторого персонажа, помещенный в переменную. Это может быть стандартный указатель, создаваемый при запуске триггера (например, многие триггера помещают в переменную actor указатель на игрока/моба, вызвавшего запуск триггера). Или вы сами поместите в переменную указатель на игрока: eval char %random.pc% — помещаем в переменную char указатель случайного игрока, находящегося в комнате с этим триггером. Тогда выражение %переменная.поле% вызывает обращение к какому-либо свойству данного объекта (имя моба/игрока/предмета, статсы и т.п.). Например: %char.name% — имя игрока, ссылка на которого была помещена в переменную char. %char.id% — ID этого игрока, %char.str% — сила игрока и т.д.

Часть 4
Грабли

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

Триггер выполняется, только если а) он подключен к какому-либо объекту и б) объект, к которому подключен триггер, существует все время, необходимое для выполнения триггера. При нарушении любого из этих двух условий выполнение триггера будет немедленно прервано. Одна из самых распространенных ошибок начинающего билдера — попытки что-либо сделать в триггере после его отключения (detach) от объекта, или после уничтожения (purge) самого объекта. По этой же причине не выполняется часть mob_death триггера, расположенная после команды wait — при этой команде выполнение триггера временно приостанавливается и выполняются другие команды мира. В том числе команда «извлечения» моба, смерть которого вызвала срабатывание триггера — и моб вместе с триггером будет удален из игры.

Когда вы обращаетесь к произвольной переменной следующим образом: %variable%, интерпретатор ищет уже существующую переменную с таким именем и подставляет ее значение на место взятого в проценты выражения. Если такой переменной нет — будет подставлено пустое значение. Если же вы хотите обратиться не к значению переменной, а к ней самой, ее название надо употребить без знаков процента. например global var1 — эта команда сделает переменную var1 глобальной. global %var1% — неправильно; если переменная var1 содержит, например, значение «123», то получим global 123 — интерпретатор попытается найти и сделать глобальной переменную 123, а переменной с таким именем, вероятней всего, не существует.

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

Глобальные переменные. Переменная, объявленная глобальной, существует столько же времени, сколько и сам объект, к которому она относится. Она доступна изо всех триггеров данного объекта, однако чтобы она стала доступна и в триггере какого-либо другого объекта, ее надо скопировать в окружение этого другого объекта командой remote. При этом создается независимая глобальная переменная с тем же именем, т.е. если вы измените значение этой переменной, это не отразится на копиях переменной. (Кроме того, по моим экспериментам после присваивания глобальной переменной нового значения, ее необходимо снова объявлять глобальной, иначе она рассматривается как обычная и уничтожается по завершению скрипта.) Если же вы хотите использовать переменную, доступную из любого триггера и содержащую значение, одинаковое для любого триггера и объекта — надо использовать мировые («world») переменные.

Делать этого, впрочем, не рекомендуется. В 99% случаев глобальная переменная после копирования не требует согласования с исходной переменной — обычно глобальные переменные используют, чтобы «запомнить» игрока или поставить «метку» о выполнении какого-либо условия. Мировые же переменные очень капризны и обладают существенным недостатком — их необходимо уничтожать вручную, о чем вполне можно забыть или сделать это некорректно. Глобальные же переменные уничтожаются при извлечении объекта, к которому принадлежат, при этом высвобождается и занимаемая ими память (хотя желательно, а иногда и обязательно для работы квеста уничтожать эти переменные при перезагрузке зоны).

Еще одна распространенная ошибка — употребление в триггерах мобов команд наподобие wecho вместо mecho и наоборот. Чтобы избежать этого, используйте команды-переменные %echo%, %send% и т.д. — интерпретатор автоматически подставит на их место команды, необходимые по типу триггера.

Распространенная ошибка — опечатки в переменных. Как и любая программа, интерпретатор триггеров делает то, что вы сказали ему делать, а не то, что бы вы хотели сделать. Если вы создадите, например, переменную %questor99%, а затем обратитесь к переменной %questro99%, интерпретатор будет обрабатывать несуществующую переменную %questro99%, т.е. пустое значение. Вроде бы тривиально, но эта ошибка едва ли не одна из самых частых и, кстати, труднонаходимых.

Операторы циклов и условий. Конструкции if-elseif-else-end, while-done, foreach-done, switch-case-break-…-done предоставляют очень удобные возможности, но создают опасность возникновения бесконечного цикла, что приводит к мгновенному падению или зависанию сервера. Бесконечный цикл может возникнуть при отсутствии оператора завершения цикла end или done или неверно заданных условиях. Например, если выражение, заданное в качестве условия в операторе while-done, будет всегда истинным, возникнет бесконечный цикл.

Часть 5
Еще несколько примеров

В качестве общеизвестных примеров триггеров взяты:

телепортящий триггер у цыган в 1 и 2 родовой деревне;

квест на главаря разбойников;

квест на кладбище около 1 родовой деревни.

Часть 6
Немного практики

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

Проводим разбиение сюжета по триггерам:

1 триггер — старик приветствует и просит помощи;

2 триггер — старик дает задание;

3 триггер — старик выдает награду при получении нужного объекта;

4 триггер — восстановление зоны после выполнения квеста, с учетом того, что квест может быть выполнен не полностью.

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

Необходимый инвентарь:

Мобы:

1. Седой старик (номер 100)
2. Страшный злобный разбойник (101)
3. Молодой неопытный разбойник (102)

Объекты:

1. Мешок книгочея (100)
2. Калита с монетами (101)
3. Крепкий сундук (102)
4. Длинный ключ (103)
5. Разбойничий жупан (104) — можно надеть на тело

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

Первый триггер — приветствие у старика. Триггер типа mob_PC_enter, номер 100, числовой аргумент 100.

wait 1
say Поздорову!
вздох
say Присаживайся - отдохни с дороги...
say Меня Рашив-книгочей зовут...
дум
wait 1s
say Беда тут со мной приключилась...
say Налетели лихие люди, отняли суму мою с деньгами и мешок с книгами.
wait 1s
say И бог бы с ними, с деньгами - недалече уж мне до дому...
say А вот книги...
вопрос %actor.name%
say Может, ты сумеешь вернуть их?
detach 100 %self.id%

Второй триггер mob_speech, номер 101. Аргумент: согласен смогу слушаю, числовой аргумент: 1.

if %actor.vnum != -1
    *проверяем, не моб ли это говорит, если да - завершаем триггер
    halt
end
makeuid questor100 %actor.id%
global questor100
*создаем копию переменной actor и делаем ее глобальной
wait 1s
улы
say Сами Боги послали мне тебя!
say Вот туда эти презренные удалились.
emot махнул рукой куда-то на восток
wait 1s
say Я, пожалуй, тут подожду.
жда
attach 102 %self.id%
detach 100 %self.id%
detach 101 %self.id%

Триггер mob_receive, номер 102 (мобу дали предмет). Числовой аргумент: 100.

if %object.vnum% != 100
    say Спасибо, но это не моя вещь!
    дать %object.name% %actor.name%
    halt
end
if %actor.id% != %questor100.id%
    *Если предмет дал не тот кто брал квест
    wait 2
    say Я не тебя просил о помощи, но все равно спасибо!
    поклон %actor.name%
    wait 1
    stand
    %echo% Старик удалился по дороге на север.
    mjunk all
    *Удаляем все предметы из инвентаря
    wait 1
    %purge% %self%
    *И удаляем моба
    halt
end
*И, наконец, если дали то, что нужно и тот, кто должен - НАГРАДА!
say Вот спасибо тебе, %actor.name%!
mload obj 105
wait 1
*Загружаем четверть водки :)
дать четверть %actor.name%
поклон %actor.name%
wait 1
stand
%echo% Старик удалился по дороге на север.
mjunk all
*Удаляем все предметы из инвентаря
%purge% %self%
*И удаляем моба

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

Триггер mob_death, номер 103, числовой аргумент: 100. Подключаем к молодому разбойнику.

mload obj 103

Можно сделать квест чуть интересней. Конечно, никто не мешает просто убить злобного разбойника… Но можно его обхитрить.

Итак. Триггер mob_PC_Enter, номер 104, числовой аргумент: 100.

eval item %actor.eq(5)%
*получаем указатель на предмет, надетый у вошедшего на теле
wait 1
if %item.vnum% != 104
    *Если предмет НЕ разбойничий жупан, то
    say Ты еще кто таков%actor.g% ?!
    рыч
    mkill %actor%
    halt
end
*Если на теле надет разбойничий жупан, то
приве %actor.name%

Ну и конечно, главное. Триггер 103 надо подключить к молодому разбойнику, триггер 104 — к злобному разбойнику, триггера 100 и 101 — к старику. Кроме того, необходимо написать триггер для комнат, запускающийся при перезагрузке зоны со 100% вероятностью.

Триггер wld_reset, номер 105, числовой аргумент: 100.

calcuid starik 100 mob
*помещаем в переменную ссылку на моба-старика
rdelete questor100 %starik.id%
*удаляем глобальную перменную questor100 с моба 100
detach 101 %starik.id%
detach 102 %starik.id%
detach 103 %starik.id%
attach 101 %starik.id%
attach 102 %starik.id%
*отключаем от старика все триггера, и подключаем заново необходимые при встрече и взятии квеста

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

Все! Квест готов :). Осталось разместить мобов и объекты в зоне и подключить триггер 105 к любой комнате зоны. Чтобы тому, кто будет отлаживать вашу зону, не пришлось искать место подключения, лучше всего выбрать комнату 100 — первую в зоне.

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

Часть 7
Поиск ошибок и отладка триггеров

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

Итак, при тестировании зоны вообще — первым делом отмените перезагрузку зоны, проставив ей тип «не перезагружать». Когда вам понадобится перезагрузить зону — вы сможете это сделать командой zreset номер_зоны. Дело в том, что иммортал, которым вы, скорее всего, тестируете зоны — это не игрок, его присутствие в зоне игнорируется и зона с типом перезагрузки «перегружать при отсутствии игроков» будет перезагружаться во время тестирования — что мешает проверить работу квеста. Для проверки процесса перезагрузки можно позднее поставить зоне перезагрузку через 1 минуту, только не забудьте перед отправкой зоны установить нужный тип и время перезагрузки.

При тестировании квеста и триггеров необходимо запустить системный лог командой syslog и при любых проблемах прежде всего обращаться к нему. При тестировании ошибки удобней всего искать и исправлять последовательно — т.е. сначала вы тестите триггера приветствия (если оно есть) и выдачи квеста и так далее, по порядку. Для обнаружения ошибок можно использовать несколько стандартных приемов. Например, список подключенных к объекту триггеров и глобальные переменные вместе с их содержимым можно контролировать командами stat и stat room. Нередко проблема заключается в неверном значении переменной — чтобы проконтролировать его, можно вставить в триггер команду типа %echo% %переменная%. Следующая наиболее распространенная ошибка — порядок подключения и отключения триггеров.

При поиске ошибки в конкретном триггере главное — не распыляться. Забудьте о продолжении квеста и том, что для него необходимо, ищите ошибку в конкретном триггере. Бывает, что триггер отказывается работать по совершенно неясным причинам — в этом случае помогает тактика «закомментировать все». Сначала закомментируйте подозрительные и просто «слабые» места вроде сложных многоступенчатых циклов, если это не помогло — продолжайте дальше, вплоть до команд %echo% и им подобных. Как правило, после комментирования какой-то строки триггер начинает работать — скорее всего именно там и допущена ошибка. Если же нет, начинайте постепенно снимать комментарии. Как правило, в результате такого «перекрестного обстрела» ошибка обнаруживается.

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

Использование OLC для отладки зоны. В сервере «Былин» есть возможность вносить изменения в зоны, не перезапуская сервер. Это OLC — онлайновый редактор зон. Команды OLC типа mlist, olist и т.д. позволяют просматривать списки мобов и предметов, команды типа trigedit, medit, redit, zedit — редактировать триггера, мобов, комнаты и т.д. Команта attach позволяет назначать триггера. Эти команды можно использовать для существенного ускорения процесса отладки зоны — найдя при тестировании квеста ошибку и исправив ее в редакторе, можно тут же, «на ходу», исправить ее в окне иммортала и продолжить тестирование. Как правило, для мелких ошибок вроде неверного агрумента, опечатки в переменной или неназначения нужного триггера это быстрее, чем заново сохранять исправленную зону и перезапускать движок, не говоря уже о повторении пройденных этапов квеста.

Заключение

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

С наилучшими пожеланиями,
Горрах.

Текст закончен: 15.01.2004
Последнее изменение файла: 20.01.2004