1. 程式人生 > >《Unity 3D遊戲客戶端基礎框架》訊息系統

《Unity 3D遊戲客戶端基礎框架》訊息系統

功能分析:

首先,我們必須先明確一個訊息系統的核心功能:

  • 一個通用的事件監聽器
  • 管理各個業務監聽的事件型別(註冊和解綁事件監聽器)
  • 全域性廣播事件
  • 廣播事件所傳引數數量和資料型別都是可變的(數量可以是 0~3,資料型別是泛型)

設計思路:

清楚了上述的幾個要求之後,我們不難自行定製一個業務層的訊息系統,即在訊息系統初始化時將每個模組繫結的訊息列表,根據訊息型別分類(用一個 string 型別的資料類標識),即建立一個字典 Dictionary<string,List<Model>> 每條訊息觸發時需要通知的模組列表,即某條訊息觸發,遍歷字典中繫結的模組列表,然後有兩種選擇方案:

  • 假如模組是與 gameObject 繫結的繼承自 MonoBehaviour 的指令碼,通過 Unity 原生的向指定模組傳送訊息的介面 gameObject.SendMessage(message) 傳送訊息,模組用則用 public void getMessage(string message) 函式接收訊息;

  • 假如模組是獨立的 C# 例項,則可以給模組設計一個基類,基類中有一個虛構函式,在具體模組中重寫這個函式,這樣訊息中心要想模組傳送觸發訊息時,直接將模組字典中繫結的模組引用強轉為基類型別,然後呼叫該虛構函式。

然而,這樣的 DIY 的訊息管理系統最常見的問題就是:模組已經銷燬了,但在字典中的引用還在,那麼訊息要傳遞給模組的時候,就會觸發 MissingReferenceException

或者 NullReferenceException 這類空引用錯誤。

外掛簡介:

Advanced CSharp Messenger 是一個 C# 高階版本的訊息傳遞系統 。它將會在載入一個新的 level 後自動清理其事件表。這將防止程式設計師意外地呼叫被毀壞的方法,從而將有助於防止很多 MissingReferenceExceptions。此訊息傳遞系統基於杆海德 CSharpMessenger 和馬格努斯沃爾費爾特的CSharpMessenger 擴充套件。

核心功能:

1.註冊一個事件監聽器:

Messenger.AddListener<T>( "訊息型別標識"
, OnCallback); //事件回撥函式 void OnCallback(T data){ }

監聽事件可以帶參也可不帶參,引數型別是泛型,既可以傳遞基礎資料型別,也可以傳遞 gameObject 物件,當然兩種情況屬於完全不同的時間,用一個字串來表示事件的型別,OnCallback 是事件出發時的回撥函式,回撥函式的引數表與監聽格式一致。

2.取消註冊事件監聽器:

這裡需要注意的就是與註冊時的引數格式完全一致,只是把 AddListener 改成 RemoveListener

Messenger.RemoveListener<T>( "訊息型別標識", OnCallback);

3.廣播事件:

//不帶參
Messenger.Broadcast( "訊息型別標識");
//帶參
Messenger.Broadcast<T>( "訊息型別標識", data1);

第一個引數是事件型別標識,後面的引數表是與 <T> 中指定的資料型別對應的回傳資料。

外掛引入:

只需在當前專案組新增一下兩個指令碼,即可開始使用 Advanced CSharp Messenger 這個訊息管理器來管理我們專案的訊息了。

Callback.cs

public delegate void Callback();
public delegate void Callback<T>(T arg1);
public delegate void Callback<T, U>(T arg1, U arg2);
public delegate void Callback<T, U, V>(T arg1, U arg2, V arg3);

Messenger.cs

/*
 * Advanced C# messenger by Ilya Suzdalnitski. V1.0
 * 
 * Based on Rod Hyde's "CSharpMessenger" and Magnus Wolffelt's "CSharpMessenger Extended".
 * 
 * Features:
    * Prevents a MissingReferenceException because of a reference to a destroyed message handler.
    * Option to log all messages
    * Extensive error detection, preventing silent bugs
 * 
 * Usage examples:
    1. Messenger.AddListener<GameObject>("prop collected", PropCollected);
       Messenger.Broadcast<GameObject>("prop collected", prop);
    2. Messenger.AddListener<float>("speed changed", SpeedChanged);
       Messenger.Broadcast<float>("speed changed", 0.5f);
 * 
 * Messenger cleans up its evenTable automatically upon loading of a new level.
 * 
 * Don't forget that the messages that should survive the cleanup, should be marked with Messenger.MarkAsPermanent(string)
 * 
 */

