遊戲中最複雜的邏輯部分就是戰鬥部分。之前一直沒有對狀态機進行理論學習,以及于在設計遊戲戰鬥邏輯的時候總是沒有安全感。下邊是一個寫的不錯的文章。
狀态機的實作方式有很多種,一般都使用比較簡單的switch case方式來實作對事件的處理和狀态的轉移,如下所示:
[html]
view plain
copy
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 plain
copy
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 plain
copy
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. new List<UnitSMBase>();
73. _smList.Add(initSM);
74.
75. 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. "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種來做狀态轉移。
其他參考: