Unity3D - 关于Delegate - SignalSlot信息槽的使用和SendMessage取替

我们知道Unity3D自身有SendMessage向对象之间发送消息,但这个消耗是比较大的,因为它很大程度上涉及了Reflection发射机制。

如何变更思路,结合C#自带的消息系统delegate委托事件,对此进行优化:

我们看以下一个简单的delegate使用:

 

[csharp] view plaincopyprint?
 
  1. public class DelegateBasic : MonoBehaviour {  
  2.       
  3.     //define my delegate statement.  
  4.     public delegate void MyDelegate(string arg1);  
  5.       
  6.     //create my delegate object  
  7.     public MyDelegate myDelegate;  
  8.       
  9.     // Use this for initialization  
  10.     void Start () {  
  11.         myDelegate += myFunciton1;  
  12.         myDelegate += myFunciton2;  
  13.     }  
  14.       
  15.     // Update is called once per frame  
  16.     void Update () {  
  17.       
  18.     }  
  19.       
  20.       
  21.     void OnGUI()  
  22.     {  
  23.         if(GUILayout.Button("INVOKE"))  
  24.         {  
  25.             myDelegate("Invoke....");  
  26.         }  
  27.           
  28.     }  
  29.       
  30.     void myFunciton1(string s)  
  31.     {  
  32.         Debug.Log("myFunciton1 " + s);  
  33.     }  
  34.       
  35.     void myFunciton2(string s)  
  36.     {  
  37.         Debug.Log("myFunciton2 " + s);  
  38.     }  
  39.       
  40.       
  41. }  

 

以上只是实现myDelegate一旦调用,那么连接了该委托的事件的方法都会被调用。

但是以上我们看到,这只是1个delegate类型对应 —— 多个与该委托定义形式一致的方法;假如我需要不同的delegate呢?而且这些方法形式定义不一样,也或者说我们需要方法传参不一样呢?

 

一、Notification通知中心结构:参考http://wiki.unity3d.com/index.php/Category:Messaging 这里有很多个写好的例子可以使用。

 

[csharp] view plaincopyprint?
 
  1. using UnityEngine;  
  2. using System.Collections;  
  3.   
  4.   
  5. // Each notification type should gets its own enum  
  6. public enum NotificationType {  
  7.     OnStuff,  
  8.     OnOtherStuff,  
  9.     OnSomeEvent,  
  10.     TotalNotifications  
  11. };  
  12.   
  13. public delegate void OnNotificationDelegate( Notification note );  
  14.   
  15. public class NotificationCenter  
  16. {  
  17.     private static NotificationCenter instance;  
  18.   
  19.     private OnNotificationDelegate [] listeners = new OnNotificationDelegate[(int)NotificationType.TotalNotifications];  
  20.   
  21.     // Instead of constructor we can use void Awake() to setup the instance if we sublcass MonoBehavoiur  
  22.     public NotificationCenter()  
  23.     {  
  24.         if( instance != null )  
  25.         {  
  26.             Debug.Log( "NotificationCenter instance is not null" );  
  27.             return;  
  28.         }  
  29.         instance = this;  
  30.     }  
  31.       
  32.       
  33.     ~NotificationCenter()  
  34.     {  
  35.         instance = null;  
  36.     }  
  37.   
  38.   
  39.     public static NotificationCenter defaultCenter  
  40.     {  
  41.         get  
  42.         {  
  43.             if( instance == null )  
  44.                 new NotificationCenter();  
  45.             return instance;  
  46.         }  
  47.     }  
  48.   
  49.   
  50.     public void addListener( OnNotificationDelegate newListenerDelegate, NotificationType type )  
  51.     {  
  52.         int typeInt = (int)type;  
  53.         listeners[typeInt] += newListenerDelegate;  
  54.     }  
  55.   
  56.   
  57.     public void removeListener( OnNotificationDelegate listenerDelegate, NotificationType type )  
  58.     {  
  59.         int typeInt = ( int )type;  
  60.         listeners[typeInt] -= listenerDelegate;  
  61.     }  
  62.   
  63.   
  64.     public void postNotification( Notification note )  
  65.     {  
  66.         int typeInt = ( int )note.type;  
  67.   
  68.         if( listeners[typeInt] != null )  
  69.             listeners[typeInt](note);  
  70.     }  
  71.       
  72.   
  73. }  
  74.   
  75.   
  76.   
  77.   
  78. // Usage:  
  79. // NotificationCenter.defaultCenter.addListener( onNotification );  
  80. // NotificationCenter.defaultCenter.sendNotification( new Notification( NotificationTypes.OnStuff, this ) );  
  81. // NotificationCenter.defaultCenter.removeListener( onNotification, NotificationType.OnStuff );  


