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("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();
}
}

Advanced CSharp Messenger的更多相关文章

  1. 【转】Unity3D研究院之通过C#使用Advanced CSharp Messenger(五十)

    http://www.xuanyusong.com/archives/2165 Advanced CSharp Messenger 属于C#事件的一种. 维基百科中由详细的说明http://wiki. ...

  2. Unity3D之通过C#使用Advanced CSharp Messenger

    Advanced CSharp Messenger 属于C#事件的一种. 维基百科中由详细的说明http://wiki.unity3d.com/index.php?title=Advanced_CSh ...

  3. Unity消息简易框架 Advanced C# messenger

    Unity消息简易框架 Advanced C# messenger Unity C# 消息机制  [转载 雨凇MOMO博客] https://www.xuanyusong.com/archives/2 ...

  4. 【转】NGUI研究院之三种方式监听NGUI的事件方法(七)

    NGUI事件的种类很多,比如点击.双击.拖动.滑动等等,他们处理事件的原理几乎万全一样,本文只用按钮来举例. 1.直接监听事件 把下面脚本直接绑定在按钮上,当按钮点击时就可以监听到,这种方法不太好很不 ...

  5. unity客户端基本框架(转载)

    框架概述: 基础系统的框架搭建,其中包括: UI框架(NGUI + MVC) 消息管理(Advanced CSharp Messenger) 网络层框架(Socket + Protobuf ) 表格数 ...

  6. 三种方式监听NGUI的事件方法

    NGUI研究院之三种方式监听NGUI的事件方法(七) NGUI事件的种类很多,比如点击.双击.拖动.滑动等等,他们处理事件的原理几乎万全一样,本文只用按钮来举例. 1.直接监听事件 把下面脚本直接绑定 ...

  7. (转)NGUI研究院之三种方式监听NGUI的事件方法

    NGUI事件的种类很多,比如点击.双击.拖动.滑动等等,他们处理事件的原理几乎万全一样,本文只用按钮来举例. 1.直接监听事件 把下面脚本直接绑定在按钮上,当按钮点击时就可以监听到,这种方法不太好很不 ...

  8. 《Unity 3D游戏客户端基础框架》消息系统

    功能分析: 首先,我们必须先明确一个消息系统的核心功能: 一个通用的事件监听器 管理各个业务监听的事件类型(注册和解绑事件监听器) 全局广播事件 广播事件所传参数数量和数据类型都是可变的(数量可以是 ...

  9. 《Unity 3D游戏客户端基础框架》概述

    框架概述: 做了那么久的业务开发,也做了一年多的核心战斗开发,最近想着自己倒腾一套游戏框架,当然暂不涉及核心玩法类型和战斗框架,核心战斗的设计要根据具体的游戏类型而定制,这里只是一些通用的基础系统的框 ...

随机推荐

  1. 获取MSSQL Server中的相关信息(视图、存储过程、触发器、表)

    在SQL SERVER得到某个数据库下面所有的表.视图.存储过程.触发器 select name from sysobjects where xtype='TR' --所有触发器select name ...

  2. Eclipse中导入外部jar包

    首先在项目下创建一个文件夹,保存我们的jar包. 在项目名上右击,依次点击[New]-->[Floder],打开新建文件夹窗口   输入文件夹名称[lib],点击[ok].我们通常在lib文件夹 ...

  3. 【HTML5】Video + DOM

    效果图: <!DOCTYPE html> <html> <body> <div style="text-align:center;"> ...

  4. android 获取路径目录方法以及判断目录是否存在,创建目录

    Environment 常用方法: * 方法:getDataDirectory()解释:返回 File ,获取 Android 数据目录.* 方法:getDownloadCacheDirectory( ...

  5. eclipse常用快捷键,这个只要新学会的常用的会陆续更新的。

    1.Ctrl+Shift+O     引用包 2.Ctrl+Shift+F      格式化代码 3.Ctrl + /  注释和解除注释代码 4.Ctrl+M  代码最大最小化 5.ctrl+shif ...

  6. IE下Checkbox标签的onchange事件兼容

    Checkbox onchange事件在谷歌上ok,在ie8上不起作用了. 一番周折,测试发现勾选了以后还要点击其他位置才会触发onchange事件. 用度娘查询了一下. 有下面两种解决方式: 用on ...

  7. SU Demos-02Filtering-03Sudipfilt

    不足之处,欢迎各位博友批评指正. 进入目录,依照惯例先看Readme, 第一个脚本, 下面是运行结果: 第二个脚本: 运行结果如下: 第三个脚本: 运行结果:

  8. 简单几何(凸包+多边形面积) POJ 3348 Cows

    题目传送门 题意:求凸包 + (int)求面积 / 50 /************************************************ * Author :Running_Tim ...

  9. webapp开发经验总结

    webapp开发的大趋势之下,本人收集整理了一写关于webapp开发的经验,欢迎大家补充指正. 关于link <link rel="apple-touch-startup-image& ...

  10. 【TYVJ】1307 联络员(最小生成树)

    http://tyvj.cn/Problem_Show.aspx?id=1307 kruskal裸题.(水题红色警报) #include <cstdio> #include <cst ...