вернуться на beanet.ru вернуться к списку проектов вернуться на главную страницу сборника

Тема: Триггеры и скрипты


Ключевые слова: триггер, скрипт, trigger, script, управление событиями

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

перейти к общему списку

Если игрок не просто бегает и отстреливает врагов, а ещё и совершает (или наблюдает) некоторые действия "сверх положенного" - происходящее на экране становится заметно интереснее. К тому же, пока ИИ (настоящий искусственный интеллект, а не то, что его заменяет) в играх не развит более-менее достойно, остаётся неплохая альтернатива - помогать NPC с помощью скриптов. То есть заведомо проверять некие условия и задавать реакцию игры на них (отдавая команды различным энтити).
Интересно отметить, что скриптом также можно с полным правом считать каждую функцию поведения энтити на уровне кода. Другое дело, что скриптование теряет значительную долю гибкости (но упрощается), если переходить на уровень маппинга. Здесь рассматривается скриптование именно на уровне маппинга.

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

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


Содержание:


Предварительные сведения

Рассмотрим принципы работы системы ввода-вывода (Inputs/Outputs) энтити.
По существу, каждая энтити представляет из себя закрытый объект ("чёрный ящик"). Всё, что позволяет Source, это установить какие-то внешние переменные (атрибуты и флаги), задать реакции на события данной энтити (Outputs), а в игре - посылать энтити управляющие команды (Inputs). Непосредственно залезть в работающий код энтити практически невозможно.

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


Так вот, чтобы снизить вероятность проблем с восприятием логики работы скриптов, представляю несколько интересных фактов:
  • механизм работы ввода-вывода следующий: если у данной энтити определена реакция (Output) на конкретное событие, то каждый раз, когда это событие с ней случается, движок проверяет и обрабатывает эту самую реакцию. В описании же реакции задаётся команда (Input), посылаемая другой (или той же самой) энтити, время задержки перед отправкой команды и одно-/многократность реакции (т.е. команда отправляется только при первом или при каждом срабатывании). Причём указанная команда (Input) не обязательно вызовет реакцию целевой энтити, а только в том случае, если она, в свою очередь, определена у неё на уровне кода или маппинга;
  • энтити в Source взаимодействуют через так называемые "внешние команды" - Inputs/Outputs. Таким образом, одной командой можно для любого события одной энтити инициировать любую реакцию другой энтити. Под любым событием и любой реакцией здесь подразумевается множество тех, которые определены в коде данных энтити (а также даны в описании энтити в *.fgd-файле). Другими словами - это множество доступных для подачи команд и множество доступных для проверки событий;
  • просто так, по собственному желанию запустить какой-либо скрипт не получится. Требуется задать условие запуска. Например: если сработал триггер, когда создано игровое пространство (загружена карта), если энтити поменяла своё состояние, если прошло определённое время от начала некоего события и др.;
  • управляющая команда не обязательно вынудит энтити сделать то, что Вы от неё хотите. Существуют разные ограничения: например, невозможность NPC достигнуть заданной точки, высокий приоритет друг/враг на уровне кода;
  • событие энтити не обязательно произойдёт в тот момент, когда Вы рассчитываете. В числе прочих неучтённых условий может случиться так, что реакция энтити будет невозможна в текущий момент времени; либо в некоторый момент времени возникнет новое (внешнее) событие, влияющее на выполнение скрипта;
  • к сожалению, большинство команд слишком простые, что вызвало необходимость во введении Valve дополнительных энтити. К примеру, нельзя задать случайный порядок выполнения или случайную выборку команд - это повлекло необходимость во введении logic_case, иногда в комбинации с несколькими logic_relay (таково моё субъективное мнение). Таким образом, если Вам никак не удаётся "заставить" данную энтити делать то, что нужно - поищите вспомогательные скриптовые энтити;
  • если проверяется некоторое условие, то управление передаётся той энтити, для которой данное условие проверяется. В большинстве случаев это самоочевидно: мы задаём только реакцию на условие, а когда это условие выполнится - заранее неизвестно (энтити просто "всегда готова" отреагировать на конкретное условие).
    Но не так уж редко проверка условия задаётся из другой энтити (т.е. будет вызвана другим скриптом в процессе игры, запрашивается извне текущей энтити). И вот здесь уже не всегда можно сообразить, что проверяющая энтити делает лишь запрос проверки. А обработку результатов делает проверяемая энтити;
  • почти для каждой энтити-триггера задано стандартное событие OnTrigger - означает стандартную реакцию триггера на то событие, для учёта которого он предназначен. Однако и почти у каждого триггера есть несколько дополнительных событий, предоставляющих более тонкий контроль за энтити.

