1. 程式人生 > >遊戲架構之模組間通訊(訊息機制)

遊戲架構之模組間通訊(訊息機制)

一、

先談一談個人對遊戲框架的一點理解,顧名思義,框架是一個專案的骨架,如同大樹的主幹,搭建框架,在此基礎上再加入各個功能模組,構成有一個完整的專案。如同一棵樹有一個健壯的主幹,再從主幹上生長出一個一個的分支,最終長成一顆枝繁葉茂的大樹。此外,框架會設定好模組的基本格式,更加有利於功能的模組化;框架還負責各個模組之間的互動,每個模組作為一個獨立的個體,內部是獨立執行的,如果模組間需要進行一些互動,則需要通過框架來實現,避免模組間直接通訊,最終模組關係錯綜複雜,難以維護。

二、

模組間的資料互動、資訊傳遞是框架中比較重要的一部分,最近根據做過的幾個專案和一些資料,編寫了一套簡單的模組間資訊傳遞機制,在此之前也發過幾篇關於模組封裝的博文,組裝到一起,應該也是可以用了。

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 在收到訊息之後,遍歷所有的節點,執行回撥類的回撥函式。

關於遊戲架構,訊息/通知機制只是其中的一部分,還有很多很多需要去學習去實踐,希望以上的內容可以幫助到大家~~~