问题背景

假设有一个玩家类Player,功能是根据不同输入切换不同的状态。例如按下J攻击状态,K跳跃状态,A/D移动状态等。

解决方案或许很简单,用多个if else嵌套,然后实现对应逻辑即可。

  1. if(按下J){
  2. 切换攻击状态
  3. }else if(按下K){
  4. 跳跃状态
  5. }....

然而有个显而易见的bug,如果在攻击下按J,会同时跳跃。总结来说,它犯了两个错误:复杂分支和可变状态。

有限状态机

尝试着画出上方逻辑的流程图。

游戏开发中的状态模式 - 图1

由图可知,流程图覆盖的核心部分:状态,输入,和转移

展开来说,有限状态机的要点是:

  • 拥有状态机所有可能状态的集合。
  • 状态机同时只能存在一个状态。
  • 一连串的输入或事件被发送给状态机。
  • 每个状态都有一系列的转移,每个转移与输入和另一状态有关。

接下来,试着实现这个有限状态机。

为了方便定义状态,建立枚举表示状态种类。

  1. enum State{
  2. STATE_STANDING,
  3. STATE_JUMPING,
  4. ....
  5. }

当对状态进行处理时,只需要一个switch语句

  1. void Handle(输入){
  2. switch(当前状态){
  3. case STATE_STANDING:
  4. if(输入J){
  5. 做出攻击状态行为
  6. }
  7. break;
  8. case STATE_JUMPING:
  9. if(输入K){
  10. 输入跳跃状态行为
  11. }
  12. break;
  13. ...
  14. }
  15. }

然而,如果我们想新加一个动作:蹲下状态下可以进行蓄力。

想要在有限状态机上进行功能增加,至少需要一个添加一个chargeTime的字段,还需要一个计时器,实现很困难。

状态模式

在同一个类中编写不同状态的不同功能是十分混乱的,所以可以把不同的状态定义成不同的类(State)。这就是基本的状态模式。

这样,就能在下蹲模式里单独添加chargeTime字段和Timer方法。

状态模式的定义:“当一个对象内在状态改变时允许改变其行为,这个对象看起来像是改变了类”。

游戏开发中的状态模式 - 图2

  • Context——状态拥有者
    • 让外界得知状态的改变或者通过外界改变内部状态。
    • 有状态属性的类。

如上述问题中的Player类。

  • State——状态接口类
    规范具体的ConcreteState在特定下表现的行为。
    如在上述问题中,有一个计时器接口,用来实现如何时能结束当前行为等父功能和蓄力等子功能。
  • ConcreteState——具体状态类
    • 继承自State
    • 实现Context在相应状态下该有的行为。

如上述问题中的站立、下蹲、攻击等状态。

状态的切换,一般来说有两种方法:

  • 交由Context本身,按条件在各状态间切换
  • 产生Context类对象时(构造函数),马上指定初始状态给Context,而在后续执行过程中状态转换则由State对象负责,Context对象不再介入。

根据有限状态机的定义,推荐使用第二种。

回到上面的流程图,状态对象本身比较清楚”在什么状态下,可以让Context对象转换到另一个State状态“。所以在每个ConcreteState类的程序代码中,可以看到”状态转换条件“的判断。以及设置哪一个ConcreteState对象成为新的状态。

因此判断条件及状态属性都被转换到ConcreteState类中。故而,可以缩减Context类的大小

并发状态机

假定我们需要给上述问题Player类新加一个Gun类。当她拿着枪的时候,她还是能做她之前的任何事情:跑动,跳跃,跳斩,等等。但是她在做这些的同时也要能开火。

如果Player有n个状态,Gun有m个状态,尝试用有限状态机解决,一共有**nm个状态。

但如果把Player和Gun看成两个单独的状态机,那么就只有n+m个状态。

换句话说,同时拥有两个Context

分层状态机

当每个状态都有重复功能代码的时候,可以使用分层的方法。

对于面向对象,可以使用继承的方法。但是这也同时增加了程序的耦合

下推状态机

多个基础状态绑定重复多次时,利用状态栈,把多个独立的状态捆绑到一起。

游戏开发中的状态模式 - 图3

状态模式的应用

  • 场景的切换
    每个场景(ConcreteState)抽象为一个状态xxxState,场景基类(State)定义场景开始、结束、更新等接口ISceneState,场景控制类(Context)保持当前游戏状态,并作为其他功能模块类的互动接口,也是执行场景转换的地方SceneStateController。
  • 角色AI
    使用状态模式来控制角色在不同在状态下的AI行为。
  • 游戏服务器连线状态
  • 关卡进行状态

板子

首先画出有限状态机

游戏开发中的状态模式 - 图4

Context.cs

  1. public class Context
  2. {
  3. State m_state = null;
  4. public void Request(int value)
  5. {
  6. m_state.Handle(value);
  7. }
  8. public void SetState(State theState)
  9. {
  10. m_state = theState;
  11. Console.WriteLine("Context.SetState: " + theState);
  12. }
  13. }

State.cs

  1. public abstract class State
  2. {
  3. protected Context m_context;
  4. public State(Context theContext)
  5. {
  6. m_context = theContext;
  7. }
  8. virtual public void Handle(int value)
  9. {
  10. Console.WriteLine("Value: " + value);
  11. }
  12. }

ConcreteStateA

  1. public class ConcreteStateA:State
  2. {
  3. public ConcreteStateA(Context theContext):base(theContext){ }
  4. public override void Handle(int value)
  5. {
  6. base.Handle(value);
  7. Console.WriteLine("ConcreteStateA");
  8. if (value >= 10)
  9. m_context.SetState(new ConcreteStateB(m_context));
  10. }
  11. }

ConcreteStateB

  1. public class ConcreteStateB:State
  2. {
  3. public ConcreteStateB(Context theContext):base(theContext){ }
  4. public override void Handle(int value)
  5. {
  6. base.Handle(value);
  7. Console.WriteLine("ConcreteStateB");
  8. if (value < 10)
  9. m_context.SetState(new ConcreteStateA(m_context));
  10. else if (value >= 20)
  11. m_context.SetState(new ConcreteStateC(m_context));
  12. }
  13. }

ConcreteStateC

  1. public class ConcreteStateC:State
  2. {
  3. public ConcreteStateC(Context theContext):base(theContext){ }
  4. public override void Handle(int value)
  5. {
  6. base.Handle(value);
  7. Console.WriteLine("ConcreteStateC");
  8. if(value < 20)
  9. m_context.SetState(new ConcreteStateA(m_context));
  10. }
  11. }

客户端代码就不写了。