天天看點

遊戲中分層狀态機的實作(轉)

    遊戲中最複雜的邏輯部分就是戰鬥部分。之前一直沒有對狀态機進行理論學習,以及于在設計遊戲戰鬥邏輯的時候總是沒有安全感。下邊是一個寫的不錯的文章。

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

其他參考:

繼續閱讀