首先,我们必须先明确一个消息系统的核心功能:
清楚了上述的几个要求之后,我们不难自行定制一个业务层的消息系统,即在消息系统初始化时将每个模块绑定的消息列表,根据消息类型分类(用一个 string 类型的数据类标识),即建立一个字典 Dictionary
每条消息触发时需要通知的模块列表,即某条消息触发,遍历字典中绑定的模块列表,然后有两种选择方案:
假如模块是与 gameObject 绑定的继承自 MonoBehaviour 的脚本,通过 Unity 原生的向指定模块发送消息的接口 gameObject.SendMessage(message)
发送消息,模块用则用 public void getMessage(string message)
函数接收消息;
假如模块是独立的 C# 实例,则可以给模块设计一个基类,基类中有一个虚构函数,在具体模块中重写这个函数,这样消息中心要想模块发送触发消息时,直接将模块字典中绑定的模块引用强转为基类类型,然后调用该虚构函数。
然而,这样的 DIY 的消息管理系统最常见的问题就是:模块已经销毁了,但在字典中的引用还在,那么消息要传递给模块的时候,就会触发 MissingReferenceException
或者 NullReferenceException
这类空引用错误。
Advanced CSharp Messenger
是一个 C# 高级版本的消息传递系统 。它将会在加载一个新的 level 后自动清理其事件表。这将防止程序员意外地调用被毁坏的方法,从而将有助于防止很多 MissingReferenceExceptions
。此消息传递系统基于杆海德 CSharpMessenger
和马格努斯沃尔费尔特的CSharpMessenger
扩展。
Messenger.AddListener( "消息类型标识", OnCallback);
//事件回调函数
void OnCallback(T data){
}
监听事件可以带参也可不带参,参数类型是泛型,既可以传递基础数据类型,也可以传递 gameObject 对象,当然两种情况属于完全不同的时间,用一个字符串来表示事件的类型,OnCallback
是事件出发时的回调函数,回调函数的参数表与监听格式一致。
这里需要注意的就是与注册时的参数格式完全一致,只是把 AddListener
改成 RemoveListener
:
Messenger.RemoveListener( "消息类型标识", OnCallback);
//不带参
Messenger.Broadcast( "消息类型标识");
//带参
Messenger.Broadcast( "消息类型标识", data1);
第一个参数是事件类型标识,后面的参数表是与
中指定的数据类型对应的回传数据。
只需在当前项目组添加一下两个脚本,即可开始使用 Advanced CSharp Messenger
这个消息管理器来管理我们项目的消息了。
Callback.cs
public delegate void Callback();
public delegate void Callback(T arg1);
public delegate void Callback(T arg1, U arg2);
public delegate void Callback(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("prop collected", PropCollected);
Messenger.Broadcast("prop collected", prop);
2. Messenger.AddListener("speed changed", SpeedChanged);
Messenger.Broadcast("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(string eventType, Callback handler) {
OnListenerAdding(eventType, handler);
eventTable[eventType] = (Callback)eventTable[eventType] + handler;
}
//Two parameters
static public void AddListener(string eventType, Callback handler) {
OnListenerAdding(eventType, handler);
eventTable[eventType] = (Callback)eventTable[eventType] + handler;
}
//Three parameters
static public void AddListener(string eventType, Callback handler) {
OnListenerAdding(eventType, handler);
eventTable[eventType] = (Callback)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(string eventType, Callback handler) {
OnListenerRemoving(eventType, handler);
eventTable[eventType] = (Callback)eventTable[eventType] - handler;
OnListenerRemoved(eventType);
}
//Two parameters
static public void RemoveListener(string eventType, Callback handler) {
OnListenerRemoving(eventType, handler);
eventTable[eventType] = (Callback)eventTable[eventType] - handler;
OnListenerRemoved(eventType);
}
//Three parameters
static public void RemoveListener(string eventType, Callback handler) {
OnListenerRemoving(eventType, handler);
eventTable[eventType] = (Callback)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(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 callback = d as Callback;
if (callback != null) {
callback(arg1);
} else {
throw CreateBroadcastSignatureException(eventType);
}
}
}
//Two parameters
static public void Broadcast(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 callback = d as Callback;
if (callback != null) {
callback(arg1, arg2);
} else {
throw CreateBroadcastSignatureException(eventType);
}
}
}
//Three parameters
static public void Broadcast(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 callback = d as Callback;
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)