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. JSON浅总

    我们在以前的学习中了解到XML是一种结构化的数据表示方式,一种可扩展标记语言!可以把XML理解成一个微型的结构化的小的数据库,保存一些小型的数据和传输数据,有严格的显示限制.但是XML语句有些冗长和繁 ...

  2. Android Studio 获取 sha1-wangfeng520@

    WIN+R   打开“运行”  输入  CMD 回车 2 CD C:\Program Files\Java\jdk1.7.0_71\bin     (JDK安装路径) keytool -list -v ...

  3. 【原创】CDM添加新磁盘,然后负载

    hdfs快占满了,所以为节点中添加新的磁盘(这块是个教训,以后用新的节点时,磁盘需要一次性插满,省得后续再添加磁盘了) 注意: 添加磁盘时,不仅仅datanode在配置时添加节点,nodemanage ...

  4. WPF/MVVM 快速开始指南(译)(转)

    WPF/MVVM 快速开始指南(译) 本篇文章是Barry Lapthorn创作的,感觉写得很好,翻译一下,做个纪念.由于英文水平实在太烂,所以翻译有错或者译得不好的地方请多指正.另外由于原文是针对W ...

  5. JVM的粗略简述

    什么是Java虚拟机 虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的.Java虚拟机有自己完善的硬体架构,如处理器.堆栈.寄存器等,还具有相应的指令系统.JVM屏蔽了与 ...

  6. 水题 ZOJ 3876 May Day Holiday

    题目传送门 /* 水题:已知1928年1月1日是星期日,若是闰年加1,总天数对7取余判断就好了: */ #include <cstdio> #include <iostream> ...

  7. windows 8 系统部署IIS并发布网站

    企业用户可以在已经部署了windows 8 的电脑中通过部署IIS服务器来发布自己公司的企业内部网站实现对企业的网络办公的管理工作. 准备篇 IIS的添加和运行 一.IIS的添加 1.请进入“控制面板 ...

  8. BZOJ3560 : DZY Loves Math V

    因为欧拉函数是非完全积性函数,所以可以考虑对每个数进行分解质因数,将每个质数的解乘起来即可. 对于一个质数$p$,设它在各个数中分别出现了$b_1,b_2,...b_n$次,那么由生成函数和欧拉函数的 ...

  9. HDU 2896 (AC自动机模板题)

    题目链接: http://acm.hdu.edu.cn/showproblem.php?pid=2896 题目大意:多个模式串.多个匹配串.其中串的字符范围是(0~127).问匹配串中含有哪几个模式串 ...

  10. BestCoder Round #73

    这场比赛打完后可以找何神玩了orz(orz)* T1Rikka with Chess 嘿嘿嘿.输出n/2+m/2即可. 我能说我智商捉鸡想了4min吗? T2Rikka with Graph 由于N个 ...