http://wiki.unity3d.com/index.php?title=Advanced_CSharp_Messenger
Author: Ilya Suzdalnitski
Contents[hide] |
Description
This is an advanced version of a messaging system for C#. It will automatically clean up its event table after a new level has been loaded. This will prevent the programmer from accidentally invoking destroyed methods and thus will help prevent many MissingReferenceExceptions. This messaging system is based on Rod Hyde'sCSharpMessenger and Magnus Wolffelt's CSharpMessenger Extended.
Foreword
Upon introduction of a messaging system into our project (CSharpMessenger Extended) we started facing very strange bugs. Unity3d would throw MissingReferenceExceptions every time a message was broadcasted. It would say that the class, where the message handler was declared in, was destroyed. The problem came out of nowhere and there was not a reasonable explanation behind it. However placing the message handler code within try-catch blocks solved the problem. We understood that it was not a good solution to have hundreds of try-catch blocks in our code. It took us some time to finally figure out where was the problem.
Cause behind the MissingReferenceException and solution
It turned out, that the MissingReferenceException bug appeared, when a new level was loaded (or current one was reloaded). For example, we have a message "start game", declared as follows:
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"); } }
At first glance, there's no problem at all, but after the level has been reloaded, Unity3d will throw an exception, saying that MainMenu has been destroyed. However there's no code that would EVER destroy the MainMenu script.
What actually happened is:
- We added a "start game" message listener to our Messenger.
- StartGameButtonPressed was called which in turn broadcasted the "start game" message.
- We reloaded the level with Application.LoadLevel.
- Step 1 repeated.
- Step 2 repeated.
Here's how eventTable of the messenger looks like at corresponding steps:
- At step 1: { "start game", mainMenu1- > StartGame(); }
- At step 4: { "start game", mainMenu1- > StartGame(); } { "start game", mainMenu2- > StartGame(); }
So at step 4 we have two message handler for the same "start game" message - the first one is for the destroyed MainMenu object (got destroyed when reloaded a level), and the second one it for the current valid MainMenu object. It turns out that when we broadcast the "start game" message after reloading the level, the messenger invokes both - the destroyed and the valid message handlers. This is where the MissingReferenceException came from.
So the solution is obvious - clear the eventTable after unloading a level. There's nothing else the programmer has to do on his side to clean up the table, it's being done automatically.
The Messenger
We're happy to present you an advanced version of C# messaging system.
Usage
Event listener
void OnPropCollected( PropType propType ) {
if (propType == PropType.Life) livesAmount++; }
Registering an event listener
void Start() {
Messenger.AddListener< Prop >( "prop collected", OnPropCollected ); }
Unregistering an event listener
Messenger.RemoveListener< Prop > ( "prop collected", OnPropCollected );
Broadcasting an event
public void OnTriggerEnter(Collider _collider)
{
Messenger.Broadcast< PropType > ( "prop collected", _collider.gameObject.GetComponent<Prop>().propType ); }
Cleaning up the messenger
The messenger cleans up its eventTable automatically when a new level loads. This will ensure that the eventTable of the messenger gets cleaned up and will save us from unexpected MissingReferenceExceptions. In case you want to clean up manager's eventTable manually, there's such an option by calling Messenger.Cleanup();
Permanent messages
If you want a certain message to survive the Cleanup, mark it with Messenger.MarkAsPermanent(string). This may be required if a certain class responds to messages broadcasted from across different levels.
Misc
Log all messages
For debugging purposes, you can set the shouldLogAllMessages flag in Messenger to true. This will log all calls to the Messenger.
Transition from other messengers
To quickly change all calls to messaging system from older CSharpMessenger's to the advanced, do the following steps:
- In MonoDevelop go to Search => Replace in files
- In Find field enter: Messenger<([^<>]+)>.([A-Za-z0-9_]+)
- In Replace field enter: Messenger.$2<$1>
- Select scope: Whole solution.
- Check the Regex search check box.
- Press the Replace button
Code
There're two files required for the messenger to work - Callback.cs and Messenger.cs.
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