Script UDK: Стейты

(Материалы по UDK)

Модератор: KrisGames

Аватара пользователя
KrisGames

Универсал Коммандо Лидер
Рейнджер
Сообщения: 415
Рег. Ср апр 28, 2010 10:20 am
Награды: 3
Репутация: 242
Вклад в проект: 75
Откуда: Иваново
Благодарил (а): 9 раз
Поблагодарили: 2 раза

Script UDK: Стейты

Сообщение KrisGames » Пн фев 20, 2012 12:55 pm

Общие сведения

Понятие стейтов (или состояний) используется программистами игр уже достаточно давно. В настоящее время данная технология известна под названием "state machine programming" и представляет собой естественный способ моделирования сложного поведения объектов.

На интуитивном уровне стейты легко понять - например представим монстров в какой-либо игре. Их поведение можно разделить на несколько состояний, типа: "ожидание игрока", "нападение на появившегося игрока", "насильственная смерть от рук последнего :)" и т. д. чем больше состояний, тем более реальней и естественней моделируется поведение монстряги.

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

Вот тут на помощь и приходит поистине уникальная возможность Анреаловского движка - поддержка реализации стейтов на уровне языка UnrealScript! А вот что это означает мы дальше и рассмотрим.

Для начала рассмотрим основные "аксиомы" использования стейтов в ускрипте:
Стейты применимы к любым классам, в том числе и к акторам. Впрочем в большинстве случаев стейты используются именно в актор классах.

Актор может находится только в одном каком-то состоянии, которое отображает действие совершаемое объектом. Например, Pawn`ы могут находится в таких состояниях как "Dying", "Attacking", "Wandering" и т. д.

В каждом стейте Вы можете определять какие-либо функции, более того, Вы можете переопределять одну и ту же функцию для разных состояний. Например, Вы пишете скрипт для монстра который атакует игрока как только его увидит. Одно из решений - определить два состояния монстра (например, "Wandering" (безмятежное состояние) и "Attacking" (атакующее состояние)). В состоянии Wandering можно определить функцию SeePlayer при выполнении которой монстр перейдет в состояние Attacking (типа "издать боевой клич и ринуться на игрока" :). В свою очередь, в состоянии Attacking можно опять определить функцию SeePlayer для того, чтобы монстр продолжал атаковать и выполнял при этом какие-то дополнительные действия.

Стейты родительского класса наследуются потомками. Без комментариев. (Подробнее о наследовании стейтов см. ниже).

Прежде чем перейти непосредственно к самим стейтам, важное замечание насчет преимуществ и недостатков использования стейтов в скрипте:

Преимущество. Как уже было выше сказано, Вы можете определять одну и ту же функцию для разных состояний. Это очень упрощает сам код, каждое состояние изолировано от других и отображает одно определенное действие объекта.

Преимущество. Возможность использования функций задержек, т.е. при написании стейтов кроме обычных ускрипт операторов Вы можете использовать специальные латентные функции (latent functions - функции задержки). Латентные функции выполняются с некоторой "задержкой" (например нам надо дождаться пока не закончится текущая анимационная последовательность модели), что позволяет Вам делать так называемый "time-based" код, причем вся сложность реализации от Вас скрыта движком. О такого рода функциях см. ниже.

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

В доке используется одно соглашение - функции определенные в классе, но не входящие в стейты называются глобальными функциями.

Определение стейтов

Общая структура стейта следующая:

Код: Выделить всё

// Объявление состояния
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 команды.

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 функции

Как уже упоминалось выше - латентные функции доступны только в стейтах. Основных функций три:
Sleep (float Seconds) - задержка исполнения кода стейта на время Seconds.
FinishAnim() - задержка исполнения кода стейта пока не завершится текущая анимационная последовательность модели. Очень широко используется в pawn классах.
FinishInterpolation() - задержка исполнения кода стейта пока не завершится движение к точке InterpolationPoint. Пример применения - муверы.

Вот показательный пример применения 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" );

}


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

Если актор находится в стейте, и реализация какой-либо функции присутствует где-либо в стейте (как в самом классе, так и в родительских классах), то вызывается стейт версия данной функции наиболее близкая к текущему классу. Все понятно?

Иначе (если актор не в стейте, либо в стейте нет вызова данной функции) вызывается глобальная версия данной функции наиболее близкая к текущему классу.

Кратко напомню, что Вы можете вызывать соответствующие версии функций, используя следующие зарезервированные слова:
Global - вызов самой общей глобальной версии функции (идем к предкам :))
Super - вызов версии функции предка (функция может быть как глобальной, так и стейт функцией).
Super(ClassName) - вызов версии функции класса ClassName (функция может быть как глобальной, так и стейт функцией).

Комбинировать эти слова не разрешается, т.е. Вы не можете сделать так: 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;

// Реализация состояния...
}

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


По материалам rusudk.ru
ICQ : 470-451-451
Mail: KrisGames@yandex.ru

Вернуться в «Конструктор UDK»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 0 гостей

 

 

cron