UnrealScript. Справочное руководство. Часть вторая
Автор: Vincent Barabus
Расширенные функции языка
Таймеры
Состояния
Обзор состояний
Метки состояний и латентные функции
Правила наследования состояний
Расширенное программирование состояний
Репликация
Итерация (ForEach)
Спецификаторы вызова функций
Доступ к статическим функциям из переменной класса
Значения по умолчанию для пременных
Доступ к значениям по умолчанию для переменных
Доступ к значениям по умолчанию для переменных через ссылку на класс
Определение значений по умолчанию с использованием блока defaultproperties
Значения по умолчанию для структур
Динамические массивы
Переменная Length
Итераторы динамических массивов
Интерфейсы
Функции делегаты
Встроенные классы
Поддержка метаданных
Обзор метаданных
Использование нескольких спецификаций метаданных
Доступные спецификации метаданных
Расширенные технические вопросы
Реализация UnrealScript
Вопросы двоичной совместимости UnrealScript
Технические примечания
Стратегия программирования на UnrealScript
Расширенные функции языка
Таймеры
Таймеры используются в качестве механизма для планирования выполнения, повторения или завершения событий. Таким образом, актор может установить таймер, зарегистрировав себя в игровом движке и установив необходимый порядок вызова функции Timer().
Таймеры UnrealScript реализованы как простой массив структур внутри каждого актора (актор может иметь несколько таймеров в стадии согласования). Структура включает в себя количество времени, оставшегося до истечения таймера, функцию для вызова по истечении определенного срока и т.д.
Обычно игровой цикл обновляет состояние каждого актора один раз за кадр, а часть функции Tick() каждого актора в включает в себя вызов функции UpdateTimers(), которая проверяет все таймеры на истечение времени и вызывает функции UnrealScript, соответствующие таймерам.
Частота таймеров ограничивается временем одного тика и не зависит ни от аппаратных ресурсов, ни от ресурсов операционной системы. Код таймеров реализован на C++, так что вы можете безопасно обновлять сотни таймеров UnrealScript без каких-либо причин для беспокойства. Конечно, вам не следует устанавливать обновление всех таймеров каждый тик, так как таймеры выполняют функции (относительно медленного) кода сценариев.
Состояния
Обзор состояний
Исторически сложилось, что программисты игр используют концепцию состояний. Состояния (также известные как "машины программирования состояний") являются естественным путем управления поведением сложных объектов. Однако, пока поддержка состояний не была реализована на уровне языка UnrealScript, разработчикам для реализации состояний проиходилось создавать конструкции "switch" на языке C или C++. Такой код трудно было писать и обновлять.
Теперь UnrealScript поддерживает состояния на уровне языка
В UnrealScript каждый актор, расположенный в игровом мире, всегда находится в одном и только одном состоянии. Его состояние отражает действия, которые он должен выполнить. Например, перемещение кисти включает несколько состояний, например, "StandOpenTimed" и "BumpOpenTimed". Для объектов Pawn есть несколько состояний, таких как "Dying", "Attacking" и "Wandering".
В языке UnrealScript вы можете писать функции и код, относящиеся к определенным состояниям. Эти функции вызываются только тогда, когда актор пребывает в определенном состоянии. Например, при разработке сценария монстра, вы реализуете функцию "SeePlayer", которая вызывается в тот момент, когда монстр "видит" игрока. "Увидев" игрока монстр перейдет из состояния "Wandering" в состояние "Attacking", начав атаку.
Самый простой способ реализовать вышеописанное - это определить некоторые состояния (Wandering и Attacking), и написать различные версии "Touch" в каждом состоянии. Язык UnrealScript поддерживает эту идею.
Перед тем, как приступить к изучению состояний, обратите внимание, что существуют два основных преимущества использования состояний и одно осложнение:
- Преимущество первое: Состояния обеспечивают простой способ записи функций, специфичных для состояний, так что вы можете вызывать одну и ту же функцию по-разному, в зависимости от того, что делает актор.
- Преимущество второе: Для состоянием вы можете написать специальный "код состояния" с использованием обычных команд UnrealScript плюс нескольких специальных функций, известных как "латентные функции". Латентная функция выполняется "медленно" и может вернуть результат по истечении определенного количества "игрового времени". Это позволяет выполнять программирование, основанное на времени выполнения, - которое дает вам ощутимые преимущества, не доступные в языках C, C++ или Java. То есть вы можете описывать события именно, как вы их представляете, например, вы можете написать сценарий, являющийся эквивалентом фразы: "открыть эту дверь через 2 секунды; пауза; воспроизвести этот звуковой эффект; открыть эту дверь; освободить этого монстра и заставить его атаковать игрока". Все это вы можете реализовать с помощью простого, линейного кода, а движок Unreal позаботится о деталях управления выполнения кода, основанного на времени выполнения.
- Осложнение: Вы можете иметь функции (например, Touch), переопределенные в нескольких состояниях, а также в дочерних классах, и вам необходимо будет выяснить, какие именно функции "Touch" будут вызываться в конкретной ситуации. Язык UnrealScript предусматривает правила, позволяющие четко разграничить этот процесс, и если вы создаете сложные иерархии классов и состояний, то эти правила вам необходимо знать.
Ниже приведены примеры состояний из сценария TriggerLight:
// Trigger turns the light on. state() TriggerTurnsOn { function Trigger( actor Other, pawn EventInstigator ) { Trigger = None; Direction = 1.0; Enable( 'Tick' ); } } // Trigger turns the light off. state() TriggerTurnsOff { function Trigger( actor Other, pawn EventInstigator ) { Trigger = None; Direction = -1.0; Enable( 'Tick' ); } }
Здесь мы объявили два разных состояния (TriggerTurnsOn и TriggerTurnsOff) и написали в каждом состоянии по версии функции Trigger. Хотя мы могли бы решить эту задачу и без использования состояний, применение состояний делает код гораздо более модульным и расширяемым: в UnrealScript, вы можете реализовать дочерний класс на базе существующего класса, добавить новые состояния, а также новые функции. Если вы попытаетесь реализовать этот сценарий без без использования состояний, то в результате код будет сложнее расширять.
Состояние может быть объявлено как редактируемое, что означает, что пользователь сможет установить состояние актора в UnrealEd. Объявить редактируемое состояние вы можете следующим образом:
state() MyState { //... }
Объявить нередактируемое состояние вы можете следующим образом:
state MyState { //... }
Вы можете также задать автоматическое или начальное состояние, в которое актер должен установлен, с помощью ключевого слова "auto". Автосостояние указывает, что все новые акторы при первом создании должны быть установлены в данное состояние:
auto state MyState { //... }
Метки состояний и латентные функции
В дополнение к функциям, состояние может включать одну или несколько меток с кодом на UnrealScript. Например:
auto state MyState { Begin: Log( "MyState has just begun!" ); Sleep( 2.0 ); Log( "MyState has finished sleeping" ); goto('Begin'); }
Приведенный выше код состояния выводит сообщение "MyState has just begun!", затем пауза в течение двух секунд, а затем выводит сообщение "MyState has finished sleeping". Самое интересное в этом примере - это вызов латентной функции "Sleep": эта функция возвращает значение не сразу после вызова, а по истечении определенного количества игрового времени. Латентные функции могут быть вызваны только из кода состояний, а не из функций. Латентные функции позволяют управлять сложными цепочками событий, которые предусматривают на некоторых этапах истечение определенного количества времени.
Код состояния начинается с определения метки, в приведенном выше примере метка называется "Begin". Метка указывает удобную точку входа в код состояния. Для меток кода состояний вы можете использовать любые имена, но метка "Begin" имеет особое значение: это начальная точка входа в код состояния.
Для всех акторов доступны три основные латентные функции:
- Sleep( float Seconds ) Останавливает выполнение кода состояния на определенное время.
- FinishAnim() Ожидает завершения текущей последовательности анимации. Эта функция позволяет писать сценарии, связанные с управлением последовательностями анимации, например, сценарии для анимации, управляемой ИИ (в отличие от анимации, управляемой временем). Реализация плавной анимации является основной целью системы ИИ.
- FinishInterpolation() ожидает завершения текущего движения InterpolationPoint.
В классе Pawn определены несколько важных ланентных функций, например, функции перемещения по игровому миру и функции краткосрочного движения. Их описание и примеры использования вы найдете в отдельных документах по реализации ИИ.
Три встроенных функции UnrealScript особенно полезны при написании кода состояния:
- Функция "Goto('LabelName')" (аналогичная оператору goto языков C, C++ и Basic) осуществляет переход к метке состояния, указанной параметром 'LabelName'.
- Специальная команда Goto('') останавливает выполнение текущего кода состояния. Выполнение не продолжится, пока вы не укажете новую метку для перехода.
- Функция "GoToState" переводит актор в новое состояние, и, опционально, к указанной метке нового состояния (если вы не укажете метку, то будет осуществлен переход к метке "Begin"). Вы можете вызвать GoToState изнутри кода состояния и переход к новому состоянию произойдет немедленно. Вы также можете вызвать GoToState из любой функции в актора, но в этом случае переход будет осуществлен только после завершения выполнения текущей функции.
Ниже приведен пример код состояния, отражающий описанные выше концепции:
// This is the automatic state to execute. auto state Idle { // When touched by another actor... function Touch( actor Other ) { log( "I was touched, so I'm going to Attacking" ); GotoState( 'Attacking' ); Log( "I have gone to the Attacking state" ); } Begin: log( "I am idle..." ); sleep( 10 ); goto 'Begin'; } // Attacking state. state Attacking { Begin: Log( "I am executing the attacking state code" ); //... }
Когда вы запустите эту программу, а затем коснетесь актар, вы получите вывод:
I am idle... I am idle... I am idle... I was touched, so I'm going to Attacking I have gone to the Attacking state I am executing the attacking state code
Убедитесь, что вы понимаете вышеприведенный код, он отражает важные аспекты применения функции GoToState: при вызове GoToState внутри функции переход не осуществляется немедленно, а только по завершении выполнения текущей функции.
Правила наследования состояний
В языке UnrealScript при создании класса, наследующего существующий класс, ваш новый класс наследует все переменные, функции и состояния базового класса. В этом необходимо тщательно разобраться.
Однако, помимо абстракции состояний, модель программирования языка UnrealScript включает дополнительные правила наследования и иерархии. Правила наследования следующие:
- Новый класс наследует все переменные из базового класса.
- Новый класс наследует все функции базового класса, не включенные в состояния. Вы можете переопределить любую из этих функций. Также вы можете добавлять совершенно новые функции, не включаемые в состояния.
- Новый класс наследует все состояния своего базового класса, в том числе функции и метки этих состояний. Вы можете переопределить любую из унаследованных функций, входящих в состояния, изменить любую из меток унаследованных состоянии, добавлять в состояния новые функции, а также добавлять в состояния новые метки.
Ниже приведен пример наследования состояний:
// Here is an example parent class. class MyParentClass extends Actor; // A non-state function. function MyInstanceFunction() { log( "Executing MyInstanceFunction" ); } // A state. state MyState { // A state function. function MyStateFunction() { Log( "Executing MyStateFunction" ); } // The "Begin" label. Begin: Log("Beginning MyState"); } // Here is an example child class. class MyChildClass extends MyParentClass; // Here I'm overriding a non-state function. function MyInstanceFunction() { Log( "Executing MyInstanceFunction in child class" ); } // Here I'm redeclaring MyState so that I can override MyStateFunction. state MyState { // Here I'm overriding MyStateFunction. function MyStateFunction() { Log( "Executing MyStateFunction" ); } // Here I'm overriding the "Begin" label. Begin: Log( "Beginning MyState in MyChildClass" ); }
Если у вас есть функция, которая реализована в глобально, в одном или нескольких состояниях и в одном или нескольких базовых классах, вы должны знать, какая именно версия функции будет вызвана в данном контексте. Правилами наследования и иерархии, разрешающие эти сложные ситуации, являются:
- Если для класса определено состояние, а в этом состоянии реализована функция, совпадающая с глобальной функцией этого класса (в текущем или в одном из базовых классов), то выполняться будет наиболее поздняя версия функции, определенной в состоянии.
- Если же с глобальной функцией не совпадает ни одна функция, реализованная для состояния (в текущем или в одном из базовых классов), то выполняться будет наиболее поздняя версия этой функции.
Расширенное программирование состояний
Если в производном классе вы не переопределяете состояние базового класса, то вы можете указать ключевое слово "extends", чтобы дополнить состояние базового класса в дочернем классе. Это полезно в тех случаях, когда у вас есть группа подобных состояний (например, MeleeAttacking и RangeAttacking), которые имеют общую функциональность с базовым состоянием Attacking. Состояние Attacking вы можете дополнить следующим образом:
// Base Attacking state. state Attacking { // Stick base functions here... } // Attacking up-close. state MeleeAttacking extends Attacking { // Stick specialized functions here... } // Attacking from a distance. state RangeAttacking extends Attacking { // Stick specialized functions here... }
В состоянии может дополнительно быть использован спецификатор ignores для игнорирования определенных функций базового состояния, например:
// Declare a state. state Retreating { // Ignore the following messages... ignores Touch, UnTouch, MyFunction; // Stick functions here... }
Функция GotoState('') позволяет вывести актор из всех состояний, то есть перевести его в состояние "no state". Когда актор пребывает в состоянии "no state", вызываются только его глобальные функции.
При каждом вызове функции GotoState для смены текущего состояния актора, движок может вызвать две специальные функции уведомления, если они определены: EndState() и BeginState(). EndState вызывается в текущем состоянии, непосредственно перед переходом в новое состояние, а BeginState вызывается сразу после перехода в новое государства. Эти функции предназначены для осуществления необходимых вам инициализации и очистки конкретных состояний.
Стек состояний
При при обычном изменении состояния вы переходите из одного состояния в другое, не имея возможности вернуться в предыдущее состояние. С применением стека состояний это возможно. Вызов функции PushState осуществляет переход в новое состояние, поместив текущее состояние на вершину стека. При этом выполнение текущего состояния останавливается. Вызов функции PopState осуществляет переход в предыдущее состояние и продолжает его исполнение от точки вызова PushState. Функция PushState по своему поведению похожа на латентные функции. Вызов из функции не будет прерывать выполнение кода (так же, как и вызов GoToState внутри функции), в то время как вызов ее из кода состояния приостановит выполнение текущего состояния до его исвлечения из стека (опять же, как и вызов GoToState внутри кода состояния).
Состояние может быть помещено в стек только один раз. Попытка поместить состояние стеке второрично не удастся. PushState работает как GotoState, она принимает имя состояния и, необязательно, метку точки входа в состояние. Новое состояние получит событие PushedState, текущее состояние получит событие PausedState. После вызова PopState текущее состояние получает события PoppedState, а новое состояние (то, что находилось рядом в стеке) получит событие ContinuedState.