如上是NotificationCenter,它的工作呢就是:

 

1.1、创建多个监听者,就相当于有多个类型的监听器。

[csharp] view plaincopyprint?
 
  1. OnNotificationDelegate [] listeners  

 

1.2、每个监听者能够“发送”属于自己类型的方法即是通知。例如有一个“登录按钮”,这个按钮有一个监听者,那么我们就可以通过Notification这个中心去向这个按钮类型的监听者注册信息(或者移除监听)。

1.3、除了以上2点只是能够实现我们前面提及的需要多个Delegate(其实就是创建多个监听者类型这个数组),那么我们需要方法的传参类型也不一样呢?

 

[csharp] view plaincopyprint?
 
  1. public delegate void OnNotificationDelegate( Notification note );  

 

我们看到单个delegate的定义,传参是Notification note;这里创建了一个消息的基类;所有类型的监听者[] 接收的传参消息都要属于单个Notification。
 
[csharp] view plaincopyprint?
 
  1. // Standard notification class.  For specific needs subclass  
  2. public class Notification  
  3. {  
  4.     public NotificationType type;  
  5.     public object userInfo;  
  6.   
  7.     public Notification( NotificationType type )  
  8.     {  
  9.         this.type = type;  
  10.     }  
  11.   
  12.   
  13.     public Notification( NotificationType type, object userInfo )  
  14.     {  
  15.         this.type = type;  
  16.         this.userInfo = userInfo;  
  17.     }  
  18. }  

如上图,就是基本的通知内容体的类定义,如果要保存更复杂或者说更多类型的信息,那么我们可以继承这个消息基类。
 
[csharp] view plaincopyprint?
 
  1. public class SuperNotification : Notification  
  2. {  
  3.     public float varFloat;  
  4.     public int varInt;  
  5.   
  6.       
  7.     public SuperNotification( NotificationType type, float varFloat, int varInt ) : base( type )  
  8.     {  
  9.         this.varFloat = varFloat;  
  10.         this.varInt = varInt;  
  11.     }  
  12. }  

如上图,就是继承了基类Notification实现更多消息载体定义。可以强制转换类型传给要调用的委托者,当该委托类型的方法接受到这个基类可以重新转化成它原来的类。
 
1.3.1、思考:如果使用Hashtable传参呢?效率会不会快一点,而且我们不需要太多的创建类。

1.3.2、思考:因为NotificationCenter中初始化这些监听者数量就直接new数组,那么假如我定义了很多这些监听者类型。是否没有使用到的监听者就不需要创建了呢?把数组改成List动态增加,效率是否会提高?

[csharp] view plaincopyprint?
 
  1. private OnNotificationDelegate [] listeners = new OnNotificationDelegate[(int)NotificationType.TotalNotifications];  
1.3.3、当切换场景时候一定要取消已经注册的监听者信息。
 

 

二、结合Notification的统一管理、范型传参定义监听方法的传参数量分类、区分不同监听者使用字符串名称;参考:http://wiki.unity3d.com/index.php/Advanced_CSharp_Messenger 

这个结构制作的Message系统我觉得比前面的更加强大,而且不需要创建更多的消息内容体类,因为采用固定数量的范型传参,一般来说传参3个基本上就很多了足够了,甚至可以传参就是前面我们定义的Notification类,这就是范型的强大。

这里就不再阐述了,记住和前面的区别:

1、监听者区分不用enum类型,而直接使用string名称定义,通过键值对应储存;

2、不用担心监听的方法传参问题,只需要区分是多少个参数来区分;

3、要支持自动切换场景时候进行这些监听者的消除;

 

2.1、思考 :如果调用时候直接用string命名来区分不同的监听者,那么这样很容易造成混乱,而且你代码多了就不知道你前面定义的这个监听者是什么了。所以可以统一固定一个enum区分或者监听者身份类。

 

 

