做者:Ilya Suzdalnitskiphp
譯自:http://wiki.unity3d.com/index.php/Advanced_CSharp_Messenger程序員
這是C#的一個高級版本的消息系統。當加載了一個新的場景(level)以後,它會自動清空事件表。這將防止程序員意外的調用已銷燬的方法,從而有助於防止出現不少MissingReferenceExceptions。這個消息系統是基於Rod Hyde的CSharpMessenger和Magnus Wolffelt的CSharpMessenger Extended而研發的。less
咱們的項目一旦引入了消息系統(CSharpMessenger Extended),咱們就開始面對很是奇怪的BUGs。每次廣播一個消息,U3D就會拋出MissingReferenceExceptions這個異常。提示會說消息處理器聲明所在類已經被銷燬。這個問題無處不在,而且沒有一個合理的解釋。不過,把消息處理器代碼放到try-cache塊中能夠解決這個問題。咱們知道,在代碼中有大量的try-cache塊不是一個好的解決方案。咱們花費了一些時間,終於找到了問題的所在。ide
是這樣的,當加載一個新場景(level)(或者從新加載當前場景)時,MissingReferenceException的BUG就會出現。例如:咱們有一個消息"start game",聲明以下:this
public class MainMenu : MonoBehaviour { void Start () { Messenger.AddListener("start game", StartGame); } void StartGame() { Debug.Log("StartGame called in" + gameObject); //This is the line that would throw an exception } void StartGameButtonPressed() { Messenger.Broadcast("start game"); } }
乍一看,代碼徹底沒有問題。可是在從新加載了這個場景以後,U3D將會拋出一個異常,提示說MainMenu對象已經被銷燬。可是沒有代碼會銷燬MainMenu腳本對象。spa
實際發生的是:3d
在相應的步驟,信使的事件表裏是這個樣子的:調試
因此在第4步咱們有兩個"start game"消息處理器——第1個是已經銷燬的MainMenu對象(在從新加載場景時被銷燬),第2個是當前有效的MainMenu對象。結果是這樣的,當咱們在從新加載場景後廣播"start game"消息時,信使把兩個消息處理器(已經銷燬的和當前有效的)都調用了。這裏就是MissingReferenceException出現的源頭。 那麼解決方法顯而易見——在卸載場景以後清空eventTable。在程序員方面不用作任何事來清空這個表,這一過程將自動完成。 rest
void OnPropCollected( PropType propType ) { if (propType == PropType.Life) livesAmount++; }
void Start() { Messenger.AddListener< Prop >( "prop collected", OnPropCollected ); }
Messenger.RemoveListener< Prop > ( "prop collected", OnPropCollected );
public void OnTriggerEnter(Collider _collider) { Messenger.Broadcast< PropType > ( "prop collected", _collider.gameObject.GetComponent<Prop>().propType ); }
當加載一個新的場景(level)時,信使會自動清空它的eventTable。這將確保信使的eventTable被清空,而且將使咱們免於意外的MissingReferenceExceptions。若是你想手動清空管理器的eventTable,你能夠調用Messenger.Cleanup()。
若是你想要某個消息倖免於Cleanup,使用Messenger.MarkAsPermanent(string)來標記它既可。它可能用於這樣的場合:某個類響應不一樣場景(level)的消息廣播。
爲了調試的目的,你能夠把信使的shouldLogAllMessages設置成true。這樣,在調用信使的任何方法時都會打印消息。
爲了快速把舊的消息系統CSharpMessenger轉變成高級的消息系統,請執行下面的步驟:
想要信使順利運做須要兩個文件:Callback.cs和Messenger.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);
/* * 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(); } }