游戏中分层状态机的实现(转)

来源:互联网 发布:淘宝心选怎么没有了 编辑:程序博客网 时间:2024/06/05 13:07
    游戏中最复杂的逻辑部分就是战斗部分。之前一直没有对状态机进行理论学习,以及于在设计游戏战斗逻辑的时候总是没有安全感。下边是一个写的不错的文章。
转自:http://blog.csdn.net/xtxy/article/details/9668221

状态机的实现方式有很多种,一般都使用比较简单的switch case方式来实现对事件的处理和状态的转移,如下所示:

[html] view plaincopy
  1. void ProcessEvent(Event event)  
  2. {  
  3.     switch(state)  
  4.     {  
  5.         case StateA:  
  6.             StateAProcess(event);  
  7.             break;  
  8.   
  9.         case StateB:  
  10.             StateBProcess(event);  
  11.             break;  
  12.     }  
  13. }  

也有利用数组实现的行列式状态机,这样方便开发人员查看,如下所示:

[csharp] view plaincopy
  1. EventHandler stateHandler[] =  
  2. {  
  3.     StateA_Event1, StateA_Event2,  
  4.     StateB_Event1, StateB_Event2,  
  5. };  
  6.   
  7. void ProcessEvent(Event event)  
  8. {  
  9.     int index = state * EVENT_NUM + event;  
  10.     stateHandler[index]();  
  11. }  

但是这些状态机都有一个共同点,就是状态之间的转移需要在状态内部显示得指明目标状态。


----------------------------------------------------------------------------------------------


在游戏中,遇到一些复杂的情况时,如果使用普通的状态机,那需要写大量的状态,举一个星际争霸中一个机枪兵的例子:


1. 机枪兵在平时站立时,处于 空闲 状态;

2. 机枪兵发现敌人,并且敌人在射程范围内,机枪兵开始攻击敌人;此时,机枪兵进入 攻击 状态;

3. 敌人死亡,机枪兵停止攻击;此时,机枪兵回到 空闲 状态;

4. 此时玩家发出进攻命令,此进攻命令是用A键点了远处的一个地面 place1 ,也就是没有具体目标的进攻;此时,机枪兵进入 移动进攻 状态;

5. 在移动过程中,机枪兵发现了敌人,所以他要脱离原来的路径,走向发现的敌人;此时,机枪兵进入 追击 状态;

6. 机枪兵和敌人的距离小于了自己的射程之后,机枪兵停下来,并且攻击敌人;此时,机枪兵进入了 攻击 状态;

7. 敌人死亡后,机枪兵重新寻路到place1,继续前进;此时机枪兵回到步骤4,回到了 移动进攻 状态。


在上面这个过程中,从步骤2到步骤3,攻击 状态转移到 空闲 状态;从步骤6到步骤7,攻击 状态转移到 移动进攻 状态;源状态都是 攻击 状态,触发事件都是 敌人死亡,但是目标状态却不相同;

也就是说,步骤2的 攻击 状态和步骤6的 攻击 状态严格意义上是不同的两个状态,一般来说有两个解决方案来满足这种情况:

1. 做 攻击A 状态和 攻击B 状态;

2. 在攻击状态内保存一个变量,来实现状态结束后跳转到不同的状态;


其实这两种方法本质上都是一样的,而且都存在同样的缺点:如果在某种情况下如果 攻击 状态收到 敌人死亡 事件之后需要跳转到其他状态,(比如机枪兵在巡逻时发现敌人的情况),那就需要增加状态或者代码分支。


如何才能将上面的 攻击 状态合并为一个,而且可以支持以后的扩展呢?这个就是需要解决的问题。

仔细分析一下,可以发现 攻击 状态之所以需要跳转到不同的目标状态,是因为在其前,机枪兵进入了不同的状态;换句话说,机枪兵退出 攻击 状态的时候,实际上是回到了之前的某个阶段的状态。(步骤2是回到前一个状态,步骤7是回到了前两个状态)


----------------------------------------------------------------------------------------------


堆栈的特性为我们很好地解决了这个问题:压人变量A,栈顶的变量就是变量A;压入变量B,栈顶的变量变为变量B;弹出变量B,栈顶的变量变回到变量A。


所以根据这个特性,可以开发一个融合堆栈的状态机,其基础构造参照了《大型多人在线游戏开发》(《Massively Multiplayer Game Development》)里面的状态机实现,代码(c sharp格式)参考如下:

[csharp] view plaincopy
  1. public enum StateChange  
  2. {  
  3.     None,  
  4.     Switch,  
  5.     Enter,  
  6.     Exit,  
  7. }  
  8.   
  9. public class UnitSMBase  
  10. {  
  11.     public UnitState state;  
  12.     public StateChange change;  
  13.       
  14.     float _deltaTime;  
  15.     protected float _checkTime;  
  16.     protected Unit _unit;  
  17.       
  18.     public UnitSMBase(Unit unit)  
  19.     {  
  20.         _unit = unit;  
  21.         _deltaTime = 0;  
  22.     }  
  23.       
  24.     public virtual void Enter()  
  25.     {  
  26.         _checkTime = 1;  
  27.     }  
  28.       
  29.     public virtual void Exit()  
  30.     {  
  31.     }  
  32.       
  33.     public void ProcessEvent(UnitEvent evt)  
  34.     {  
  35.         state = UnitState.None;  
  36.         change = StateChange.None;  
  37.           
  38.         if(evt == UnitEvent.Update)  
  39.         {  
  40.             if(_deltaTime >= _checkTime)  
  41.             {  
  42.                 _deltaTime = 0;  
  43.                 evt = UnitEvent.UpdateFixTime;  
  44.             }  
  45.             else  
  46.             {  
  47.                 _deltaTime += Time.deltaTime;  
  48.             }  
  49.         }  
  50.           
  51.         DoProcess(evt);  
  52.     }  
  53.       
  54.     protected virtual void DoProcess(UnitEvent evt)  
  55.     {  
  56.     }  
  57.       
  58.     public virtual bool CanGo(UnitState unitState)  
  59.     {  
  60.         return true;  
  61.     }  
  62. }  
  63.   
  64.   
  65. public class UnitSmMgr  
  66. {  
  67.     List<UnitSMBase> _smList;  
  68.     Dictionary<UnitState, UnitSMBase> _smStateDict;  
  69.   
  70.     public UnitSmMgr(UnitSMBase initSM, UnitState initState)  
  71.     {  
  72.         _smList = new List<UnitSMBase>();  
  73.         _smList.Add(initSM);  
  74.           
  75.         _smStateDict = new Dictionary<UnitState, UnitSMBase>();  
  76.         _smStateDict[initState] = initSM;  
  77.   
  78.                 initSM.Enter();  
  79.     }  
  80.   
  81.     public void RegisterSM(UnitSMBase sm, UnitState state)  
  82.     {  
  83.         _smStateDict[state] = sm;  
  84.     }  
  85.   
  86.     public void ProcessEvent(UnitEvent evt)  
  87.     {  
  88.         _smList[0].ProcessEvent(evt);  
  89.           
  90.         switch(_smList[0].change)  
  91.         {  
  92.         case StateChange.Enter:  
  93.             _smList.Insert(0, _smStateDict[_smList[0].state]);  
  94.             _smList[0].Enter();  
  95.             break;  
  96.               
  97.         case StateChange.Switch:  
  98.              _smList[0].Exit();  
  99.             _smList[0] = _smStateDict[_smList[0].state];  
  100.             _smList[0].Enter();  
  101.             break;  
  102.               
  103.         case StateChange.Exit:  
  104.             _smList[0].Exit();  
  105.             _smList.RemoveAt(0);  
  106.             break;  
  107.         }  
  108.           
  109.         if(0 == _smList.Count)  
  110.         {  
  111.             Debug.LogError("state machine is empty");  
  112.         }  
  113.     }  
  114. }  

主要思路如下:


1. 每个状态都是一个类,他们继承于一个公共类,其包含进入,退出,处理事件的虚方法;

2. 状态机有一个状态堆栈,这里使用List来实现;

3. 状态机初始化时有一个初始状态,一般为idle状态,其成为堆栈的第一个元素;

4. 状态转移分为3种情况:a 进入目标状态,b 退出当前状态,c 切换到目标状态(即先退出当前状态,再进入目标状态);

5. 当前有效的状态就是状态堆栈里面栈顶的那个状态,即:_smList[0];


按照这个状态机模型来实现前面讲过的机枪兵的例子,其中状态机图中左边卫栈顶,右边为栈底:

1. 机枪兵在平时站立时,处于 空闲 状态;

初始化状态机,并将 空闲 状态作为初始状态放入状态机堆栈中;状态机堆栈:【空闲】


2. 机枪兵发现敌人,并且敌人在射程范围内,机枪兵开始攻击敌人;此时,机枪兵进入 攻击 状态;

进入 攻击 状态;状态机堆栈:【攻击】【空闲】


3. 敌人死亡,机枪兵停止攻击;此时,机枪兵回到 空闲 状态;

退出当前状态;状态机堆栈:【空闲】


4. 此时玩家发出进攻命令,此进攻命令是用A键点了远处的一个地面 place1 ,也就是没有具体目标的进攻;此时,机枪兵进入 移动进攻 状态;

进入 移动进攻 状态;状态机堆栈:【移动进攻】【空闲】


5. 在移动过程中,机枪兵发现了敌人,所以他要脱离原来的路径,走向发现的敌人;此时,机枪兵进入 追击 状态;

进入 追击 状态;状态机堆栈:【追击】【移动进攻】【空闲】


6. 机枪兵和敌人的距离小于了自己的射程之后,机枪兵停下来,并且攻击敌人;此时,机枪兵进入了 攻击 状态;

切换到 攻击 状态;状态机堆栈:【攻击】【移动进攻】【空闲】


7. 敌人死亡后,机枪兵重新寻路到place1,继续前进;此时机枪兵回到步骤4,回到了 移动进攻 状态。

退出当前状态;状态机堆栈:【移动攻击】【空闲】


这样的话,不需要记录之前状态的信息,就能完成状态之间的正确转移;开发逻辑时,只需要注意状态发生变化时应该使用3种方式里面的哪1种来做状态转移。

其他参考:
http://blog.csdn.net/turkeyzhou/article/details/7695813
http://zgx10.blog.163.com/blog/static/6490547201102012444301/
原创粉丝点击