遊戲架構之模組間通訊(訊息機制)
一、
先談一談個人對遊戲框架的一點理解,顧名思義,框架是一個專案的骨架,如同大樹的主幹,搭建框架,在此基礎上再加入各個功能模組,構成有一個完整的專案。如同一棵樹有一個健壯的主幹,再從主幹上生長出一個一個的分支,最終長成一顆枝繁葉茂的大樹。此外,框架會設定好模組的基本格式,更加有利於功能的模組化;框架還負責各個模組之間的互動,每個模組作為一個獨立的個體,內部是獨立執行的,如果模組間需要進行一些互動,則需要通過框架來實現,避免模組間直接通訊,最終模組關係錯綜複雜,難以維護。
二、
模組間的資料互動、資訊傳遞是框架中比較重要的一部分,最近根據做過的幾個專案和一些資料,編寫了一套簡單的模組間資訊傳遞機制,在此之前也發過幾篇關於模組封裝的博文,組裝到一起,應該也是可以用了。
1. 訊息:由唯一訊息ID、訊息體組成(有的寫法也會將訊息ID分離出訊息體,不包含在訊息體內,這樣方便訊息轉發都多個不同模組,但不便於管理)。訊息ID,用int值表示,根據需求劃分一定數量的ID給每個模組,模組內部單獨管理;訊息體:資料資訊的載體,一般是一個子類,這樣方便不同模組自定義資料格式。
要注意一點,跨模組訊息,A模組需要B模組的資料,就需要註冊B模組的訊息,這樣B在傳送訊息之後,只要註冊過這條訊息的模組,都會接收到訊息,這也要求模組內定義ID後,不能隨意變動ID,建議採用列舉表示,使用時將列舉轉為Int。
2. 建立訊息中心,儲存所有的訊息及對應接收回調函式,各模組通過管理者將訊息註冊到訊息中心,有對外的發訊息介面,供各模組呼叫,當然同樣要有登出介面。在收到訊息之後執行對應的回撥函式,將引數傳遞到註冊過訊息的多個具體模組,模組內部自行處理。
3. 各個模組管理者,在指令碼執行開始,註冊所需要的訊息,在指令碼待銷燬的時候登出,提供一個訊息接收回調,訊息中心會將訊息下發到回撥,然後內部處理訊息。
4. 關於訊息中心儲存記錄訊息,我用的字典Dictionary<int,委託>儲存對應的ID和回撥,利用委託的一個優點就是委託的“+=”和“-=”,比如有多個模組註冊了同一個訊息,可以將callback+=newCallback,這樣來把所有的回撥記錄下來,在登出時減掉。
但委託減法具有不可預測的結果,雖然改成Event事件可以避免程式報錯,但結果與委託一樣也會有這種問題,為了避免出現問題,在使用減法時,每次只減掉一個元素(即 a-= b,不要a-=(b+c) ),就不會發生意外了,可以忽略程式碼裡的警告了
http://www.jetbrains.com/help/resharper/2018.2/DelegateSubtraction.html
Demo程式碼如下,寫的比較簡單,功能還待完善~~
//訊息中心主要負責註冊、登出訊息,傳送訊息到對應模組的回撥
public delegate void CallbackDele(Msg msg);
//訊息體 父類
public class Msg
{
public int msgId { get; protected set; }
public object sender { get; protected set; }
}
//訊息中心
public class NotifyManager : MonoBehaviour
{
//單例
static NotifyManager instance;
public static NotifyManager Instance
{
get
{
if (instance == null)
{
GameObject newObj = new GameObject("NotifyCenter");
instance = newObj.AddComponent<NotifyManager>();
DontDestroyOnLoad(newObj);
}
return instance;
}
}
//記錄已註冊訊息
Dictionary<int, CallbackDele> callbackDic = new Dictionary<int, CallbackDele>();
//記錄待處理事件
Queue<Action> todoCallback = new Queue<Action>();
//註冊訊息
public bool Attach(CallbackDele callback, int[] msgIds)
{
if (callback == null)
return false;
for (int i = 0; i < msgIds.Length; i++)
{
Attach(callback, msgIds[i]);
}
return true;
}
public bool Attach(CallbackDele callback, int msgId)
{
if (callback == null)
return false;
if (!callbackDic.ContainsKey(msgId))
{
callbackDic.Add(msgId, callback);
}
else
{
callbackDic[msgId] += callback;
}
return true;
}
//登出訊息
public bool Detach(CallbackDele callback, int[] msgIds)
{
if (callback == null)
return false;
for (int i = 0; i < msgIds.Length; i++)
{
Detach(callback, msgIds[i]);
}
return true;
}
public bool Detach(CallbackDele callback, int msgId)
{
if (!callbackDic.ContainsKey(msgId) || callback == null)
return false;
//委託減法具有不可預測的結果:官方文件解釋
//http://www.jetbrains.com/help/resharper/2018.2/DelegateSubtraction.html
//每次減掉一個委託,不會發生意外,可忽略該警告
callbackDic[msgId] -= callback;
if (callbackDic[msgId] == null)
callbackDic.Remove(msgId);
return true;
}
//通知/廣播/分發訊息
public bool PostMsg(Msg msg = null)
{
if (msg.msgId > 0 && callbackDic.ContainsKey(msg.msgId) && null != callbackDic[msg.msgId])
{
//加入佇列
lock (todoCallback)
{
todoCallback.Enqueue(() => callbackDic[msg.msgId](msg));
}
return true;
}
return false;
}
//重新整理待處理訊息事件
void Update()
{
if (todoCallback.Count == 0)
return;
lock (todoCallback)
{
while (todoCallback.Count > 0)
{
todoCallback.Dequeue()();
}
todoCallback.Clear();
}
}
}
//每個訊息對應唯一ID,每個模組分配一定數量的ID,定義模組的起始ID
public class MsgIdSetting
{
public const int mgrIdSpan = 100;
}
public enum MgrId
{
//分模組劃分訊息ID,定義Id起點個長度,每個模組單獨管理自己的Id
//0~~99
demo01MgrId = 0,
//100~~199
demo02MgrId = MsgIdSetting.mgrIdSpan * 1,
//200~~299
demo03MgrId = MsgIdSetting.mgrIdSpan * 2,
// ··· ···
}
//單例模板,每個模組管理者繼承模板
public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
//單例
private static T instance;
public static T Instance
{
get
{
if (instance == null)
{
instance = FindObjectOfType(typeof(T)) as T;
if (instance == null)
{
GameObject newObj = new GameObject(typeof(T).ToString());
instance = newObj.AddComponent<T>();
}
}
return instance;
}
}
}
//測試Demo
//模組管理者需要定義自己的訊息體格式,訊息ID,在指定的時機註冊、登出所需訊息
//任何指令碼都可以傳送訊息,傳送後會執行對應註冊的callback回撥
public enum Demo01MsgId
{
//模組訊息Id,獲取起點Id,依次取值
dufaultId = MgrId.demo01MgrId,
creatRole = MgrId.demo02MgrId + 1,
deleteRole = MgrId.demo03MgrId + 2,
// ······
}
public class Demo01Msg : Msg
{
//模組自定義訊息體
public Demo01Msg(int newmsgId, string newname, bool newsexual, int newage, object newsender = null)
{
msgId = newmsgId;
name = newname;
sexual = newsexual;
age = newage;
sender = newsender;
}
public string name;
public bool sexual;
public int age;
}
public class Demo01Manager : MgrSingle<Demo01Manager>
{
//Awake
protected override void Awake()
{
NotifyManager.Instance.Attach(Callback, new int[] { (int)Demo01MsgId.creatRole, (int)Demo01MsgId.deleteRole });
}
void OnDestroy()
{
NotifyManager.Instance.Detach(Callback, new int[] { (int)Demo01MsgId.creatRole, (int)Demo01MsgId.deleteRole });
}
void Callback(Msg msg)
{
if (msg == null || msg.msgId <= 0)
{
Debug.Log("Receive A Empty Msg");
}
else
{
switch(msg.msgId)
{
case (int)Demo01MsgId.creatRole:
Demo01Msg creatRoleMsg = msg as Demo01Msg;
Debug.Log("Creat Role: " + creatRoleMsg.name + "-" + creatRoleMsg.sexual + "-" + creatRoleMsg.age);
break;
case (int)Demo01MsgId.deleteRole:
Demo01Msg deleteRoleMsg = msg as Demo01Msg;
Debug.Log("Delete Role: " + deleteRoleMsg.name + "-" + deleteRoleMsg.sexual + "-" + deleteRoleMsg.age);
break;
default:
Debug.LogWarning("Receive A Msg Without Callback");
break;
//······
}
}
}
public void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
SendMsg();
}
void SendMsg()
{
Demo01Msg msg = new Demo01Msg((int)Demo01MsgId.creatRole, "XiaoMing", true, 18, this);
NotifyManager.Instance.PostMsg(msg);
}
}
public class Demo02Manager : MgrSingle<Demo02Manager>
{
//Awake
protected override void Awake()
{
NotifyManager.Instance.Attach(Callback, (int)Demo01MsgId.creatRole);
}
void OnDestroy()
{
NotifyManager.Instance.Detach(Callback, (int)Demo01MsgId.creatRole);
}
void Callback(Msg msg)
{
if (msg == null)
{
Debug.Log("Receive A Empty Msg");
}
else
{
switch (msg.msgId)
{
case (int)Demo01MsgId.creatRole:
Demo01Msg creatRoleMsg = msg as Demo01Msg;
Debug.Log("Demo02 Receive Demo01 Msg: Creat Role");
break;
default:
Debug.LogWarning("Receive A Msg Without Callback");
break;
//······
}
}
}
}
三、
以上只是一種比較常見的訊息機制,在此基礎上還可以進行改進、封裝,因為涉及到一些專案,這裡不粘程式碼了,簡單說一下設計思路吧
1. 訊息分類:這一點與上面Demo一樣,按模組對訊息進行分類
2. 訊息中心:每個模組的管理者作為一個訊息中心,負責本模組訊息的 註冊、登出、處理。總訊息中心,不處理具體訊息,只負責不同模組間的訊息流轉。
要註冊一條訊息,模組先判斷是否是本模組訊息,是的話直接註冊到本模組,若不是,則轉發到上級的訊息中心,訊息中心再將訊息識別下發到對應的模組,對應模組進行註冊。
原本所有的訊息都是在訊息中心進行處理,現在在模組內部處理,訊息中心只負責將訊息下發到對應的模組即可。
比如說北京有一個快遞中心,一天,在朝陽區的A要寄快遞給海淀區的B,A找到朝陽區的快遞員上門取件,快遞員取件後將快遞送到快遞中心,再由快遞中心識別快遞物品,委派海淀區的快遞員將快遞配送給B。但第二天,朝陽區的A想要寄快遞給同在朝陽區的C,同樣朝陽區的快遞員會上門取件,然後將快遞送到快遞中心,經識別後將快遞委派給朝陽區的快遞員,再配送給C。這樣就顯得比較繁瑣了,快遞中心的負荷也會非常大。
快遞中心感覺這樣好心累,要進行改革,於是增大了快遞員的權利,可以直接處理自己負責地區的快遞,無需再經過快遞中心。這樣A在寄快遞給C的時候,朝陽區的快遞員從A取件之後,發現這是朝陽地區內的快遞,是寄給C的,就可以直接配送給C,省時又省力。如果A再給B寄快遞,朝陽區的快遞員取件之後,識別快遞是其他地區的,就直接將快遞送到快遞中心,快遞中心收到之後,只需識別是海淀區的,無需關注收件人是誰,再將快遞流轉到海淀區的快遞員,由該快遞員配送到C手中。這樣來,整個快遞流程就完美了~~
3. 記錄訊息及其回撥:
網上搜到的大都是用委託或事件來記錄訊息的,前面也提到過,利用“+=”“-=”計算可以記錄一條訊息和多個回撥。
也想過用每個訊息用一個List來記錄所有的回撥,但明顯這樣是不可取的。
這裡介紹另一種記錄方式:
記錄的不是具體某個回撥函式,而是採用連結串列的方式記錄回撥函式所在的類。
3.1 寫一個父類或介面,定義一個Callback函式,所有的模組管理者,都重寫或實現該函式,用作訊息的回撥。
3.2 定義一個訊息節點類(包含兩個屬性,本節點的回撥指令碼,下一個節點Next),註冊訊息的第一個回撥類後,其Next指向第二個回撥類,依次類推,記錄一個訊息的所有回撥類。
3.3 只需記錄訊息ID和第一個節點,獲取第一個節點後依次獲取節點的Next節點,知道Next節點為空,即遍歷完所有節點。
3.4 在收到訊息之後,遍歷所有的節點,執行回撥類的回撥函式。
關於遊戲架構,訊息/通知機制只是其中的一部分,還有很多很多需要去學習去實踐,希望以上的內容可以幫助到大家~~~