Стейты
Понятие стейтов (или состояний) используется программистами игр уже достаточно давно. В настоящее время данная технология известна под названием "state machine programming" и представляет собой естественный способ моделирования сложного поведения объектов. На интуитивном уровне стейты легко понять - например представим монстров в какой-либо игре. Их поведение можно разделить на несколько состояний, типа: "ожидание игрока", "нападение на появившегося игрока", "насильственная смерть от рук последнего :)" и т. д. чем больше состояний, тем более реальней и естественней моделируется поведение монстряги. Разумеется стейты применяются не только для реализации AI но и вообще любых объектов (если в этом есть необходимость конечно). Казалось бы все просто, однако реализация на уровне кода подобных вещей не только сложна, но и еще довольно "муторна", приходится учитывать множество нюансов и вылавливать кучу багов :( Вот тут на помощь и приходит поистине уникальная возможность Анреаловского движка - поддержка реализации стейтов на уровне языка UnrealScript! А вот что это означает мы дальше и рассмотрим.
Для начала рассмотрим основные "аксиомы" использования стейтов в ускрипте:
Прежде чем перейти непосредственно к самим стейтам, важное замечание насчет преимуществ и недостатков использования стейтов в ускрипте:
В доке используется одно соглашение - функции определенные в классе, но не входящие в стейты называются глобальными функциями.
Общая структура стейта следующая:
// Объявление состояния state SomeState { // Блок функций, используемый в состоянии, например Touch(), Bump() и т.д. // Функций может быть сколько угодно, либо они могут вообще отсутствовать function SomeFunction1() { // Какой-то код } function SomeFunction2() { // Какой-то код } // Блок операторов, каждому блоку операторов соответствует одна метка // Если операторов нет, то метки не нужны Label1: // Какой-то код Label2: // Какой-то код } // Конец определения состояния |
Пояснения. Стейт объявляется при помощи зарезервированного слова state за которым идет название состояния. Движок определяет начальное состояние исходя из значения переменной InitialState, если значение InitialState не указано то выбирается состояние, перед которым указано слово auto, например:
auto state StartState; // Стартовое состояние
|
Обратите внимание, в теле самого стейта операторы должны следовать за какой-то меткой. Метка - это любой идентификатор, однако метка Begin - считается специальной, стейт начинает выполняться именно с этой метки, например:
// Пример простого класса, который выводит Hello world в лог class ex_state extends actor; // Стартовое состояние auto state StartState { Begin: // Стартовая метка Log("Hello world!!!"); // Выводим в лог сообщение Sleep(2.0); // Латентная функция - выполнение кода ЭТОГО стейта задерживается на 2 с. Goto 'Begin'; // Переход в текущем стейте на метку "Begin" } |
Обратите внимание на оператор Goto - переход на указанную метку (в данном случае Begin). Также обратите внимание на функцию Sleep - это латентная функция, и ее назначение - задерживать код исполнения на определенное время, как уже было сказано, такого рода функции могут применяться только в стейтах.
Теперь перейдем к определению функций в стейтах, тут все достаточно просто - объявляете нужную функцию в стейте и все дела :) Вот другая реализация вышеприведенного примера:
// Пример простого класса, который выводит Hello world в лог class ex_state2 extends actor; // Стартовое состояние auto state StartState { function Timer() { Log("Hello world!!!"); // Выводим в лог сообщение } // Стартовая метка Begin: SetTimer(2.0, True); // Устанавливаем вызовы таймера на двухсекундный интервал } |
Эта одна из важных особенностей стейтов - функции, определяемые в одном состоянии, выполняются только когда активно данное состояние! Вы можете определить десять состояний и в каждом из них по функции Timer(), но в каждый момент времени, выполняться будет только Timer текущего состояния.
Выше Вы уже встречались с оператором Goto, сейчас мы подробней рассмотрим goto команды.
Goto 'label' - переход к метке label в пределах данного стейта.
Goto('') - остановка выполнения кода стейта до тех пор, пока не произойдет переход в новое состояние или на другую метку.
GotoState('SomeState' , 'SomeLabel') - переход в другое состояние SomeState к метке SomeLabel. Если метка не указана, то переход осуществляется к метке Begin. GotoState можно вызывать как из стейт-функций, так и из обычных (глобальных) функций в коде класса. В последнем случае переход к новому состоянию происходит не сразу, а только при возвращении управления стейту.
GotoState('') - переход в "не стейтовое" состояние (no-state), т.е. выполняться будут только глобальные функции.
Как только осуществляется переход в новое состояние (например, по GotoState) движок игры автоматически вызывает две функции: BeginState() и EndState(), Вы можете использовать эти функции для своих целей:
state SomeState { // Вызывается при старте выполнения SomeState function BeginState() { // Какой-либо код по Вашему усмотрению } // Вызывается при завершении выполнения стейта function EndState() { // Какой-либо код по Вашему усмотрению } } |
Также вот небольшой код как итог по goto командам. Создайте отдельный пак StateEx и в нем класс ex_state3. Скомпилируйте его и запустите УТ, затем в консоли введите summon StateEx.ex_state3. Дальше действовать по обстоятельствам :)
class ex_state3 extends Actor; function PostBeginPlay() { BroadcastMessage("PostBeginPlay called"); SetTimer(3.0,True); } function Timer() { BroadcastMessage("Global timer executing..."); } auto state Idle { function Touch( actor Other ) { BroadcastMessage("Going to DisableState..."); Sleep(2.0); GotoState('DisableState'); BroadcastMessage("I have gone to the DisableState"); } function Timer() { BroadcastMessage( "I am idle..." ); } Begin: SetTimer(5.0,True); } state DisableState { Begin: BroadcastMessage("Disabling state code"); Sleep(3.0); GotoState(''); } defaultproperties { DrawType=DT_Mesh; Mesh=LodMesh'UnrealShare.Skaarjw' } |
Как уже упоминалось выше - латентные функции доступны только в стейтах. Основных функций три:
Вот показательный пример применения latent функций в классе Skaarj (отрывок кода).
// Если есть враг и умение дистанционной атаки (RangedAttack) if ( bHasRangedAttack && (Enemy != None) ) { // Поворачиваемся к врагу, TurnToward тоже latent Функция TurnToward(Enemy); // Ждем конца анимации модели... FinishAnim(); // ...затем проверяем возможность атаки на врага if ( CanFireAtEnemy() ) { // Запускаем соответствующую анимацию модели PlayRangedAttack(); // Опять ждем конца анимации... FinishAnim(); } // Вызываем врага на битву :) PlayChallenge(); // И дожидаемся окончания анимации... FinishAnim(); } |
Также в классе Engine.Pawn определен ряд других полезных функций, например MoveTo, MoveToward, WaitForLanding и т.д. (можете сами посмотреть, перед объявлением латентных функций стоит модификатор latent).
Разумеется, в технологии стейтов не обошлось и без концепций объектно-ориентированного программирования, на чем основан весь UnrealScript. Просто запомните следующие правила (впрочем, они довольно естественны):
Рассмотрим простейший пример.
// Родительский класс class SomeParent extends Actor; // Глобальная функция function MyInstanceFunction() { log( "Executing MyInstanceFunction in parent class" ); } // Наш стейт state MyState { // Функция в стейте function MyStateFunction() { Log( "Executing MyStateFunction in parent class" ); } // Стартовая метка Begin Begin: Log("Beginning MyState in parent class"); } |
Класс-потомок.
// Класс-потомок class ChildClass extends SomeParent; // Перегружаем глобальную функцию родителя function MyInstanceFunction() { Log( "Executing MyInstanceFunction in childclass" ); } // Перегрузка стейта state MyState { // Перегрузка функции в стейте function MyStateFunction() { Log( "Executing MyStateFunction in childclass" ); } // Перегрузка метки стейта Begin: Log( "Beginning MyState in ChildClass" ); } |
Как уже говорилось выше, могут возникнуть проблемы в понимании того какая именно функция вызывается, т.к. одна и та же функция может быть объявлена как в разных классах и быть одновременно и глобальной и стейт функцией. Чтобы не запутаться, вот два правила от дяди Тима :)
Кратко напомню, что Вы можете вызывать соответствующие версии функций, используя следующие зарезервированные слова:
Комбинировать эти слова не разрешается, т.е. Вы не можете сделать так: Super(Actor).Global.Touch
Дополнительные возможности стейтов
Если стейт объявлен только в текущем классе, то Вы можете использовать слово "expands" чтобы объявить стейт расширяемым по отношению к другому стейту в этом же классе. Это полезно в тех случаях, когда у Вас имеется группа стейтов незначительно отличающихся друг от друга и имеющих одинаковую функциональность. Например, состояния RangeAttacking (дистанционная атака) и MeleeAttacking ("рукопашная" схватка) имеют общее состояние Attacking. Общая структура такова.
class SomeClass extends Actor; // Базовое состояние Attacking state Attacking { // Здесь помещаете базовые функции... } // Более специализированное состояние MeleeAttacking state MeleeAttacking expands Attacking { // Здесь помещаете особенные для этого состояния функции } // Также специализированное состояние RangeAttacking state RangeAttacking expands Attacking { // Здесь помещаете особенные для этого состояния функции } |
Следующая особенная возможность стейтов - "игнорирование" определенных функций. Это полезно когда Вы не хотите, чтобы в стейте вызывались определенные функции типа Touch, Bump, SeePlayer и т.д.
// Отступаем... state Retreating { // В этом состоянии игнорируем вызовы следующих функций ignores Touch, UnTouch, MyFunction; // Реализация состояния... } |
Вот пожалуй и все. Базовые возможности стейтов мы рассмотрели. Добавлю что эта технология действительно великолепна, просто в начале достаточно непривычно использовать такого рода штуки.
Но по крайней мере изучить стоит.
Автор: Shadow
Mail: shadow_m777@mail.ru
Основная часть материала основана на библии ускриптера UnrealScript Language Reference плюс собственная импровизация :)
Замечание от автора: этот туториал будет обновляться.