2.7 使用并行状态机来创建可信的角色
Thor Alexander, Hard Coded Games
thor@hardcodedgames.com
本文的示例代码包含在所附的光盘中。
|
当 |
游戏开发人员可以获得更多的资源来创建更有深度、更引人入胜的游戏模式时,他们就会开始试图让游戏角色具有更多强大且富于变化的能力。由玩家和AI控制的角色可以执行更多种类的行动,这些行动的对象可以是其他角色以及游戏世界中的物品。随着可供执行的行动越来越多,游戏设计人员所创建的角色就能以更可信、更动人的方式来执行行动。
本文讨论了怎样使用并行(parallel)状态机来控制这些可信的角色。状态机(State Machine)是进行游戏开发的常见工具。
状态机提供了下面这些属性,这让它成为实现游戏设计目标的理想工具。
状态为针对行为改变进行建模提供了良好的机制。
状态以及它们之间的转换可以用易读的状态图来说明。
状态可以在客户端和服务端之间复制以使用于在线游戏中。
状态可以在多个游戏子系统间重用,如图2-25所示。

图2-25 角色状态可以用在很多地方
本文所包含的某些图表是使用UML标记来表示的,读者可参考UML[Booch98]以获得对UML的深入讨论。
2.7.1 状态模式(State Pattern)
如果分离(separation)可以从多态(polymorphism)这一面向对象特性中获益,那么把行为分离到不同的对象中是一个有用的方法。多态使得我们可以用相同的方式使用两个对象,调用同样的方法(method),即使这些对象实现这些方法的方式完全不同。多态使得超类(super class)可以定义一个通用的接口,由子类在必要的时候实现具体的方法。
状态模式是一个非常有用的软件设计模式[Gamma94],它利用多态来为同一个对象的不同状态定义不同的行为。图2-26所示是一个状态模式的UML类图。这里,行动者(也就是Actor对象)有一个名为当前状态(currentState)的对象成员,它的类型是具体状态(ConcreteState)。这个状态方法的接口由它的超类抽象状态(AbstractState)定义。这个子类实现了超类指定的Enter(进入状态)和Exit(退出状态)方法。这些方法在状态机实现中很常见,如果在运行时进入或退出一个状态,它们将被分别调用。

图2-26 状态模式的UML类图
图2-27是对这个例子进行了扩展并且描述了加入多态后的状况示意图。这里使用了一个简单的例子,它具有3种具体状态,分别对应于行动者能够进入的3种可行的战斗状态。这些战斗状态中的每一都继承自抽象类状态(State)。这个角色具有一个SetCurrentState(设置当前状态)方法,这个方法可以让当前状态属性迁移到一个新的战斗状态中。
下面这段代码是一个用[Python]实现的例子。
class Actor:
def __init__(self):
self.fightingStatePunch = State.Punch()
self.fightingStateKick = State.Kick()
self.fightingStateBlock = State.Block()
self.currentState = self.fightingStateBlock
def SetCurrentState(self,targetState):
self.currentState.Exit(self)
self.currentState = targetState
self.currentState.Enter(self)
def Punch(self):
self.SetCurrentState(self.fightingStatePunch)
def Kick(self):
self.SetCurrentState(self.fightingStateKick)
def Block(self):
self.SetCurrentState(self.fightingStateBlock)
…

图2-27 使用多态的状态模式
注意,上面的例子是怎样把这个行动者对象传入SetCurrentState方法中,并且调用Enter和Exit方法的。
2.7.2 并行(Parallel)状态层
随着游戏设计人员为角色加入越来越多的功能,它们的状态图会变得日益复杂以至于难以处理。为了控制复杂度,这些功能应该被分解到独立的概念层次中去。如果某个功能与某一层中其他功能只能以互斥的方式执行,就把这个功能放入这一层中。而可以独立或是并发执行的功能则被放入不同的层次。图2-28展示了MMP游戏角色常见的行为,它们被分为3个层次。