Примеры механизма Input/Output:
- "открыть дверь, когда игрок подошёл к ней" задаётся одной реакцией на срабатывание невидимого триггера-области, который занимает некоторое пространство около двери (энтити func_door) - OnTrigger : <имя энтити-двери> : Open. Команда Open уже определена (Valve) в коде энтити-двери, заставляя её открываться;
- "открыть дверь и заблокировать её, когда игрок подошёл к двери или посмотрел в окно" задаётся инициирующей реакцией для каждого из условий, а также набором реакций самой энтити-двери (подробнее см. далее - Пользовательские управляющие команды (FireUser) и события (OnUser)).



Типы энтити и дополнительные условия, на которые следует реакция, задаются флагами триггера (одинаковыми для всех типов триггеров). Это могут быть:


Clients
игрок, в любом варианте;
NPCs
все остальные NPC;
Pushables
энтити, которые не получают повреждений при столкновении с игроком (обычно это неразрушимые объекты), а просто "отскакивают" по законам физики;
Physics Objects
энтити-модели prop_physics;
Only player ally NPCs
только NPC-союзники (дружественные игроку);
Only clients in vehicles
игрок в том случае, если находится в транспортном средстве;
Everything (not including physics debris)
любые объекты, за исключением обломков;
Only clients *not* in vehicles
игрок в том случае, если не находится в транспортном средстве;
Physics debris
Обломки, а также физические объекты (prop_physics) с отмеченным флагом Debris - Don't collide with the player or other debris;
Only NPCs in vehicles (respects player ally flag)
только NPC, которые движутся в транспортном средстве (учитывая признак союзника)

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

Есть очень полезные энтити для задания более тонких ограничений - filter_activator_name (ссылка на англ.) и filter_activator_class (ссылка на англ.); наиболее часто употребляемые триггеры как раз могут их учитывать. В параметрах этих фильтров указывается, какая энтити или класс энтити, соответственно, будет условием прохождения (или наоборот, отсеивания) фильтрации.
Существуют и другие энтити-фильтры с именами вида filter_activator_*.




Флаг - Fire once only - любой реакции

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




Энтити trigger_once

Брашевая энтити, которая удаляется (в игре) после однократного срабатывания. Независимо от того, включён ли флаг Fire once only для какой-либо реакции, триггер запускает все заданные реакции один раз, а затем удаляется из игры.




Энтити trigger_multiple

Брашевая энтити, которая срабатывает сколько угодно раз. Данный триггер генерирует события при каждом входе (OnStartTouch, OnStartTouchAll) и каждом выходе (OnEndTouch, OnEndTouchAll) энтити из занимаемого им пространства - требуется лишь задать реакцию (разумеется, событие с флагом Fire once only сработает только один раз). Также, есть возможность извне проверять (TouchTest), где находится энтити относительно пространства триггера - внутри или снаружи; точнее, касается она триггера или не касается (OnTouching, OnNotTouching).

Не забывайте учитывать, что данная энтити периодически тестирует своё состояние. В случае, когда запускается длительная (по времени) последовательность скриптов, необходимо либо заведомо устанавливать больший период времени между проверками (атрибут Delay Before Reset), либо отключать (Disable) энтити после срабатывания и включать по окончании скриптов (Enable), либо использовать внешнюю проверку (TouchTest) с откликом энтити на OnTouching и OnNotTouching. Если этого не сделать, возможны случаи, когда реакции от последующих срабатываний будут смешиваться с реакциями текущего срабатывания.
Наиболее прост в реализации второй способ. По сравнению с первым, он гарантирует, что следующее срабатывание произойдёт только после окончания текущего. По сравнению с третьим, он легче в реализации, так как не нужны дополнительные энтити.
Кроме того, если данная энтити управляет ещё одним триггером проверки, может возникать "паразитный" эффект - будто срабатывание произошло несколько раз. В таком случае помогут второй и третий способы (Enable/Disable и TouchTest).


