Advanced CSharp Messenger

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:

  1. We added a "start game" message listener to our Messenger.
  2. StartGameButtonPressed was called which in turn broadcasted the "start game" message.
  3. We reloaded the level with Application.LoadLevel.
  4. Step 1 repeated.
  5. 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:

  1. In MonoDevelop go to Search => Replace in files
  2. In Find field enter: Messenger<([^<>]+)>.([A-Za-z0-9_]+)
  3. In Replace field enter: Messenger.$2<$1>
  4. Select scope: Whole solution.
  5. Check the Regex search check box.
  6. 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.Log(

转载于:https://www.cnblogs.com/mimime/p/6240239.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值