《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)