Unity框架設(shè)計:基于狀態(tài)機(jī)的架構(gòu)與設(shè)計
前言
我們做游戲的時候經(jīng)常會有流程控制,流程控制的方法有很多,行為決策樹,狀態(tài)機(jī)等。本質(zhì)差別都不大,就是把每一段執(zhí)行邏輯做成一個一個的節(jié)點,根據(jù)條件執(zhí)行某個節(jié)點,切換到某個節(jié)點。今天給大家分享一下基于狀態(tài)機(jī)來做游戲流程的控制。
1 一個簡單的狀態(tài)機(jī)案例
?
我們先來拆解一個使用案例,通過這個案例讓大家對狀態(tài)機(jī)的流程控制有一個基本的了解。首先我們來構(gòu)建一些狀態(tài)節(jié)點,放入到狀態(tài)機(jī)中。編寫偽代碼如下:
創(chuàng)建一個狀態(tài)機(jī):?
初始化邏輯節(jié)點NodeInit,用來做初始化的邏輯控制, NodeLogin,用來做登錄場景的邏輯控制, NodeTown節(jié)點用來做游戲戰(zhàn)斗場景的邏輯控制。

每個狀態(tài)機(jī)節(jié)點,都有幾個統(tǒng)一的固定的入口,這些入口如何設(shè)計與行業(yè)相關(guān),比如我們的游戲行業(yè),設(shè)計狀態(tài)機(jī)節(jié)點接口一般如下:
public interface IFsmNode { /// <summary> /// 節(jié)點名稱 /// </summary> string Name { get; } void OnEnter(); void OnUpdate(); void OnFixedUpdate(); void OnExit(); void OnHandleMessage(object msg); }
Name: 狀態(tài)機(jī)節(jié)點的名字;
OnEnter: 狀態(tài)機(jī)進(jìn)入到這個狀態(tài)節(jié)點時執(zhí)行,一般用于初始化;
OnExit: 狀態(tài)機(jī)來開這個狀態(tài)節(jié)點時執(zhí)行,一般用戶結(jié)束時候的一些銷毀資源與釋放等;
OnUpdate: 每一幀都會調(diào)用狀態(tài)機(jī)節(jié)點的update, 很多每幀處理的事務(wù)可以放OnUpdate;
OnFixedUpdate: 每個FixedUpdate 都會調(diào)用狀態(tài)機(jī)的OnFixedUpdate函數(shù),一些固定迭代次數(shù)的更新可以放此接口。
OnHandleMessage(object msg): 給狀態(tài)機(jī)節(jié)點觸發(fā)事件消息的時候調(diào)用這個接口,來作為狀態(tài)機(jī)節(jié)點處理事件消息的控制入口。
?
每個狀態(tài)機(jī)節(jié)點,都實現(xiàn)IFsmNode所對應(yīng)的接口,放入到狀態(tài)機(jī)中統(tǒng)一管理。案例中我們在游戲開始時先執(zhí)行NodeInit狀態(tài)節(jié)點,完成游戲的初始化。
public void StartGame()
{
_fsm.Run(nameof(NodeInit));
}
先來看NodeInit節(jié)點處理的邏輯,NodeInit只在OnEnter里面實現(xiàn)了初始化的相關(guān)邏輯,其它接口,沒有任何邏輯處理。代碼如下
void IFsmNode.OnEnter() { AudioPlayerSetting.InitAudioSetting(); // 使用協(xié)程初始化 this.StartCoroutine(Init()); } private IEnumerator Init() { // 加載UIRoot var uiRoot = WindowManager.Instance.CreateUIRoot<CanvasRoot>("UIPanel/UIRoot"); yield return uiRoot; // 加載常駐面板 yield return GameObjectPoolManager.Instance.CreatePool("UIPanel/UILoading", true); // 進(jìn)入到登錄流程 FsmManager.Instance.Change(nameof(NodeLogin)); }
如上面的代碼所示, 當(dāng)狀態(tài)機(jī)執(zhí)行NodeInit節(jié)點狀態(tài)的時候,會初始化時調(diào)用OnEnter接口, NodeInit的OnEnter接口中,調(diào)用了Init函數(shù)來做初始化,首先會創(chuàng)建一個UIRoot, 然后把資源加載界面顯示出來,完成資源加載后,進(jìn)入到登錄邏輯節(jié)點場景,注意這里,狀態(tài)機(jī)就由原來的NodeInit切換到NodeLogin狀態(tài)機(jī)節(jié)點。當(dāng)進(jìn)入NodeLogin節(jié)點的時候,就會執(zhí)行它的OnEnter接口,接下來我們看下登錄節(jié)點的邏輯處理如下:
顯示一個登錄的UI界面,同時切換場景到登錄場景,這樣我們的狀態(tài)機(jī)控制邏輯就切換到登錄場景了,如圖所示:

接下來輸入用戶名+密碼,點擊”Run Game”按鈕,看下RunGame按鈕的處理:
給狀態(tài)機(jī)的節(jié)點發(fā)送一個登錄事件消息, 這樣就可以調(diào)用到狀態(tài)機(jī)節(jié)點的事件處理函數(shù),
private void OnHandleEvent(IEventMessage msg)
{
if(msg is LoginEvent.ConnectServer)
{
FsmManager.Instance.Change(nameof(NodeTown));
}
}
在事件處理函數(shù)中,調(diào)用狀態(tài)機(jī)切換到NodeTown狀態(tài)機(jī)節(jié)點運(yùn)行。最后我們來看下NodeTown游戲戰(zhàn)斗場景中的節(jié)點處理,初始化OnEnter接口如下:
void IFsmNode.OnEnter()
{
string sceneName = "Scene/Town";
SceneManager.Instance.ChangeMainScene(sceneName, OnSceneLoad);
UITools.OpenWindow<UILoading>(sceneName);
UITools.OpenWindow<UIMain>();
AudioManager.Instance.PlayMusic("Audio/Music/town", true);
}
?
切換到游戲戰(zhàn)斗場景,顯示戰(zhàn)斗的主UI, 播放游戲的背景音樂。在看下其它接口,OnUpdate迭代游戲世界變化,OnExit, 刪除掉游戲世界釋放掉資源,代碼如下:
void IFsmNode.OnExit()
{
_gameWorld.Destroy();
UITools.CloseWindow<UIMain>();
}
如圖所示:

通過這個案例的分析,我們確定了游戲狀態(tài)機(jī)的設(shè)計,總結(jié)如下:
Step1: 設(shè)計一些游戲狀態(tài)節(jié)點,節(jié)點中實現(xiàn)具體的一些邏輯處理接口;
Step2: 將游戲狀態(tài)節(jié)點加入到游戲狀態(tài)機(jī)中;
Step3: 給狀態(tài)機(jī)編寫好”切換節(jié)點”的接口,進(jìn)入節(jié)點之前,先調(diào)用上一個節(jié)點的離開OnExit接口,然后調(diào)用新節(jié)點的OnEnter接口, 根據(jù)游戲的需求,每次Update, FixedUpdate, 迭代狀態(tài)機(jī)節(jié)點的OnUpdate與OnFixedUpdate接口。
?
2基于狀態(tài)機(jī)控制的具體實現(xiàn)與設(shè)計
?
有了上面的分析,我們對狀態(tài)機(jī)就了解的很清楚了,自然設(shè)計一個狀態(tài)機(jī)用來控制游戲的跳轉(zhuǎn)控制邏輯就是非常簡單的事情了,我們把游戲中的基于狀態(tài)機(jī)的控制分成“與項目無關(guān)”與“與游戲項目相關(guān)”的兩個部分來設(shè)計與處理。先來看下”與項目無關(guān)”的狀態(tài)機(jī)部分設(shè)計: 兩個代碼: IFsmNode.cs與FiniteStateMachine.cs, IFsmNode.cs代碼負(fù)責(zé)定義狀態(tài)機(jī)節(jié)點的接口,上文中的代碼已經(jīng)給出了游戲開發(fā)中狀態(tài)機(jī)節(jié)點常用接口。開發(fā)者在實現(xiàn)具體業(yè)務(wù)邏輯的時候,只要繼承這個接口并實現(xiàn)即可。
FiniteStateMachine.cs, 主要實現(xiàn)了對狀態(tài)機(jī)節(jié)點的管理,主要數(shù)據(jù)成員與接口如下:
privatereadonly List<IFsmNode> _nodes = new List<IFsmNode>(); 定義一個數(shù)據(jù)成員保存所有的狀態(tài)機(jī)節(jié)點。
private IFsmNode _curNode;
private IFsmNode _preNode;
定義兩個數(shù)據(jù)成員 curNode與prevNode來保存當(dāng)前正在運(yùn)行的狀態(tài)節(jié)點與上一個狀態(tài)節(jié)點;
publicvoid AddNode(IFsmNode node) 定義一個接口,將新的狀態(tài)節(jié)點加入到狀態(tài)機(jī)中;
publicvoid Run(string entryNode) 定義一個接口,作為執(zhí)行第一個狀態(tài)節(jié)點的接口;
publicvoid Transition(string nodeName)定義一個接口,作為執(zhí)行由當(dāng)前狀態(tài)切換到新的狀態(tài)機(jī)節(jié)點的接口;
基于Update,來調(diào)用當(dāng)前執(zhí)行的狀態(tài)機(jī)節(jié)點的Update,FixedUpdate, HandleMessage接口。
這樣驅(qū)動了狀態(tài)機(jī)節(jié)點的相關(guān)接口的調(diào)用與執(zhí)行。寫好FiniteStateMachine, IFsmNode兩個代碼以后,狀態(tài)機(jī)就已經(jīng)設(shè)計完成了,接下來就是具體游戲項目中的使用。也就是與使用相關(guān)的代碼了。其實非常簡單,主要有3步:
Step1: 創(chuàng)建一個狀態(tài)機(jī)對象;
Step2: 我們要添加一個狀態(tài)機(jī)的邏輯節(jié)點,只要繼承IFsmNode,實現(xiàn)相關(guān)接口,并把邏輯節(jié)點放到狀態(tài)機(jī)對象中統(tǒng)一管理起來。
Step3: 根據(jù)業(yè)務(wù)邏輯來切換運(yùn)行的狀態(tài)機(jī)的節(jié)點。從而到達(dá)邏輯控制的目的。
?
3: 基于狀態(tài)機(jī)擴(kuò)展一些特殊的狀態(tài)控制
?
狀態(tài)機(jī)設(shè)計完成以后,我們還可以基于狀態(tài)機(jī)來做一些特殊的狀態(tài)控制,讓我們的邏輯代碼更清晰,維護(hù)起來更方便,比如最常見的順序執(zhí)行狀態(tài)機(jī)ProcedureFsm。就是說執(zhí)行完一個狀態(tài)節(jié)點,馬上執(zhí)行第二個狀態(tài)節(jié)點。這樣我們做順序流程就非常方便了,比如熱更新的順序流程狀態(tài)機(jī):
1: 檢查版本狀態(tài)節(jié)點;
2: 增量下載信息比對節(jié)點;
3: 增量下載資源節(jié)點;
4: 下載完成后進(jìn)入游戲節(jié)點;
把這些狀態(tài)機(jī)節(jié)點加入到ProcedureFsm中,那么它就會從第一個節(jié)點開始運(yùn)行,后面每個節(jié)點依次執(zhí)行。
?
項目中是否用狀態(tài)機(jī)的方式來做為你的邏輯控制,這個可以根據(jù)具體的需求來進(jìn)行分析。沒有絕對的好與壞,適合即可。
?
今天的分享就到這里,關(guān)注我(加入到學(xué)習(xí)群),可以獲取”Unity?狀態(tài)機(jī)”相關(guān)源碼與實現(xiàn)。
附:視頻教程
https://www.bycwedu.com/promotion_channels/2146264125?