//#define LOG_ALL_MESSAGES
//#define LOG_ADD_LISTENER
//#define LOG_BROADCAST_MESSAGE
#define REQUIRE_LISTENER

using System;
using System.Collections.Generic;
using UnityEngine;

static internal class Messenger {
    #region Internal variables

    //Disable the unused variable warning
#pragma warning disable 0414
    //Ensures that the MessengerHelper will be created automatically upon start of the game.
    static private MessengerHelper messengerHelper = ( new GameObject("MessengerHelper") ).AddComponent< MessengerHelper >();
#pragma warning restore 0414

    static public Dictionary<string, Delegate> eventTable = new Dictionary<string, Delegate>();

    //Message handlers that should never be removed, regardless of calling Cleanup
    static public List< string > permanentMessages = new List< string > ();
    #endregion
    #region Helper methods
    //Marks a certain message as permanent.
    static public void MarkAsPermanent(string eventType) {
#if LOG_ALL_MESSAGES
        Debug.Log("Messenger MarkAsPermanent \t\"" + eventType + "\"");
#endif

        permanentMessages.Add( eventType );
    }


    static public void Cleanup()
    {
#if LOG_ALL_MESSAGES
        Debug.Log("MESSENGER Cleanup. Make sure that none of necessary listeners are removed.");
#endif

        List< string > messagesToRemove = new List<string>();

        foreach (KeyValuePair<string, Delegate> pair in eventTable) {
            bool wasFound = false;

            foreach (string message in permanentMessages) {
                if (pair.Key == message) {
                    wasFound = true;
                    break;
                }
            }

            if (!wasFound)
                messagesToRemove.Add( pair.Key );
        }

        foreach (string message in messagesToRemove) {
            eventTable.Remove( message );
        }
    }

    static public void PrintEventTable()
    {
        Debug.Log("\t\t\t=== MESSENGER PrintEventTable ===");

        foreach (KeyValuePair<string, Delegate> pair in eventTable) {
            Debug.Log("\t\t\t" + pair.Key + "\t\t" + pair.Value);
        }

        Debug.Log("\n");
    }
    #endregion

    #region Message logging and exception throwing
    static public void OnListenerAdding(string eventType, Delegate listenerBeingAdded) {
#if LOG_ALL_MESSAGES || LOG_ADD_LISTENER
        Debug.Log("MESSENGER OnListenerAdding \t\"" + eventType + "\"\t{" + listenerBeingAdded.Target + " -> " + listenerBeingAdded.Method + "}");
#endif

        if (!eventTable.ContainsKey(eventType)) {
            eventTable.Add(eventType, null );
        }

        Delegate d = eventTable[eventType];
        if (d != null && d.GetType() != listenerBeingAdded.GetType()) {
            throw new ListenerException(string.Format("Attempting to add listener with inconsistent signature for event type {0}. Current listeners have type {1} and listener being added has type {2}", eventType, d.GetType().Name, listenerBeingAdded.GetType().Name));
        }
    }

    static public void OnListenerRemoving(string eventType, Delegate listenerBeingRemoved) {
#if LOG_ALL_MESSAGES
        Debug.Log("MESSENGER OnListenerRemoving \t\"" + eventType + "\"\t{" + listenerBeingRemoved.Target + " -> " + listenerBeingRemoved.Method + "}");
#endif

        if (eventTable.ContainsKey(eventType)) {
            Delegate d = eventTable[eventType];

            if (d == null) {
                throw new ListenerException(string.Format("Attempting to remove listener with for event type \"{0}\" but current listener is null.", eventType));
            } else if (d.GetType() != listenerBeingRemoved.GetType()) {
                throw new ListenerException(string.Format("Attempting to remove listener with inconsistent signature for event type {0}. Current listeners have type {1} and listener being removed has type {2}", eventType, d.GetType().Name, listenerBeingRemoved.GetType().Name));
            }
        } else {
            throw new ListenerException(string.Format("Attempting to remove listener for type \"{0}\" but Messenger doesn't know about this event type.", eventType));
        }
    }