三、除了以上2个实现新的消息系统外,还有一种学习QT的消息槽结构来实现,参考:http://www.cocoachina.com/bbs/read.php?tid=68048&page=1 当然本人不熟悉QT的机制,稍微看了后,作为一个区分来研究下。其实这种方式去实现delegate只是一种习惯了,还不能完全是QT的消息槽机制。

 

[csharp] view plaincopyprint?
 
  1. using UnityEngine;  
  2. using System.Collections;  
  3.   
  4. public class ZObject : MonoBehaviour {  
  5.        
  6.     public delegate void SIGNAL(Hashtable args);  
  7.       
  8.     public virtual void CONNECT(ref SIGNAL signal, SIGNAL slot){  
  9.         signal += slot;  
  10.     }  
  11.       
  12.     public virtual void DISCONNECT(ref SIGNAL signal, SIGNAL slot){  
  13.         signal -= slot;  
  14.     }  
  15.       
  16.     public void EMIT(SIGNAL signal, Hashtable args){  
  17.         if(signal != null)  
  18.             signal(args);  
  19.     }  
  20. }  

以上首先声明一个消息槽机制的基础对象,它必须可以CONNECT和DISCONNECT、EMIT(发射信号);想象一下,有个信号源向天空发射了一枚烟花信号是救命的,然后很多个人(就是槽)就会接受到这个信号的内容并且调用。

 

我们定义一个信号发射体,例如一个按钮:

 

[csharp] view plaincopyprint?
 
  1. public class ClassA : ZObject {  
  2.       
  3.     public SIGNAL mouseClickSignal;  
  4.       
  5.     void OnMouseDown(){  
  6.         EMIT(mouseClickSignal, new Hashtable(){  
  7.             {"sender",      gameObject},  
  8.             {"position",    Vector3.one},  
  9.             {"string",      "hello world"},  
  10.             {"time",        Time.time}  
  11.         });   
  12.     }  
  13. }  


我们看到,这个按钮有一个点击的信号,当点击时候发射这个信号。

 

疑问:那么谁要知道它的信号呢?就是-槽了。对,接下来只要把这个信号把需要获取这个信号的槽连接起来,形成信号槽系统即可!

 

[csharp] view plaincopyprint?
 
  1. public class ControllerB : ZObject {  
  2.         
  3.     public ClassA objectA;  
  4.     public ClassA objectB;  
  5.     void Awake () {  
  6.           
  7.         CONNECT(ref objectA.mouseClickSignal, //sender's signal  
  8.                 SLOT_MouseClicked   //receiver's slot method  
  9.                 );   
  10.           
  11.         CONNECT(ref objectB.mouseClickSignal, //sender's signal  
  12.                 SLOT_MouseClicked   //receiver's slot method  
  13.                 );   
  14.           
  15.     }  
  16.       
  17.     //SLOT method   
  18.     //A slot is a function that is called in response to a particular signal  
  19.     void SLOT_MouseClicked(Hashtable args){  
  20.           
  21.         GameObject sender = (GameObject)args["sender"];  
  22.         string str = args["string"].ToString();  
  23.         float _time = (float)args["time"];  
  24.           
  25.         Debug.Log("RECEIVE SIGNAL : "+sender.name + " Say:"+str +" at:"+_time + " , call SLOT_MouseClicked");  
  26.     }  
  27.       
  28. }  


如上,我们看到这个槽方法是:

 

 

[csharp] view plaincopyprint?
 
  1. void SLOT_MouseClicked(Hashtable args){  

我们甚至还可以新建很多个其他槽方法,来连接这个信号即可。

 

 

3.1、思考,这个QT信号槽机制还是很不错的,很好理解。但是呢,如果切换场景的时候,要把这些信号给DISCONNECT掉就比较麻烦了。因为它们的信号有可能是分散到各地,唯一要注意就是你在哪里CONNECT了信号,就应该在这个COMPONENT里面DISCONNECT掉它。

 

四、总结:

通过以上3点的C#支持的delegate消息系统强化后,能替代Unity3d的SendMessage这种消耗巨大的方式。

 

五、补充:

待续

posted @ 2014-06-14 11:56  TouchAfflatus  阅读(373)  评论(0编辑  收藏  举报