UDN
Search public documentation:

UnrealScriptStatesCH
English Translation
日本語訳
한국어

Interested in the Unreal Engine?
Visit the Unreal Technology site.

Looking for jobs and company info?
Check out the Epic games site.

Questions about support via UDN?
Contact the UDN Staff

UE3 主页 > 虚幻脚本 >UnrealScript语言参考指南 > UnrealScript状态

UnrealScript 状态


概述


从游戏历史来说,游戏开发人员自从游戏发展到"pong"阶段后便开始使用状态这个概念。状态(也被称为“状态机编程”)是使复杂的物体行为更加容易管理的一种常用方式。然而,在UnrealScript出现之前,状态在编程语言的层次上还没有得到支持,需要开发人员基于物体的状态来创建C/C++"switch"语句。这样的代码是难于书写和更新的。

UnrealScript在编程语言的层次上支持状态。

在UnrealScript中,世界中的每个actor都处于并仅处于一个状态。它的状态反映了它要执行的动作。比如,移动画刷有几个状态,像"StandOpenTimed" 和 "BumpOpenTimed"。Pawns有一些,像"Dying"、 "Attacking"、 和 "Wandering"等状态。

在UnrealScript中,你可以针对一个特定的状态来书写函数和代码。这些函数和代码仅当actor在那个状态时被调用。比如,你正在写一个关于怪物的脚本,并正在思考如何处理"SeePlayer"函数。当您在到处巡逻的时候,您想攻击您看到的玩家。当您正在攻击玩家时,您想不停地进行连续攻击。

实现这个目的最容易的方式便是通过定义几个状态(Wandering(到处走动) 和 Attacking(攻击)),并且在每个状态写一个不同版本的"Touch"函数。UnrealScript支持这样做。

在对状态进行深入研究之前,你需要知道状态的两个主要好处和一个复杂性:

  • 好处: 状态提供了一个用于来书写针对状态的函数的简单方法,所以你可以根据actor的行为以不同的方式来处理同一个函数。
  • 好处: 使用状态,您可以使用完整的规范的UnrealScript命令和几个以"latent functions"著称的专用函数来书写专用的"state code(状态代码)"。一个latent函数的执行是比较慢的(也就是非阻塞的),并且可能要在一定的游戏时间过后才能返回。这可以使您进行基于时间的编程 – 这个主要的好处是无论是C, C++, 还是Java都没有提供的。也就是,你可以按照您想的方式来书写代码;比如,你书写一个脚本来表达"打开这个门; 暂停2s中; 播放这个音效;打开那个门;发放一个怪物来攻击玩家"。你可以通过使用简单的线性的代码来完成它,并且虚幻引擎会负责管理基于时间的代码执行的细节。
  • 复杂性: 现在您可以多个状态及子类中重载函数(像 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中设置actor的状态,反之可以设置不可编辑状态。如果想定义一个可以编辑的状态,请按照以下方式:

state() MyState
{
   ...
}

如果要声明一个不可编辑的状态,请按照:

state MyState
{
   ...
}

您也可以通过"auto"关键字来指定一个actor的自动的或者初始的状态。这会导致当新的actor初次被激活时都会被置于那个状态。

auto state MyState
{
   ...
}

状态标签和Latent函数


除了函数,一个状态可以包含一个或多个标签,在其后面可以书写UnrealScript代码。For example:

auto state MyState
{
Begin:
   `log( "MyState has just begun!" );
   Sleep( 2.0 );
   `log( "MyState has finished sleeping" );
   goto('Begin');
}

上面的状态代码输出了信息 "MyState has just begun!" ,接着它停止了2秒钟,然后它输出了信息 "MyState has finished sleeping" 。这个例子中的一个有趣的事情便是调用latent函数"Sleep": 这个函数的调用并不是立即返回,而是在一定量的游戏时间过去后才返回。Latent函数仅能在状态代码中并且不能从函数中调用。Latent函数可以帮助您管理包括一段时间的经过在内的一系列复杂的事件。

所有的状态代码都是以标签定义开始;在上面的例子中标签命名为"Begin"。标签提供了一个进入状态代码的方便的入口点。你可以在状态代码中使用任何标签名称,但是"Begin"标签是专用的: 它是那个状态的默认入口点。

所有的actor都有三个主要的latent函数可以使用:

  • Sleep( float Seconds )使状态的执行暂停一段时间,然后再继续执行。
  • FinishAnim()等待直到当前你正在播放的动画序列播放完成,然后继续执行。这个函数使编写由动画驱动的脚本(也就是执行是由网格物体动画控制的脚本)变得容易。比如,大多数AI脚本都是由动画驱动的(相对于有时间驱动的脚本),因为平滑过度的动画是AI系统的主要目标。
  • FinishInterpolation()等待当前的InterpolationPoint移动完成然后继续。

Pawn类为动作定义了几个重要的latent函数比如在世界中导航及短期运动。请查看单独的AI文档来获得它们用途的描述。

3个native UnrealScript函数在书写状态代码时尤其地有用:

  • "Goto('标签名')"函数(和C/C++/Basic中的goto类似),该函数使状态代码在指定的标签处继续执行。
  • 特殊的"Stop"命令,它会导致状态代码停止执行。状态代码在您到达一个新的状态或者到达当前状态中的一个新的标签之前将不会继续执行。
  • "GotoState"函数会使actor跳转到一个新的状态,然后在指定的标签处继续执行(如果您没有指定一个标签,默认则是"Begin"标签)。你可以在状态代码中调用GotoState函数,它将会立刻跳转到目标状态。你也可以在actor中的任何函数中调用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" );
	...
}