图2-28 把互斥行为分解到并行的层次中去
1.移动层
让游戏中角色可以在游戏世界中四处移动是最基本的行为。在大多数现代游戏的实现中,移动(movement)独立于正在播放的实际动画(譬如说,步行、跑步、游泳)。移动仅对表示角色碰撞模型的二维或三维对象在游戏世界中的平移(translation)进行处理。这是游戏的移动层所负责的范围。为了清楚起见,本文仅对平移进行讨论,并且忽略任何可以控制角色旋转(譬如说,左转或右转)的额外层次。设计并实现这样一个转向(steer)层将作为留给读者的练习。
表2-1中的状态对应于角色需要执行的移动行为。通过对这些状态两两之间的合法转换进行定义,可以把它们组合为一个移动状态机。这样就可以从一个状态转换到另一个状态,也可以在同一个状态中循环,如图2-29所示。自身循环的状态表示一个可以被重复的行为,譬如说坠落或是步行。
表2-1 移动状态
|
移动状态 |
描述 |
|
Stop(停止,缺省状态) |
休息,不移动 |
|
MoveBackward(前移) |
向后移动 |
|
MoveForward(后移) |
向前移动 |
|
MoveLeft(左移) |
保持面向前方,向左移动 |
|
MoveRight(右移) |
保持面向前方,向右移动 |

图2-29 移动层的状态图
本文已经定义了移动状态和它们之间的转换,接下来可以把它们转化为一个类图,如图2-30所示。每种状态都从公共超类移动状态(MovementState)中继承,如图2-29中所示的状态模式那样。这些状态子类将从抽象超类移动状态中继承它们共享的方法接口。

图2-30 移动层的类图
下面的Python代码给出了移动状态层的部分实现。
class MovementState():
def __init__(self):
self.id = 0
self.transitionList = []
def CanTransition(targetState):
if targetState.GetId() in self.transitionList:
return 1 ### Valid transition.
else:
return 0 ### Invalid transition.
def GetId(self):
return self.id
def Enter(self,actor):
return
def Exit(self,actor):
return
class MoveForward(MovementState):
def __init__(self):
self.id = 1
self.transitionList = [
MovementState.MoveForward.GetId(),
MovementState.Stop.GetId() ]
def Enter(self,actor):
### perform move-forward tasks on the actor here.
…
return
def Exit(self,actor):
### perform state-exit cleanup tasks here.
…
return
…
注意每个状态对象都维护了各自的惟一标识(id)和转换列表(transitionList)属性。每种状态可以进行的合法转换保存在它的转换列表中。CanTransition(能否进行转换)方法会使用这个列表来检查是否可以从当前状态合法地转换到目标状态(targetState)。
2.姿势层
这个层次表示了角色可以呈现的各种姿势(posture)。移动层处理了与角色在游戏世界中实际移动相关的问题,而姿势层则处理了角色在移动时的外观。这一层控制的是类似于从坐下到站立或从站立到步行这样的转换。它还可以帮助我们管理更高级的行进方式,譬如说骑马和游泳。图2-31和表2-2列出了一些基本的姿势状态以及它们之间的合法转换。

图2-31 姿势层的状态图
表2-2 姿势状态
|
姿势状态 |
描述 |
|
Standing(站立,缺省状态) |
空闲的站立状态 |
|
Sitting(坐下) |
在地上坐着 |
|
Walking(步行) |
在地上步行 |
|
Running(跑步) |
在地上跑步 |
|
Falling(掉下) |
正在空中往下掉 |
|
Swimming(游泳) |
在水中游泳 |
|
Mounted(骑马) |
骑马 |
|
Sprawled(平躺) |
躺在地上 |
姿势层有两个显而易见的用途:a)通知动画子系统什么时候应该改变这个角色正在播放的动画;b)接受或拒绝用户界面提出的状态改变请求。它还有一个非常有用但是不太明显的用途:他可以把这些状态传给看得到玩家的敌人的AI,这样这个敌人就可以根据玩家姿势的改变作出反应。譬如说,AI可能会忽视一个处于中立姿势的玩家,但是如果玩家拔出武器进入战斗姿势,AI可能就会对其进行攻击。
图2-32所示的姿势层的类图也遵循状态模式,它从抽象超类姿势状态(PostureState)中派生出所有的具体姿势状态。姿势层的实现和前面所示的移动层代码非常接近,因此这里不再重复。

图2-32 姿势层的类图
3.行动层
游戏系统的最后一层处理角色可以进行的各种行动。这里,本文把行动(action)定义为那些不会随着姿势和移动的变化而变化的行为。譬如说,游戏中角色应该能够挥动他的剑,无论他是在跑步、站立不动还是在骑马。游戏设计人员可以把行动层理解为对上身(upper-body)行为进行处理,而姿势层是对下身(lower-body)行为进行处理。不过也会有例外,某些情况下需要把上身的状态也归为姿势层并且阻止行动层,譬如说在游泳或从高处掉下时。本文会在随后讨论的跨层阻止(cross-layer blocking)中处理这种例外。图2-33和表2-3的行动层和行动状态包含了MMP游戏中常见的行为。这些部分组合在一起形成了行动层的类图,如图2-34所示。

