狀態機的實現方式有很多種,一般都使用比較簡單的switch case方式來實現對事件的處理和狀態的轉移,如下所示:
void ProcessEvent(Event event)
{
switch(state)
{
case StateA:
StateAProcess(event);
break;
case StateB:
StateBProcess(event);
break;
}
}
也有利用數組實現的行列式狀態機,這樣方便開發人員查看,如下所示:
EventHandler stateHandler[] =
{
StateA_Event1, StateA_Event2,
StateB_Event1, StateB_Event2,
};
void ProcessEvent(Event event)
{
int index = state * EVENT_NUM + event;
stateHandler[index]();
}
但是這些狀態機都有一個共同點,就是狀態之間的轉移需要在狀態內部顯示得指明目標狀態。
----------------------------------------------------------------------------------------------
在遊戲中,遇到一些複雜的情況時,如果使用普通的狀態機,那需要寫大量的狀態,舉一個星際爭霸中一個機槍兵的例子:
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格式)參考如下:
public enum StateChange
{
None,
Switch,
Enter,
Exit,
}
public class UnitSMBase
{
public UnitState state;
public StateChange change;
float _deltaTime;
protected float _checkTime;
protected Unit _unit;
public UnitSMBase(Unit unit)
{
_unit = unit;
_deltaTime = 0;
}
public virtual void Enter()
{
_checkTime = 1;
}
public virtual void Exit()
{
}
public void ProcessEvent(UnitEvent evt)
{
state = UnitState.None;
change = StateChange.None;
if(evt == UnitEvent.Update)
{
if(_deltaTime >= _checkTime)
{
_deltaTime = 0;
evt = UnitEvent.UpdateFixTime;
}
else
{
_deltaTime += Time.deltaTime;
}
}
DoProcess(evt);
}
protected virtual void DoProcess(UnitEvent evt)
{
}
public virtual bool CanGo(UnitState unitState)
{
return true;
}
}
public class UnitSmMgr
{
List<UnitSMBase> _smList;
Dictionary<UnitState, UnitSMBase> _smStateDict;
public UnitSmMgr(UnitSMBase initSM, UnitState initState)
{
_smList = new List<UnitSMBase>();
_smList.Add(initSM);
_smStateDict = new Dictionary<UnitState, UnitSMBase>();
_smStateDict[initState] = initSM;
initSM.Enter();
}
public void RegisterSM(UnitSMBase sm, UnitState state)
{
_smStateDict[state] = sm;
}
public void ProcessEvent(UnitEvent evt)
{
_smList[0].ProcessEvent(evt);
switch(_smList[0].change)
{
case StateChange.Enter:
_smList.Insert(0, _smStateDict[_smList[0].state]);
_smList[0].Enter();
break;
case StateChange.Switch:
_smList[0].Exit();
_smList[0] = _smStateDict[_smList[0].state];
_smList[0].Enter();
break;
case StateChange.Exit:
_smList[0].Exit();
_smList.RemoveAt(0);
break;
}
if(0 == _smList.Count)
{
Debug.LogError("state machine is empty");
}
}
}
主要思路如下:
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種來做狀態轉移。