当您运行这个程序然后触摸那个actor,你会看到:

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" );
}

当您在一个或多个状态中、及在一个或多个父类中有一个全局执行的函数,你需要理解在给定的环境中应该调用哪个版本的函数。范围规则解决了这些复杂的情况:

  • 如果对象在一个状态中,并且函数的实现存在于那个状态的某处(不管是在actor类中还是在某个父类中),则调用最子类的状态函数版本。
  • 否则,调用最底层派生函数的非状态版本。

高级的状态编程


如果一个状态没有重载父类中具有相同名称的状态,那么你可以随意地使用"extends"关键字来使该状态在当前类的所存在的状态上进行扩展。这是有用的,比如,在您有一组具有很多相同功能的类似状态的情况(比如MeleeAttacking 和 RangeAttacking)。在这种情况下你可以像这样声明一个作为基类的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...
}

您可以从它的"state"变量、类型"name"变量来说明一个actor在哪个特定的状态中。

通过使用 GotoState('') 函数使一个actor处在"no state(没有状态)"中是可以实现的。当一个actor在"no state(没有状态)"时,仅可以调用它的全局(非状态)函数。

无论您什么时候使用 GotoState 命令来设置一个actor的状态,引擎都可以调用两个专用的通知函数,如果您已经定义了它们: EndState()BeginState()EndState 在新的状态开始之前将会在当前的状态中立刻被调用, BeginState 在新的状态开始后将立刻被调用。这些函数为您的状态可能需要进行的任何特定状态的初始化和清除提供了方便。

状态栈

使用正常的状态变换时,当你从一个状态到另一个状态后,便不能再回到先前的状态。但是通过使用状态栈使这个功能可以实现。调用 PushState 函数将会转换到放在栈的顶部的新状态, 当前的状态将会被冻结。当调用 PopState 时,将会恢复先前的状态并继续从 PushState 被调用的点开始继续执行。 当在状态代码内部使用 PushState 时,它是作为latent函数的,所以和您在函数中调用 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。当然,如果 BaseState 在栈上,调用 IsInState('BaseState', true) 将会返回true。