图2-33 典型MMP游戏中的行动层
表2-3 行动状态
|
行动状态 |
描述 |
|
Idle(空闲,缺省状态) |
空闲状态 |
|
Crafting(制造) |
建造或是修复物品 |
|
Casting(施放魔法) |
使用一个魔法符咒 |
|
Fighting(战斗) |
攻击一个角色或对象 |
|
Gesturing(做手势) |
挥手,跳舞,等等 |
|
Spawning(出生) |
在游戏世界中诞生 |

图2-34 行动层的类图
2.7.3 状态管理器
角色所执行的行为已经分离到了并行的层次中,这时需要另一个类来对它们进行管理。这里要介绍的是状态管理器(StateManager)类。行动者类的每个实例都具有对这个管理器的引用功能。状态管理器维护了每个状态层当前状态的引用:当前行动状态(currentActionState)、当前移动状态(currentMovementState)以及当前姿势状态(currentPostureState),如图2-35所示。

图2-35 我们状态层的状态管理器的类图
下面这段Python代码是一个状态管理器的实现。
class StateManager:
def __init__(self):
self.currentActionState = ActionState.Idle()
self.currentMovmentState = MovementState.Stop()
self.currentPostureState = PostureState.Stand()
…
def SetActionState(self,actor,targetState):
if self.currentActionState.CanTransition(targetState):
self.currentActionState.Exit(actor)
self.currentActionState = targetState
self.currentActionState.Enter(actor)
return 1 ### Successful transition.
else:
return 0 ### Cannot transition.
def SetMovementState(self,actor,targetState):
if self.currentMovementState.CanTransition(targetState):
self.currentMovementState.Exit(actor)
self.currentMovementState = targetState
self.currentMovementState.Enter(actor)
return 1 ### Successful transition.
else:
return 0 ### Cannot transition.
…
2.7.4 跨层阻止(Cross-Layer Blocking)
在大多数情况下,每层的状态可以独立于其他层次执行,但是总会存在一些例外。譬如说,如果游戏的角色从很高的电梯上掉下来,处于空中时,游戏设计人员想要让他不能进行移动或是做出任何行动。为实现这点就需要一个能够进行跨层阻止的机制。
如果在移动状态超类中增加一个布尔变量blocked(是否被阻止),就能通过Block(阻止)和Unblock(取消阻止)方法设置或清除它,代码如下。
class MovementState():
def __init__(self):
self.id = 0
self.transitionList = []
self.blocked = 0 ### false
def Block(self):
self.blocked = 1
def Unblock(self):
self.blocked = 0
接着可以在CanTransition方法中加入一个新的检查,并在状态被设为阻止时返回一个表示转换失败的代码。
def CanTransition(targetState):
if self.blocked == 1:
return 0 ### Transitions are blocked
if targetState.GetId() in self.transitionList:
return 1 ### Valid transition.
else:
return 0 ### Invalid transition.
现在就可以阻止移动状态的转换了。
class Falling(PostureState):
…
def Enter(self,actor):
actor.currentStates.SetMovementState(actor,StopState)
actor.currentStates.SetActionState(actor,IdleState)
actor.currentStates.currentMovementState.Block()
actor.currentStates.currentActionState.Block()
…
return
def Exit(self, actor):
actor.currentStates.currentMovementState.Unblock()
actor.currentStates.currentActionState.Unblock()
…
return
这里,姿势状态Falling的Enter方法会强制对象进入移动状态Stop以及行动状态Idle,然后阻止这些状态进行转换。一旦角色退出Falling状态,它会取消对这些状态的阻止。
2.7.5 总结
本文介绍了一个并行状态机制的设计和实现方法,它基于久经考验的状态模式。要在下一代游戏中加入更为可信的角色需要创建更高级的状态机制,而本文中并行状态机制的设计和实现则是一个良好的开端。开发人员还可以把状态模式中的每个状态都定义为一个单件(singleton),这样一来,系统中每个给定状态在内存中就只有一个实例。这么做可以把对内存的使用保持在一个较低的水平,本文推荐使用这个方法来对上面所介绍的方案进行优化。
2.7.6 参考文献
[Booch98] Booch, Grady, The Unified Modeling Language User Guide, Addison-Wesley, Inc., 1998.
[Gamma94] Gamma, et al., Design Patterns, Addison-Wesley Longman, Inc., 1994.
[Python] Python Language Web site, http://www.python.org.






