遊戲中分層狀態機的實現

狀態機的實現方式有很多種,一般都使用比較簡單的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種來做狀態轉移。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章