Тема: Триггеры и скрипты Ключевые слова: триггер, скрипт, trigger, script, управление событиями См. также:
Если игрок не просто бегает и отстреливает врагов, а ещё и совершает (или наблюдает) некоторые действия "сверх положенного" -
происходящее на экране становится заметно интереснее.
К тому же, пока ИИ (настоящий искусственный интеллект, а не то, что его заменяет) в играх
не развит более-менее достойно, остаётся неплохая альтернатива - помогать NPC с помощью скриптов. То
есть заведомо проверять некие условия и задавать реакцию игры на них (отдавая команды различным
энтити).
Интересно отметить, что скриптом также можно с полным правом считать каждую функцию поведения энтити на уровне кода. Другое дело, что скриптование теряет значительную долю гибкости (но упрощается), если переходить на уровень маппинга. Здесь рассматривается скриптование именно на уровне маппинга. Для удобства построения скриптов Valve создала некое подобие объектно-ориентированного программирования. То есть можно создавать "функции" скриптов, делать "чёрный ящик" из конкретной энтити, условное ветвление и даже вводить свои собственные "команды". Замечание: далее по тексту цепочка реакций подразумевает множество реакций, связанных по типу Input-Output (конец текущей - начало следующей); последовательность реакций подразумевает множество реакций на одно и то же событие (когда реакции выполняются независимо друг от друга). Содержание:
Предварительные сведения Рассмотрим принципы работы системы ввода-вывода (Inputs/Outputs) энтити. По существу, каждая энтити представляет из себя закрытый объект ("чёрный ящик"). Всё, что позволяет Source, это установить какие-то внешние переменные (атрибуты и флаги), задать реакции на события данной энтити (Outputs), а в игре - посылать энтити управляющие команды (Inputs). Непосредственно залезть в работающий код энтити практически невозможно. Замечание: конечно, если использовать отладку программного кода, то вроде бы заглянуть и вмешаться можно. Но дело это кропотливое и хлопотное, поэтому в подавляющем большинстве случаев Вы едва ли будете тратить время на "ковыряние" в низкоуровневой механике скриптов. Так вот, чтобы снизить вероятность проблем с восприятием логики работы скриптов, представляю несколько интересных фактов:
Примеры механизма 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). Схожий метод применяется и для случаев, если реакция нужна по состоянию на конкретный момент, только по запросу извне. В данной ситуации нужно поступать следующим образом:
Как только 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 основных случая:
В идеальном случае logic_timer с выдержкой случайной паузы должен запускаться из той энтити, которая по замыслу разработчика выполняет последнюю команду из серии; причём запускаться по событию, которое произойдёт с данной энтити по ожиданиям разработчика. Реально такое возможно далеко не часто - элементарно, из-за отсутствия проверки такого события на уровне кода энтити. Оптимальное построение скриптов со случайной паузой:
В этом случае logic_timer играет роль дозатора, разделяющего "череду событий" на две части - "до" и "после" (для каждого таймера). Для выбора случайной реакции из группы подходит энтити logic_case. Для этого ей всего лишь надо отдать команду PickRandom - будет выбрано случайное событие из тех, которые были заданы вариантами. Если происходит множественная выборка, и значения не должны повторяться до тех пор, пока это возможно, используйте PickRandomShuffle (выбирает неповторяющиеся случайные значения, пока не кончатся заданные варианты). Полезные советы
Статьи (рус): Статьи (eng):
Номер статьи: 40
|