Схожий метод применяется и для случаев, если реакция нужна по состоянию на конкретный момент, только по запросу извне. В данной ситуации нужно поступать следующим образом:
  • опрашивать состояние энтити управляющей командой TouchTest (извне);
  • в самой энтити сделать отклик на события-состояния: OnTouching и OnNotTouching.

Как только trigger_multiple получает команду TouchTest, он генерирует одно из двух событий (OnTouching либо OnNotTouching) в зависимости от результата проверки. В результате происходит выполнение соответствующих команд только по внешнему запросу.

Ещё раз: обратите внимание, что указанные два события-состояния не генерируются до тех пор, пока не получена управляющая команда TouchTest. То есть они не происходят "сами по себе".




Объединение последовательности управляющих команд ("функция")

Роль функции играет энтити logic_relay, которая объединяет несколько скриптов (реакций) в одно целое. Смысл объединения в том, что задав последовательность реакций всего один раз, можно вызывать её откуда и сколько угодно в пределах карты. Само собой, при этом нужно повторять только одну команду - для logic_relay.
Ещё одно полезное свойство logic_relay состоит в том, что её можно "включать" (Enable) и "выключать" (Disable), избегая переделки множества подчинённых скриптов. Когда энтити "выключена", можно не заботиться, что на неё подаются "лишние" команды (разумеется, кроме включения). Такие команды может подавать, например, триггер, реагирующий каждый раз на пересечение его игроком. Но пока logic_relay отключена, дальше её проверки действие не пойдёт.
Единственный важный недостаток энтити - невозможно задавать параметры "функции", от которых будут меняться реакции. Но это вообще особенность скриптов Source.

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

Разумеется, в роли logic_relay может выступать любая другая подходящая энтити. Но раз уж специально выделена эта, то имеет смысл использовать именно её (она "затóчена" для подобного использования).




Пользовательские управляющие команды (FireUser) и события (OnUser)

Это дополнительный метод облегчения процесса создания скриптов (очень удобен в некоторых случаях). Он доступен у любой энтити, которая вообще использует систему ввода-вывода (Inputs/Outputs).
Работает следующим образом: в случае отправки запроса FireUser1…FireUser4 энтити, она генерирует событие OnUser1…OnUser4, соответственно. Получается расширение функциональности любой пригодной энтити четырьмя своеобразными триггерами.

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

Пример: пусть два npc_combinegunship летят по одной и той же цепочке из path_track-ов; долетев до четвёртого по счёту path_track, первый должен полететь налево, второй - направо. Само собой, левое и правое направление заданы отдельными цепочками (Л) и (П), которые начинаются вблизи четвёртого элемента исходной цепочки.
Тогда для четвёртого path_track исходной цепочки задаётся реакция вида:
OnPass : !activator : FireUser1 (при прохождении данной энтити посылается команда той энтити, которая активировала данное событие).
Для первого npc_combinegunship задаётся реакция вида:
OnUser1 : !self : FlyToSpecificTrackViaPath : <конец (Л)> (по событию OnUser1 этот npc_combinegunship получает конечный path_track левой цепочки и начинает лететь к нему вдоль нового пути).
Для второго npc_combinegunship, соответственно, задаём:
OnUser1 : !self : FlyToSpecificTrackViaPath : <конец (П)> (по событию OnUser1 этот npc_combinegunship получает конечный path_track правой цепочки и начинает лететь к нему вдоль нового пути).
Вот, собственно, и всё: npc_combinegunship "отдают команды сами себе", когда проходят нужную точку. Ради этого не потребовалось даже специально задавать имена подконтрольным энтити!

Сначала может показаться, что предложено излишне мудрёное решение задачи, но если Вы решите перенести срабатывание из четвёртого элемента в любой другой, нужно будет перенести всего одну реакцию (OnPass) - в реакциях npc_combinegunship ничего менять не надо.
Возможно, данный пример выглядит неубедительно; тогда попробуйте сделать поезд/лифт, который останавливается и открывает/закрывает двери в пределах одной карты несколько раз (т.е. выполняет одни и те же действия). Если задавать поведение через реакцию поезда/лифта, понадобится несколько раз (на каждую остановку) триггерами задавать одно и то же. Если поведение задаётся последовательностью реакций в каждой точке останова, получится много лишних (с точки зрения оптимизации) повторяющихся команд. Если же поведение задаётся способом FireUser/OnUser - выйдет очень просто и наглядно (как с "чёрным ящиком"): всего по одной реакции в каждой точке и последовательность реакций всего для одной энтити; да ещё и можно задавать четыре вида остановок.