    static public void OnListenerRemoved(string eventType) {
        if (eventTable[eventType] == null) {
            eventTable.Remove(eventType);
        }
    }

    static public void OnBroadcasting(string eventType) {
#if REQUIRE_LISTENER
        if (!eventTable.ContainsKey(eventType)) {
            throw new BroadcastException(string.Format("Broadcasting message \"{0}\" but no listener found. Try marking the message with Messenger.MarkAsPermanent.", eventType));
        }
#endif
    }

    static public BroadcastException CreateBroadcastSignatureException(string eventType) {
        return new BroadcastException(string.Format("Broadcasting message \"{0}\" but listeners have a different signature than the broadcaster.", eventType));
    }

    public class BroadcastException : Exception {
        public BroadcastException(string msg)
            : base(msg) {
        }
    }

    public class ListenerException : Exception {
        public ListenerException(string msg)
            : base(msg) {
        }
    }
    #endregion

    #region AddListener
    //No parameters
    static public void AddListener(string eventType, Callback handler) {
        OnListenerAdding(eventType, handler);
        eventTable[eventType] = (Callback)eventTable[eventType] + handler;
    }

    //Single parameter
    static public void AddListener<T>(string eventType, Callback<T> handler) {
        OnListenerAdding(eventType, handler);
        eventTable[eventType] = (Callback<T>)eventTable[eventType] + handler;
    }

    //Two parameters
    static public void AddListener<T, U>(string eventType, Callback<T, U> handler) {
        OnListenerAdding(eventType, handler);
        eventTable[eventType] = (Callback<T, U>)eventTable[eventType] + handler;
    }

    //Three parameters
    static public void AddListener<T, U, V>(string eventType, Callback<T, U, V> handler) {
        OnListenerAdding(eventType, handler);
        eventTable[eventType] = (Callback<T, U, V>)eventTable[eventType] + handler;
    }
    #endregion

    #region RemoveListener
    //No parameters
    static public void RemoveListener(string eventType, Callback handler) {
        OnListenerRemoving(eventType, handler);   
        eventTable[eventType] = (Callback)eventTable[eventType] - handler;
        OnListenerRemoved(eventType);
    }

    //Single parameter
    static public void RemoveListener<T>(string eventType, Callback<T> handler) {
        OnListenerRemoving(eventType, handler);
        eventTable[eventType] = (Callback<T>)eventTable[eventType] - handler;
        OnListenerRemoved(eventType);
    }

    //Two parameters
    static public void RemoveListener<T, U>(string eventType, Callback<T, U> handler) {
        OnListenerRemoving(eventType, handler);
        eventTable[eventType] = (Callback<T, U>)eventTable[eventType] - handler;
        OnListenerRemoved(eventType);
    }

    //Three parameters
    static public void RemoveListener<T, U, V>(string eventType, Callback<T, U, V> handler) {
        OnListenerRemoving(eventType, handler);
        eventTable[eventType] = (Callback<T, U, V>)eventTable[eventType] - handler;
        OnListenerRemoved(eventType);
    }
    #endregion

    #region Broadcast
    //No parameters
    static public void Broadcast(string eventType) {
#if LOG_ALL_MESSAGES || LOG_BROADCAST_MESSAGE
        Debug.Log("MESSENGER\t" + System.DateTime.Now.ToString("hh:mm:ss.fff") + "\t\t\tInvoking \t\"" + eventType + "\"");
#endif
        OnBroadcasting(eventType);

        Delegate d;
        if (eventTable.TryGetValue(eventType, out d)) {
            Callback callback = d as Callback;

            if (callback != null) {
                callback();
            } else {
                throw CreateBroadcastSignatureException(eventType);
            }
        }
    }

