允许一个对象在其内部状态改变时改变自身的行为。对象看起来好像是在修改自身类。。
(摘自《游戏编程模式》)
我们熟悉的名词:有限状态机(finite state machines,fsm)就是状态模式的应用。正如Unity 的动画状态机。
状态机
根据上面给出Unity游戏引擎的Animator状态机可以看出,在状态机中,无论任何时刻,在状态中只有一种激活状态。这就是状态模式最鲜明的特点。如何构造自己的状态机呢?我们需要进行如下步骤:
- 枚举所有的状态,并且在这组状态之间可以相互转换。
- 确定在这种状态中同一时刻只能处于一种状态。
- 状态机可以接受一组输入或者事件,输入或事件可以改变状态机当前状态。
实现
- 枚举:我们需要把状态机中的状态编写为一个枚举类
enum State
{
Evade, //躲避
Stand, //站立
Jump, //跳跃
Dive //俯冲
}
- 我们需要将状态和事件绑定起来
class Character
{
public:
State state;
void HandleInput(Input input)
{
switch(state)
{
case State.Evade:
//处理躲避期间可以处理的事件
if(input.PressUp(Key.Down))
{
this->Play(Stand);
state=State.Stand;
}
break;
case State.Stand:
//处理站立期间可以处理的事件
if(input.PressDown(Key.Down))
{
this->Play(Evade);
state=State.Evade;
}
else if(input.PressDown(Key.B))
{
this->Play(Jump);
state=State.Jump;
}
break;
case State.Jump:
//处理跳跃期间可以处理的事件
if(input.PressDown(Key.Down))
{
this->Play(Dive);
state=State.Dive;
}
break;
case State.Dive:
//处理俯冲期间可以处理的事件
break;
}
}
}
这就是状态机的一个最简单的例子。switch...case的组织方式将每一个状态下可以运行的代码整合在一起,就不容易出现逻辑上的错误。上面当触发某一个状态时,修改state值就意味着状态的修改。
状态模式
上述状态机例子简单易懂且符合人的逻辑,但其思想仍是面向过程编程。如何利用面向对象设计实现状态模式呢?首先要做的就是将状态从一个简单的枚举值变为一个类实例。
class AnimationState
{
public:
virtual ~AnimationState(){}
virtual void Entry(Character& character)
virtual AnimationState* HandleInput(Character& chara,Input input){}
virtual void Update(Character& chara){}
}
下一步就是将switch...case的内容在HandleInput中进行编写,以Stand状态为例:
class CharacterStandState : AnimationState
{
public:
virtual void Entry(Character& character)
{
character.Play(Evade);
}
virtual AnimationState* HandleInput(Character& chara,Input input)
{
if(input.PressDown(Key.Down))
{
return new CharactyerEvadeState();
}
else if(input.PressDown(Key.B))
{
return new CharacterJumpState();
}
}
}
最后一步就是在Character中利用虚函数调用实现不同状态下调用不同的处理函数
class Character
{
public:
virtual void HandleInput(Input input)
{
AnimationState* state=currentState->HandleInput(*this,input);
if(state!=nullptr)
{
delete currentState;
currrentState=state;
}
state->Entry(*this);
}
public:
AnimationState* currentState;
}
除此之外,我们也可以在切换状态这一个环节使用静态对象作为状态机状态切换的赋值实例。这也意味着,若状态机实例含有其他成员变量,那么它只能拥有相同的一份!其他对象即使运用到这个状态机,这些对象对应的状态实例的成员变量都是一致的(实际上是运用了享元模式)。代码将不再给出。
状态机的缺陷和改进
并发状态机
我们在通常情况下不会仅仅让角色只执行一个人物。例如:魂斗罗中角色可以一边射击一遍行走;英雄联盟中可以打出Q闪等衔接操作。我们当然可以通过枚举将所有动作组合的可能性枚举出来,但是工作量将会大大增加,且状态机失去了灵活性。这时候就需要并发状态机,并发状态机的处理方式就是将状态机进行了分层操作。
class Character
{
public:
virtual void HandleInput(Input input)
{
AnimationState* state1=currentState_layer1->HandleInput(*this,input);
AnimationState* state2=currentState_layer2->HandleInput(*this,input);
if(state1!=nullptr)
{
delete currentState;
currrentState=state1;
}
if(state2!=nullptr)
{
delete currentState;
currrentState=state2;
}
state1->Entry(*this);
state2->Entry(*this);
}
public:
AnimationState* currentState_layer1;
AnimationState* currentState_layer2;
}
层次状态机
一个状态有一个父状态。当有一个事件进来的时候,如果子状态不处理它,那么就沿着继承链传递给它的父状态来处理。
class ParentState : AnimationState
{
virtual AnimationState* HandleInput(Character& chara,Input input)
{
if(.....)
else(....)
}
}
class ChildState : ParentState
{
virtual AnimationState* HandleInput(Character& chara,Input input)
{
if(.....)
else(....)
//若子状态不处理,则让其父状态处理
reurn ParentState::HandleInput(chara,input);
}
}
下推自动机:使用状态栈
使用状态栈将使得状态机有了历史记录以方便退回。