Примечание: ключевое слово !activator в описании любой реакции указывает на энтити, которая запустила данную цепочку реакций на основе проверок в программном коде движка. Ключевое слово !self указывает на энтити, для которой описывается данная реакция (т.е. указатель на саму себя). Ключевое слово !caller указывает на энтити, которая вызвала текущую реакцию подачей управляющей команды; т.е. это предыдущая энтити в цепочке реакций (в приведённом выше примере для каждого npc_combinegunship это четвёртый path_track исходной цепочки).




Подсчёт событий (условий)

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

Простое использование math_counter подразумевает проверку только его граничных значений (или даже одной границы). Ограничения задаются атрибутами Minimum Legal Value и Maximum Legal Value - нижняя и верхняя границы, соответственно.
Реакция происходит на события OnHitMin/OnHitMax, OnGetValue. Соответственно, для того, чтобы вызвать конкретное событие, счётчик можно увеличивать (Add), уменьшать (Subtract), умножать (Multiply) или делить (Divide) на целое число. Кроме того, есть возможность проверять текущее значение (GetValue) - эта команда вызывает событие OnGetValue, для которого можно задать реакцию на конкретное значение.
Если корректно задавать ограничения, переполнения math_counter никогда не происходит, так как при выходе за допустимую границу счётчик просто останавливается на граничном значении. Но он не "замораживается", и последующие изменения в обратную сторону работают как обычно - от текущего (граничного) значения.




Элементы случайности

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

Для выдерживания случайной паузы служит энтити logic_timer, в которой предусмотрены не только строгие интервалы.
Принцип работы logic_timer: как только становится активной (Enable) - начинает отсчёт времени от заданного числа (с п.точкой) до нуля. Если задан параметр Use Random Time, начальное значение для отсчёта времени выбирается случайно в диапазоне от Minimum Random Interval до Maximum Random Interval.
По достижении нуля генерируется событие OnTimer, а затем энтити возвращается в первоначальное состояние, и снова начинается отсчёт времени.

Использование logic_timer предполагает 2 основных случая:
  • когда нужен автономный таймер, срабатывающий периодически или время от времени;
  • когда нужно выдержать паузу случайной длительности.
Естественно, для строго заданной паузы более экономно использовать отсрочку времени подачи управляющей команды (поле After a delay in seconds of), вместо добавления лишней энтити logic_timer.


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

Оптимальное построение скриптов со случайной паузой:
  • logic_relay с подачей серии команд, где последняя активирует logic_timer;
  • logic_timer по событию OnTimer выдаёт команду Trigger для следующей logic_relay (а затем деактивируется);
и так далее.

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


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




Полезные советы

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

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

    Ещё пример: представьте себе, что бомбы-липучки доктора Магнуссона (Episode Two) или приятели-кубы (Portal, Portal 2) доставляются ограниченное количество раз (например, единожды). Приятно будет играть? Ну, разве что в качестве "призовых уровней". А фактически это будет в некотором роде издевательство над игроком, несмотря на то, что "так более реалистично". Хотя эти скрипты весьма легко сделать…

    С другой стороны: один и тот же разговор между двумя NPC вряд ли будет уместен к повтору. Так что можно не беспокоиться и делать подобные скриптовые сцены "одноразовыми".


    Примечание: универсальность и многократный перезапуск скрипта, который содержит группу реакций, в своей основе легко реализуется через logic_relay или FireUser/OnUser. Остаётся только отслеживать нюансы каждой энтити (например, невозможность повторного использования) и при необходимости реализовывать задуманное обходным путём.





Статьи (рус):
Статьи (eng):

перейти к общему списку

Номер статьи: 40

Сборник полезной информации по созданию модификаций на движке Valve Source Engine (игры Half-Life 2, Episode One, Episode Two)