    //Single parameter
    static public void Broadcast<T>(string eventType, T arg1) {
#if LOG_ALL_MESSAGES || LOG_BROADCAST_MESSAGE
        Debug.Log("MESSENGER\t" + System.DateTime.Now.ToString("hh:mm:ss.fff") + "\t\t\tInvoking \t\"" + eventType + "\"");
#endif
        OnBroadcasting(eventType);

        Delegate d;
        if (eventTable.TryGetValue(eventType, out d)) {
            Callback<T> callback = d as Callback<T>;

            if (callback != null) {
                callback(arg1);
            } else {
                throw CreateBroadcastSignatureException(eventType);
            }
        }
    }

    //Two parameters
    static public void Broadcast<T, U>(string eventType, T arg1, U arg2) {
#if LOG_ALL_MESSAGES || LOG_BROADCAST_MESSAGE
        Debug.Log("MESSENGER\t" + System.DateTime.Now.ToString("hh:mm:ss.fff") + "\t\t\tInvoking \t\"" + eventType + "\"");
#endif
        OnBroadcasting(eventType);

        Delegate d;
        if (eventTable.TryGetValue(eventType, out d)) {
            Callback<T, U> callback = d as Callback<T, U>;

            if (callback != null) {
                callback(arg1, arg2);
            } else {
                throw CreateBroadcastSignatureException(eventType);
            }
        }
    }

    //Three parameters
    static public void Broadcast<T, U, V>(string eventType, T arg1, U arg2, V arg3) {
#if LOG_ALL_MESSAGES || LOG_BROADCAST_MESSAGE
        Debug.Log("MESSENGER\t" + System.DateTime.Now.ToString("hh:mm:ss.fff") + "\t\t\tInvoking \t\"" + eventType + "\"");
#endif
        OnBroadcasting(eventType);

        Delegate d;
        if (eventTable.TryGetValue(eventType, out d)) {
            Callback<T, U, V> callback = d as Callback<T, U, V>;

            if (callback != null) {
                callback(arg1, arg2, arg3);
            } else {
                throw CreateBroadcastSignatureException(eventType);
            }
        }
    }
    #endregion
}

//This manager will ensure that the messenger's eventTable will be cleaned up upon loading of a new level.
public sealed class MessengerHelper : MonoBehaviour {
    void Awake ()
    {
        DontDestroyOnLoad(gameObject);  
    }

    //Clean up eventTable every time a new level loads.
    public void OnLevelWasLoaded(int unused) {
        Messenger.Cleanup();
    }
}

當然,假如為了指令碼管理方便,也可將兩部分程式碼都合併在同一個指令碼中,而且事件繫結的 key 都是以一個 string 來標誌的,為了統一管理訊息,這裡我又建立了一個指令碼 Msg_Define.cs

public class Msg_Define{
    public const string MSG_START = "MSG_START";
    public const string MSG_AWAKE = "MSG_AWAKE";
}

測試例項:

這裡我們可以直接在一個測試場景中新建一個C#測試指令碼,並都繫結到場景中的相機上(保證點選Unity執行時,指令碼會處於工作狀態),然後通過在指令碼中廣播一個事件(以多種傳參形式),然後在指令碼自身進行監聽,如此完成自發自收的測試:

指令碼 TestMsg.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TestMsg : MonoBehaviour {

    void Awake()
    {
        //監聽訊息
        Messenger.AddListener(Msg_Define.MSG_AWAKE, OnAwakeCall);
        Messenger.AddListener<int>(Msg_Define.MSG_START, OnStartCall);
        //傳送不帶引數廣播
        Messenger.Broadcast(Msg_Define.MSG_AWAKE);
    }

    void Start()
    {
        //傳送帶引數廣播
        Messenger.Broadcast<int>(Msg_Define.MSG_START,666);
    }

    void OnDestroy()
    {
        //移除監聽
        Messenger.RemoveListener(Msg_Define.MSG_AWAKE, OnAwakeCall);
        Messenger.RemoveListener<int>(Msg_Define.MSG_START, OnStartCall);
    }
    //訊息回撥
    void OnAwakeCall()
    {
        Debug.logger.Log("awake");
    }

    void OnStartCall(int num)
    {
        Debug.logger.Log("start"+num);
    }
}

執行 Unity,可以在 Unity 的 Console 面板中看到輸出結果:

awake
UnityEngine.Logger:Log(Object)
start666
UnityEngine.Logger:Log(Object)

參考資料: