Расширенные функции языкаТаймерыТаймеры используются в качестве механизма для планирования выполнения, повторения или завершения событий. Таким образом, актор может установить таймер, зарегистрировав себя в игровом движке и установив необходимый порядок вызова функции 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. Объявить редактируемое состояние вы можете следующим образом:
Объявить нередактируемое состояние вы можете следующим образом:
Вы можете также задать автоматическое или начальное состояние, в которое актер должен установлен, с помощью ключевого слова "auto". Автосостояние указывает, что все новые акторы при первом создании должны быть установлены в данное состояние:
Метки состояний и латентные функции
В дополнение к функциям, состояние может включать одну или несколько меток с кодом на 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.
Код: Выделить всё
state FirstState
{
function Myfunction()
{
doSomething();
PushState('SecondState');
// this will be executed immediately since we're inside of a function (no latent functionality)
JustPushedSecondState();
}
Begin:
doSomething();
PushState('SecondState');
// this will be executed once SecondState is popped since we're inside of a state code block (latent functionality)
JustPoppedSecondState();
}
state SecondState
{
event PushState()
{
// we got pushed, push back
PopState();
}
}
Воспользовавшись функцией IsInState, вы сможете проверить, находится ли определенное состояние в стеке. Эта функция проверяет только имя состояния и для них не может быть использована для проверки по имени базового состояния. Например:
Код: Выделить всё
state BaseState
{
...
}
state ExtendedState extends BaseState
{
...
}
Если активно состояние ExtendedState, то IsInState('BaseState') вернет false. Конечно же, вызов IsInState('BaseState', true) вернет истину, если BaseState в стеке.
Итерация (ForEach)Команда foreach языка UnrealScript упрощает работу с большими группами акторов, например, со всеми акторами на уровне или со всеми акторами на определенном расстоянии от указанного актора. "foreach" работает в сочетании с особой функцией, называемой "итератором" ("iterator"), назначением которой является перебор списка акторов.
Ниже приведен простой пример использования команды foreach:
Код: Выделить всё
// Display a list of all lights in the level.
function Something()
{
local actor A;
// Go through all actors in the level.
log( "Lights:" );
foreach AllActors( class 'Actor', A )
{
if( A.LightType != LT_None )
log( A );
}
}
Первым параметром во всех командах foreach является имя класса, определяющего тип акторов для поиска. Вы можете использовать этот параметр для ограничения диапазона поиска, например, перебирать только акторы, производные от класса Pawn.
Второй параметр команды foreach - это переменная, которой при каждой итерации цикла foreach присваивается ссылка на очередной актор из перебираемого списка.
Ниже приведены все функции итерации, которые работают с оператором "foreach".
AllActors ( class<actor> BaseClass, out actor Actor, optional name MatchTag )
Перебирает все акторы уровня. Если вы укажете дополнительный параметр MatchTag, то в поиск будут включены только акторы, переменная "Tag" которых соответствует указанному вами имени.
DynamicActors( class<actor> BaseClass, out actor Actor )
Перебирает все акторы, которые были созданы с начала запуска уровня, игнорируя те, что изначально размещены в уровне.
ChildActors( class<actor> BaseClass, out actor Actor )
Перебирает все акторы, принадлежащие указанному актору.
BasedActors( class<actor> BaseClass, out actor Actor )
Перебирает все акторы, использующие указанный актор в качестве базового.
TouchingActors( class<actor> BaseClass, out actor Actor )
Перебирает все акторы, которые касаются (или взаимопроникают) с указанным актором.
TraceActors( class<actor> BaseClass, out actor Actor, out vector HitLoc, out vector HitNorm, vector End, optional vector Start, optional vector Extent )
Перебирает все акторы, которые касаются линии, от ее начальной точки, определяемой параметром Start, до ее конечной точки, определяемой параметром End, с учетом степени касания, определяемой параметром Extent. При каждой итерации для HitLoc установливается координата касания, а для HitNorm - нормаль касания.
OverlappingActors( class<actor> BaseClass, out actor Actor, float Radius, optional vector Loc, optional bool bIgnoreHidden )
Перебирает все акторы, находящиеся в указанном радиусе от указанного места (или, если место не указано, то от местоположения указанного актора).
VisibleActors( class<actor> BaseClass, out actor Actor, optional float Radius, optional vector Loc )
Перебирает все акторы, видимые с указанного места (или, если место не указано, то от местоположения вызывающего актора).
VisibleCollidingActors ( class<actor> BaseClass, out actor Actor, float Radius, optional vector Loc, optional bool bIgnoreHidden );
Возвращает все сталкивающиеся (bCollideActors==true) акторы в пределах указанного радиуса, видимые с места, указанного Loc (или, если место не указано, то видимые от местоположения вызывающего актора). Параметр bIgnoreHidden позволяет игнорировать все скрытые акторы. Работает быстрее, чем AllActors(), поскольку этот итератор использует хэш столкновений.
CollidingActors ( class<actor> BaseClass, out actor Actor, float Radius, optional vector Loc );
Возвращает все сталкивающиеся (bCollideActors==true) акторы в пределах указанного радиуса от места, указанного параметром Loc (или, если место не указано, то от местоположения вызывающего актора). При достаточно малых радиусах работает быстрее, чем AllActors(), поскольку этот итератор использует хэш столкновений.
Обратите внимание: Все функции итерации являются методами классов, наследующих класс Actor. Если вам необходимо перебрать определенные классы, не наследующие класс Actor, то воспользуйтесь переменной, ссылающейся на класс, наследующий Actor, и следующим синтаксисом:
foreach ActorVar.DynamicActors(class'Pawn', P)
Таким образом, с помощью класса Interaction вы можете сделать следующее:
foreach ViewportOwner.Actor.DynamicActors(class'Pawn', P)
Обратите внимание: Итераторы теперь поддерживают и динамические массивы. Подробнее о динамических массивах читайте в разделе "Итераторы динамических массивов".
Спецификаторы вызова функцийВ сложных ситуациях при программировании вам часто будет необходимо обратиться к конкретной версии функции, а не к ее наиболее поздней реализации. Для этого в языке UnrealScript есть следующие ключевые слова:
Global Вызывает наиболее позднюю версию глобальной (не включенной в состояние) функции.
Super Вызывает соответствующую версию функции базового класса. Вызываемая функция может быть как функцией состояния, так и глобальной функцией, в зависимости от контекста.
Super(classname) Вызывает соответствующую версию функции, находящуюся в указанном классе, или версию ближайшего из базовых классов. Вызываемая функция может быть как функцией состояния, так и глобальной функцией, в зависимости от контекста.
Объединение нескольких спецификаторов вызова (например, Super(Actor).Global.Touch) является недопустимым.
Ниже приведено несколько примеров применения спецификаторов вызова:
Код: Выделить всё
class MyClass extends Pawn;
function MyExample( actor Other )
{
Super(Pawn).Touch( Other );
Global.Touch( Other );
Super.Touch( Other );
}
Также, например, функция BeginPlay() вызывается, когда актор входит в игровой процесс. Функция BeginPlay() реализована в классе Actor и включает некоторые важные операции, которые должны быть выполнены. Теперь, скажем, вы хотите переопределить функцию BeginPlay() в вашем новом классе MyClass, чтобы добавить новую функциональность. Чтобы сделать это безопасно, необходимо вызвать версию BeginPlay() из базового класса:
class MyClass extends Pawn;
Код: Выделить всё
function BeginPlay()
{
// Call the version of BeginPlay in the parent class (important).
Super.BeginPlay();
// Now do custom BeginPlay stuff.
//...
}
Доступ к статическим функциям из переменной классаСтатические функции из переменной класса могут быть вызваны следующим образом:
Код: Выделить всё
var class C;
var class<Pawn> PC;
class'SkaarjTrooper'.static.SomeFunction(); // Call a static function
// in a specific class.
PC.static.SomeFunction(); // Call a static function in a variable class.
class<Pawn>(C).static.SomeFunction(); // Call a static function in a
//casted class expression.
Значения по умолчанию для пременныхДоступ к значениям по умолчанию для переменныхUnrealEd позволяет дизайнерам уровней редактировать переменные "по умолчанию" класса объекта. Когда создается новый актор, все его переменные инициализируются значениями по умолчанию. Иногда это полезно для ручного сброса переменной в значение по умолчанию. Например, когда игрок бросает предмет инвентаря, код предмета инвентаря должен сбросить некоторые из значений актора в значения по умолчанию. В UnrealScript можно получить доступ к переменным по умолчанию класса с использованием ключевго слова "Default.". Например:
Код: Выделить всё
var() float Health, Stamina;
//...
// Reset some variables to their defaults.
function ResetToDefaults()
{
// Reset health, and stamina.
Health = Default.Health;
Stamina = Default.Stamina;
}
Доступ к значениям по умолчанию для переменных через ссылку на классЕсли у вас есть ссылка на класс (переменная типа class или class<classlimitor>), то вы можете получить доступ к свойствам по умолчанию для класса, на который она ссылается, не имея самого объекта этого класса. Этот синтаксис работает с любыми выражениями типа class.
Код: Выделить всё
var class C;
var class<Pawn> PC;
Health = class'Spotlight'.default.LightBrightness; // Access the default value of
// LightBrightness in the Spotlight class.
Health = PC.default.Health; // Access the default value of Health in
// a variable class identified by PC.
Health = class<Pawn>(C).default.Health; // Access the default value
// of Health in a casted class
// expression.
Определение значений по умолчанию с использованием блока defaultpropertiesВ дополнение к установке значений по умолчанию для свойств актора с использованией окна свойств в UnrealEd, вы также можете присвоить значения по умолчанию для переменных членов классов путем размещения специальных выоажений внутри блока класса defaultproperties.
В блоке defaultproperties не допустимо размещение операций, за исключением операций над динамическими массивами
Запятые могут быть размещены в конце каждой строки, но это не обязательно
Значения по умолчанию наследуются дочерними классами. Значения, указанные в defaultproperties дочернего класса переопределяют значения, указанные для базового классов.
Синтаксис Синтаксис блока defaultproperties немного отличается от стандартного синтаксиса UnrealScript
Простые типы (Ints, Floats, Bools, Bytes):
VarName=Value
Статические массивы:
ArrayProp(0)=Value1
ArrayProp(1)=Value2
или
ArrayProp[0]=Value1
ArrayProp[1]=Value2
Динамические массивы:
ArrayProp=(Value1,Value2,Value3)
или
ArrayProp(0)=Value1
ArrayProp(1)=Value2
ArrayProp(2)=Value3
или
ArrayProp.Add(Value1)
ArrayProp.Add(Value2)
ArrayProp.Add(Value3)
Имена
NameProp='Value'
или
NameProp=Value
Объекты
ObjectProp=ObjectClass'ObjectName'
Подобъекты
Begin Object Class=ObjectClass Name=ObjectName
VarName=Value
...
End Object
ObjectProperty=ObjectName
Структуры (включая векторы):
StructProperty=(InnerStructPropertyA=Value1,InnerStructPropertyB=Value2)
или
StructProperty={(
InnerStructPropertyA=Value1,
InnerStructPropertyB=Value2
)}
Обратите внимание: Внутри структуры значений по умолчанию некоторые типы используют несколько иной синтаксис.
Встраиваемые (inline) статические массивы должны быть объявлены как (Обратите внимание, что здесь для доступа к элементам массива используются скобки "[]", вместо круглых скобок "()"):
StructProperty=(StaticArray[0]=Value,StaticArrayProp[1]=Value)
Встраиваемые (inline) динамические массивы должны быть объявлены с использованием следующего синтаксиса:
StructProperty=(DynamicArray=(Value,Value))
Встраиваемые (inline) переменные имен должны быть заключены в кавычки:
StructProperty=(NameProperty="Value")
Операции над динамическими массивами. Они могут быть использованы для модификации содержимого динамического массива, который может быть унаследован от бызового класса.
Array.Empty - полностью очищает массив
Array.Add(element) - добавляет элемент к концу массива
Array.Remove(element) - удаляет элемент из массива, эта процедура может удалить все вхождения элемента
Array.RemoveIndex(index) - удаляет элемент по указанному индексу
Array.Replace(elm1, elm2) - заменяет elm1 на elm2. Все вхождения будут заменены. Если elm1 будет не найден, то выводится предупреждение.
Ознакомьтесь со следующим примером (основанном на Actor.uc):
Код: Выделить всё
defaultproperties
{
// objects
MessageClass=class'LocalMessage'
// declare an inline subobject of class SpriteComponent named "Sprite"
Begin Object Class=SpriteComponent Name=Sprite
// values specified here override SpriteComponent's own defaultproperties
Sprite=Texture2D'EngineResources.S_Actor'
HiddenGame=true
End Object
//todo
Components.Add(Sprite)
// declare an inline subobject of class CylinderComponent named "CollisionCylinder"
Begin Object Class=CylinderComponent Name=CollisionCylinder
// values specified here override CylinderComponent's own defaultproperties
CollisionRadius=10
CollisionHeight=10
AlwaysLoadOnClient=True
AlwaysLoadOnServer=True
End Object
//todo
Components.Add(CollisionCylinder)
CollisionComponent=CollisionCylinder
// floats (leading '+' and trailing 'f' characters are ignored)
DrawScale=00001.000000
Mass=+00100.000000
NetPriority=00001.f
// ints
NetUpdateFrequency=100
// enumerations
Role=ROLE_Authority
RemoteRole=ROLE_None
// structs
DrawScale3D=(X=1,Y=1,Z=1)
// bools
bJustTeleported=true
bMovable=true
bHiddenEdGroup=false
bReplicateMovement=true
// names
InitialState=None
// dynamic array (in this case, a dynamic class array)
SupportedEvents(0)=class'SeqEvent_Touch'
SupportedEvents(1)=class'SeqEvent_UnTouch'
SupportedEvents(2)=class'SeqEvent_Destroyed'
SupportedEvents(3)=class'SeqEvent_TakeDamage'
}
Значения по умолчанию для структурПри объявлении структуры в UnrealScript можно дополнительно указать значения по умолчанию для свойства структуры. При каждом использовании структуры в UnrealScript ее члены будут инициализироваться этими значениями. Синтаксис идентичен блоку defaultproperties для класса - единственное исключение в том, что вы должны назвать блок structdefaultproperties. Например:
Код: Выделить всё
struct LinearColor
{
var() config float R, G, B, A;
structdefaultproperties
{
A=1.f
}
};
При каждом определении переменной LinearColor в UnrealScript, значение ее члена A будет установлено в 1.f. Также важно учесть, что блок defaultproperties класса переопределяет свойства по умолчанию для структуры. Если в вашем классе есть переменная типа LinearColor, любое значение, присвоенное ей в блоке defaultproperties заменит значение, определенное в блоке structdefaultproperties.
Код: Выделить всё
defaultproperties
{
NormalColor=(R=1.f,B=1.f,G=1.f) // value of A will be 1.0f for this property
DarkColor=(R=1.f,B=1.f,G=1.f,A=0.2f) // value of A will be 0.2f for this property
}
Динамические массивыРанее мы рассмотрели статические массивы. Это означает, что размер (количество элементов в массиве) устанавливается во время компиляции и не может быть изменен. Динамические и статические массивы имеют следующие общие характеристики:
Одинаковое время доступа - затраты времени на доступ к элементам массива не зависят от размера массива
Нет ограничений на тим элемента - Вы можете создаывть массивы любых типов: целые числа, векторы, акторы и т.д. (с тем исключением, что логические типы действительны только для динамических массивов)
Поведение при доступе - Вы можете получить доступ по индексу к любому элементу массива, и наоборот, попытка получить доступ к элементу по индексу, выходящему за границы массива, будет неудачной.
Динамические массивы обеспечивают способ получить функциональность статического массива с возможностью изменения числа элементов во время выполнения, в целях удовлетворения меняющихся потребностей. Для использования динамических массивов, мы должны знать несколько их особенностей.
Первое - это объявление переменной. Объявление динамического массива очень похоже на объявление любой другой переменной UnrealScript (то есть имеет вид var/local type varname). Для объявления динамического массива указызается тип array, а затем тип массива, заключенный в угловые скобки. Если тип массива содержит скобки (например class<Actor>), то вы должны поставить пробел между закрывающей скобкой типа и закрывающей скобкой объявления массива, иначе компилятор интерпретирует двойную угловую скобку как оператор >>.
Примеры: Объявление динамического массива целых чисел с именем IntList:
var array<int> IntList;
Объявление динамического массива типа class<PlayerController> с именем Players:
var array<class<PlayerController> > Players;
При запуске сценария IntList будет содержать 0 элементов. Динамическими массивами поддерживаются методы, позволяющие добавлять элементы в массив, изымать элементы из массива, а также произвольно увеличивать или уменьшать длину массива. Синтаксис вызова этих методов (использованием переменной IntList): IntList.MethodName().
Для динамических массивов доступны следующие методы:Add(int Count): увеличивает длину массива на Count элементов, идентично вызову FArray::AddZeroed().
Insert(int Index, int Count): где Index - это индекс массива для вставки элементов, а Count число элементов для вставки. Все существующие элементы в этом месте массива смещаются вверх, а новые элементы создаются и вставляются в указанном месте. Вставка 5-ти элементов по индексу 3 смещтит вверх (на значение индекса) все элементы массива, начиная от индекс 3 на 5 элементов. Элемент, ранее распологавшийся по индексу 3 теперь будет расположен по индексу 8, элемент 4 теперь будет элементом 9 и так далее. Все добавленные элементы инициализируются значениями по умолчанию (ноль/нуль для всех типов, кроме структур, имеющих structdefaultproperties).
Remove(int Index, int Count): где Index - это начальный индекс для удаления элементов из массива, и Count - это число удаляемых элементов. Это позволяет удалить группу элементов из массива, начиная с любого допустимого индекса. Обратите внимание, что значения индексов оставшихся элементов (начиная с индекса, равного Index+Count) изменятся в меньшую сторону. Имейте это в виду, если вы храните значения индексов динамических массивов.
AddItem(Item): добавляет Item в конец массива, увеличивая длину массива на один элемент.
RemoveItem(Item): удаляет все экземпляры Item, используя линейный поиск.
InsertItem(int Index, Item): вставляет Item в массив по индексу Index, увеличивая длину массива на один элемент.
Find(...) - находит индекс элемента в массиве. Есть две версии Find: стандартный поиск для элемента по значению, и специализированная версия для поиска структуры по значению одного из свойств структуры
Find(Value): где Value - это значение для поиска. Возвращает индекс первого найденного элемента в массиве, который соответствует указанному значению, или -1, если это значение не было найдено. Value может быть представлено ??любым допустимым выражением.
Find(PropertyName, Value): где PropertyName - это имя свойства структуры для поиска (должно иметь тип 'Name'), а Value - это искомое значение. Возвращает индекс первой найденной структуры в массиве, свойство с именем PropertyName которой соответствует значению, указанному Value, или -1, если это значение не было найдено. Value может быть представлено ??любым допустимым выражением.
Sort(SortDelegate) - SortDelegate - это делегат для сортировки содержимого массива. SortDelegate должен иметь следующий вид:
delegate int ExampleSort(ArrayType A, ArrayType B) { return A < B ? -1 : 0; } // отрицательное возвращаемое значение указывает, элементы должны поменяться местами
Переменная LengthДинамические массивы имеют переменную, называемую Length, значением которой является текущая длина (количество элементов) динамического массива. Для получения доступа к Length, применительно к нашему массиву IntList используется запись: IntList.Length. Мы можем не только прочитать, но и непосредственно установить значение переменной Length, что позволяет нам изменить количество элементов в массиве. При изменении переменной Length изменяется и длина массива. Например, если мы установим IntList.Length = 5, а затем установим IntList.Length = 10, дополнительные 5 элементов, добавленных последней операцией, добавляются в конец массива с сохранением первоначальных 5 элементов и их значений. Если мы уменьшим длину, то элементы будут сняты с конца массива. Обратите внимание, что при добавлении элементов в массив операцией Insert() или путем увеличения длины, элементы инициализируются в значения по умолчанию для типов переменных (0 для целых, None для ссылок на класс и т.д.). Следует также отметить, что вы можете увеличить длину динамического массива, обратившить по большему индексу, чем текущее значение длины массива. Это позволяет увеличивать массив так же, как если бы вы установили большее значение для Length.
OldLength = Array.length
Array.Length = OldLength + 1
Array[OldLength] = NewValue
Array[Array.Length] = NewValue
Array.AddItem(NewValue)
- это эквивалентные формы одной и той же операции.
Заметим, однако, что вы не можете увеличивать длину массива и получать доступ к его членам одновременно. Выражение:
Array[Array.length].myStructVariable = newVal
не будет работать.
Предостережение: переменная Length динамического массива никогда не должна увеличиваться или уменьшаться операторами '++','--','+=' или'-=', и вы не должны передавать Length в функцию в качестве параметра (если функция может изменить ее значение). Выполнение этих операций приведет к утечке памяти и сбоям. Устанавливайте значение Length только через оператор '=' (или через обращение по большему индексу, чем текущее значение длины массива).
Обратите внимание: array<bool> не поддерживается!
Последнее замечание: динамические массивы не реплицируются. Вы можете обойти это с помощью функции, реплицирующей индексы динамического массива и соответствующие значения элементов. Однако, при этом вы также должны учитывать временное рассогласование между клиентом и сервером.
Итераторы динамических массивовДинамические массивы теперь поддерживает оператор 'foreach'. Базовый синтаксис следующий: 'foreach ArrayVariable(out ArrayItem,optional out ItemIndex) {}', при каждой итерации осуществляется инкремент индекса и возвращение элемента, а также возвращение индекса, если указан соответствующий параметр.
Код: Выделить всё
function IterateThroughArray(array<string> SomeArray)
{
local string ArrayItem;
local int Index;
foreach SomeArray(ArrayItem)
{
`log("Array iterator test #1:"@ArrayItem);
}
foreach SomeArray(ArrayItem,Index)
{
`log("Array iterator test #2:"@ArrayItem@Index);
}
}
Поддержка метаданныхВ игре и в редакторе функциональность может быть расширена с помощью свойств метаданных.
Обзор метаданныхПроизвольные метаданные могут быть связаны со свойством UnrealScript следующим образом:
Для переменной:
var float MyVar<TAG=VALUE>
Для перечисления:
enum EMyEnum
{
EME_ValA<TAG=VALUE>,
EME_ValB<TAG=VALUE>,
};
Использование нескольких спецификаций метаданныхВы можете использовать несколько спецификаций метаданных для одного свойства, разделив их символом |.
Например:var() LinearColor DrawColor<DisplayName=Draw Color|EditCondition=bOverrideDrawColor>;
Доступные спецификации метаданных
Ниже приведены теги, поддерживаемые в настоящее время, и их описание:
<ToolTip=TEXT_STRING>
Делает TEXT_STRING подсказкой, всплывающей при наведении мыши на соответствующее свойство в окне редактора свойств.
Обратите внимание: Недавно была добавлена поддержка комментариев вида /** VALUE */, которые автоматически переводятся компилятором сценариев в тег ToolTip метаданных.
<DisplayName=TEXT_STRING>
В окне редактора свойств имя свойства отображается как TEXT_STRING, а не настоящее имя свойства.
Пример:
Var() bool bEnableSpawning<DisplayName=Spawning Enabled>;
Внимание: Использование DisplayName для перечислений создаст проблемы, если вы измените параметр UPropertyInputCombo для сортировки перечислений в комбинированном списке редактора.
<EditCondition=ConditionalPropertyName>
Это позволяет менять статус редактируемости свойства в редакторе свойств в зависимости от значения другого (булевого) свойства.
Например, вы можете сделать следующие настройки в классе MyPackage.MyClass:
/** Enable or disable spawning */
Var() bool bEnableSpawning;
/** Set the rate at which AIs are spawned. Has no effect unless bEnableSpawning = TRUE */
Var() float RespawnsPerSecond<EditCondition=bEnableSpawning>;
После чего RespawnsPerSecond в редакторе будет серым, когда bEnableSpawning ложно. Это помогает сделать наборы свойств для дизайнеров менее запутанными.
Важно: Эти настройки метаданных требуют наличия контролируемой переменной (RespawnsPerSecond) и обязательное наличие настраиваемого элемента свойства (WxCustomPropertyItem_ConditionalItem).
Для их включения необходимо внести соответствующие изменения в файле Editor.ini:
[UnrealEd.CustomPropertyItemBindings]
CustomPropertyClasses=(PropertyPathName=" MyPackage.MyClass: RespawnsPerSecond ",PropertyItemClassName="WxCustomPropertyItem_ConditionalItem")
<FriendlyName=TEXT_STRING>
Используется Редактором UI .
<AllowAbstract>
При наличии в свойстве Class, выпадающие списки для редактирования свойств будут включать в себя и абстрактные классы. Если спецификация не указана, то списки содержат только конкретные (не абстрактные) классы. Для этой спецификации метаданных не нужно указывать такие значения, как True или False.
<AutoComment=BOOLEAN_VALUE>
При добавлении к свойству Kismet Sequence Action, свойство и его текущее значение будет автоматически появляться в сценарии в качестве комментария над действием. Чтобы увидеть это в действии поместите новую последовательность действий "Gate" в сценарий. В данном классе эта опция метаданных используется как bOpen и AutoCloseCount.
Расширенные технические вопросыРеализация UnrealScriptДля получения более подробной информации о том, как UnrealScript работает, посетите страницы Compile Process , Execution , Byte Code и UnrealScript Implementation .
Вопросы двоичной совместимости UnrealScriptUnrealScript разрабатывается таким образом, что классы в файлах пакетов могут развиваться в течение долгого времени без потерь двоичной совместимости. В данном случае двоичная совместимость означает, что "зависимые двоичные файлы могут быть загружены и слинкованы без ошибок"; наличие различных версий и модификаций ваших функций и классов - это уже отдельный вопрос. В частности, видами модификаций, которые могут быть произведены без риска потери совместимости, являются:
.uc файлы сценариев в пакете могут быть перекомпилированы без потери двоичной совместимости.
Добавление в пакет новых классов.
Добавление к классам новых функций.
Добавление к классам новых состояний.
Добавление к классам новых переменных.
Удаление из класса закрытых (private) переменных.
Другие преобразования, как правило, небезопасны, в том числе (но не ограничиваясь):
Добавление новых членов структур.
Удаление класса из пакета.
Изменение типа любой переменной, параметра или возвращаемого значения функции.
Изменение числа параметров функции.
Технические примечанияСборка мусора. Все объекты и акторы в Unreal обрабатываются системой сборки мусора аналогично Java VM. Сборщик мусора Unreal использует функциональность сериализации класса UObject и рекурсивно определяет ссылки на другие объекты каждого активного объекта. В результате, объект не удаляется явно, сборщик мусора отслеживает их использование и удаляет объект только в случае его неиспользования. Этот подход имеет побочный эффект в виде скрытого удаления неиспользуемых объектов, однако это гораздо более эффективно, чем подсчет ссылок и удаление отдельных объектов. Подробнее читайте на странице Сборка мусора .
UnrealScript основан на байт-коде. Код языка UnrealScript компилируется в серии байт-кода, похожие на серии P-кода или байт-кода Java. Это делает UnrealScript независимым от платформы, позволяет осуществлять перенос компонентов клиента и сервера Unreal на другие платформы, то есть на Macintosh или на Unix.
Unreal, как виртуальная машина. Движок Unreal можно рассматривать как виртуальную машину для 3D-игр, как и язык и встроенные иерархии классов Java составляют виртуальную машину для сценариев веб-страниц. Виртуальная машина Unreal изначально портативна (благодаря разделению кода, зависимого от платформы, на отдельные модули) и расширяема (благодаря расширяемой иерархии классов). Однако, у нас нет никаких планов на документирование Unreal VM в объеме, необходимом для ее реализации других на платформах сторонними разработчиками.
Компилятор UnrealScript является трехпроходным. В отличие от C++, компиляция сценариев UnrealScript осуществляется в три разных прохода. В первый проход обрабатываются переменные, структуры, перечисления, константы, состояния, обявления функций и строится каркас каждого класса. На втором проходе код сценариев компилируется в байт-код. Два прохода позволяют собрать и связать сложные иерархии сценариев с круговыми зависимостями без отдельной фазы линковки. Третий проход анализирует и импортирует свойства по умолчанию для классов с использованием значений, указанных в блоке defaultproperties .uc файла.
Устойчивое состояние актора. Важно отметить, что в играх на двежке Unreal пользователь может сохранять игру в любое время, а состояния всех акторов, включая состояния выполнения их сценариев, могут быть сохранены только в то время, когда все акторы находятся на низком уровне стека UnrealScript. В целях устойчивости введено ограничение: латентные функции могут вызываться только из кода состояний. Код состояния выполняется на низком уровне стека, и, таким образом, состояние может быть легко сериализовано. Код функций, особенно, код встроенных функций на C++, может находиться на любом уровне стека, и их состояние не всегда возможно сохранить на диске, а затем восстановить.
Файлы Unreal имеют встроенный (native) двоичный формат. Файлы Unreal содержат индексы, сериализованные в дамп объектов, в частности, в пакеты Unreal. Файлы Unreal аналогичны DLL, они могут включать в себя ссылки на другие объекты, хранящиеся в других файлах Unreal. Такой подход сокращает времян загрузки (каждый пакет не загружается более одного раза), позволяет распространять контент Unreal в подготовленных "пакетах" через Интернет.
Почему UnrealScript не поддерживает статические переменные. C++ поддерживает статические переменные потому, что берет свои корни от низкоуровневого языка. Язык Java поддерживает статические переменные по причинам, которые кажутся не очень хорошо продумаными. Но таким переменным нет места в языке UnrealScript из-за неопределенностей, связанных с сериализацией, выводом и множеством уровней: статические переменные должны иметь "глобальные" семантики, а это означает, что все статические переменные во всех активных уровнях Unreal имеют одинаковое значение. Должны ли они быть в пакете? Должны ли они быть на уровне? Если да, то как они должны быть сериализованы: с классом в его .u файл, или с уровнем в .unr файл? Являются ли они уникальными для базового класса или модифицированные варианты классов имеют свои собственные значения статических переменных? В UnrealScript мы обошли эти проблемы, отказавшись от статических переменных, как от особенности языка, и позволили программистам управлять глобальными переменными путем создания классов. Если вам нужны переменные, которые доступны для всех объектов уровня, то вы можете создать новый класс для хранения этих переменных и, при этом, все переменные будут сериализованы вместе с уровнем. Таким образом исключается неопределенность. Примерами таких классов являются LevelInfo и GameInfo.
Стратегия программирования на UnrealScriptЗдесь мы рассмотрим несколько моментов, касающихся эффективной разработки кода на UnrealScript, использования сильных сторон UnrealScript, и способов предотвращения возможных ошибок.
Код на UnrealScript медленее, чем код на C или C++. Обычная программа на C++ работает примерно в 20 раз быстрее, чем сценарий на UnrealScript. Философия программирования при написании наших собственных сценариев заключается в следующем: создавать сценарии, которые почти всегда простаивают. Другими словами, используйте UnrealScript только для обработки "интересных" событий, реакции на которые вы хотите настроить, а не для выполнения механических задач, таких как расчет движения, которые решаются физической подсистемой движка Unreal гораздо эффективнее.
Например, при написании сценария снаряда обычно пишутся функции HitWall(), Bounce() и Touch (), описывающие действия, выполняемые при наступлении основных событий. Таким образом, 95% времени сценарий для вашего снаряда не выполняется, а только ждет уведомления от физической подсистемы. Такой подход по своей сути очень эффективен. Хотя UnrealScript гораздо медленнее, чем C++, код на UnrealScript в среднем занимает 5-10% процессорного времени.
Используйте как можно больше латентных функций (таких, как FinishAnim и Sleep). Основной поток ваших сценариев состоит из них, вы создаете код для управления анимацией или код, выполнение которого основано на течении времени. Подобный код является достаточно эффективным в UnrealScript.
Во время тестирования сценариев регулярно заглядывайте в журнал Unreal. Система исполнения сценариев UnrealScript часто выводит в журнал полезные предупреждения, которые уведомляют о наличии нефатальных проблем.
Будьте осторожны с кодом, который может привести к бесконечной рекурсии. Например, команда "Move" перемещает актор и вызывает функцию Bump() при столкновении. Поэтому, если вы используете команду Move в функции Bump, вы рискуете получить бесконечную рекурсию. Будьте осторожны. Бесконечные рекурсии и бесконечные циклы - это две ошибки, которые UnrealScript не обрабатывает корректно.
Создание и уничтожение акторов на стороне сервера - это довольно дорогостоящие операции, и крайне дорогостоящие операции в сетевой игре, потому что создание и уничтожение расходует пропускную способность сети. Используйте их разумно и рассматривайте актор как "тяжеловесный" объект. К примеру, не пытайтесь создавать системы частиц путем создания 100 уникальных акторов и их отправления по различным траекториям с использованием физической подсистемы. Это будет ОЧЕНЬ медленно.
Используйте объектно-ориентированные возможности UnrealScript как можно более полно. Создание новой функциональности путем переопределения существующих функций и состояний приводит к чистому коду, который легко модифицировать и легко интегрировать для разработки с участием других людей. Избегайте использования традиционных методов C, при обработке состояний объектов и акторов не используйте конструкции switch(), потому что такой код бесконтрольно разрастается по мере добавления новых классов.
.u пакеты UnrealScript собираются строго в порядке, установленном в списке EditPackages .ini файла, при этом каждый пакет может ссылаться только на объекты, входящие в состав текущего пакета, в состав ранее скомпилированных пакетов, но никогда не может ссылаться на пакеты, собираемые после текущего. Если вы обнаружите возникновение циклических ссылок между пакетами, то есть два решения:
Пакет с набором базовых классов ставится на сборку в первую очередь, а за ним пакет с набором дочерних классов. Также убедитесь, что базовые классы никогда не ссылаются на дочерние классы. В этом заключается хорошая практика программирования и это обычно работает.
Заметим, что если класс C должен ссылаться на класс или объект O в пакете, компилируемом позднее, то вы можете разбить класс на две части: абстрактный базовый класс определить как С (но не включать значения по умолчанию для его переменных в свойства по умолчанию), а производный класс D поместить во второй пакет, для которого указываются правильные значения по умолчанию.
Если два .u пакета неразрывно связаны между собой ссылками, то объедините их в один пакет. Это будет более разумно, потому что пакеты предназначены только для разделения кода на модули и вы не получите реальной пользы (например, экономии памяти) от разделения классов на несколько пакетов.
Взято с
GameDev.ruАвтор:
Vincent Barabus