Unity3D - 關於Delegate - SignalSlot信息槽的使用和SendMessage取替

我們知道Unity3D自身有SendMessage向對象之間發送消息,但這個消耗是比較大的,因爲它很大程度上涉及了Reflection發射機制。

如何變更思路,結合C#自帶的消息系統delegate委託事件,對此進行優化:

我們看以下一個簡單的delegate使用:

public class DelegateBasic : MonoBehaviour {
	
	//define my delegate statement.
	public delegate void MyDelegate(string arg1);
	
	//create my delegate object
	public MyDelegate myDelegate;
	
	// Use this for initialization
	void Start () {
		myDelegate += myFunciton1;
		myDelegate += myFunciton2;
	}
	
	// Update is called once per frame
	void Update () {
	
	}
	
	
	void OnGUI()
	{
		if(GUILayout.Button("INVOKE"))
		{
			myDelegate("Invoke....");
		}
		
	}
	
	void myFunciton1(string s)
	{
		Debug.Log("myFunciton1 " + s);
	}
	
	void myFunciton2(string s)
	{
		Debug.Log("myFunciton2 " + s);
	}
	
	
}

以上只是實現myDelegate一旦調用,那麼連接了該委託的事件的方法都會被調用。

但是以上我們看到,這只是1個delegate類型對應 —— 多個與該委託定義形式一致的方法;假如我需要不同的delegate呢?而且這些方法形式定義不一樣,也或者說我們需要方法傳參不一樣呢?


一、Notification通知中心結構:參考http://wiki.unity3d.com/index.php/Category:Messaging 這裏有很多個寫好的例子可以使用。

using UnityEngine;
using System.Collections;


// Each notification type should gets its own enum
public enum NotificationType {
	OnStuff,
	OnOtherStuff,
	OnSomeEvent,
	TotalNotifications
};

public delegate void OnNotificationDelegate( Notification note );

public class NotificationCenter
{
    private static NotificationCenter instance;

    private OnNotificationDelegate [] listeners = new OnNotificationDelegate[(int)NotificationType.TotalNotifications];

    // Instead of constructor we can use void Awake() to setup the instance if we sublcass MonoBehavoiur
    public NotificationCenter()
    {
        if( instance != null )
        {
            Debug.Log( "NotificationCenter instance is not null" );
            return;
        }
        instance = this;
    }
	
	
	~NotificationCenter()
	{
		instance = null;
	}


    public static NotificationCenter defaultCenter
    {
        get
        {
            if( instance == null )
                new NotificationCenter();
            return instance;
        }
    }


    public void addListener( OnNotificationDelegate newListenerDelegate, NotificationType type )
    {
        int typeInt = (int)type;
        listeners[typeInt] += newListenerDelegate;
    }


    public void removeListener( OnNotificationDelegate listenerDelegate, NotificationType type )
    {
        int typeInt = ( int )type;
        listeners[typeInt] -= listenerDelegate;
    }


    public void postNotification( Notification note )
    {
        int typeInt = ( int )note.type;

        if( listeners[typeInt] != null )
            listeners[typeInt](note);
    }
    

}




// Usage:
// NotificationCenter.defaultCenter.addListener( onNotification );
// NotificationCenter.defaultCenter.sendNotification( new Notification( NotificationTypes.OnStuff, this ) );
// NotificationCenter.defaultCenter.removeListener( onNotification, NotificationType.OnStuff );

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

1.1、創建多個監聽者,就相當於有多個類型的監聽器。

OnNotificationDelegate [] listeners

1.2、每個監聽者能夠“發送”屬於自己類型的方法即是通知。例如有一個“登錄按鈕”,這個按鈕有一個監聽者,那麼我們就可以通過Notification這個中心去向這個按鈕類型的監聽者註冊信息(或者移除監聽)。

1.3、除了以上2點只是能夠實現我們前面提及的需要多個Delegate(其實就是創建多個監聽者類型這個數組),那麼我們需要方法的傳參類型也不一樣呢?

public delegate void OnNotificationDelegate( Notification note );
我們看到單個delegate的定義,傳參是Notification note;這裏創建了一個消息的基類;所有類型的監聽者[] 接收的傳參消息都要屬於單個Notification。

// Standard notification class.  For specific needs subclass
public class Notification
{
    public NotificationType type;
    public object userInfo;

    public Notification( NotificationType type )
    {
        this.type = type;
    }


    public Notification( NotificationType type, object userInfo )
    {
        this.type = type;
        this.userInfo = userInfo;
    }
}

如上圖,就是基本的通知內容體的類定義,如果要保存更復雜或者說更多類型的信息,那麼我們可以繼承這個消息基類。

public class SuperNotification : Notification
{
	public float varFloat;
	public int varInt;

	
	public SuperNotification( NotificationType type, float varFloat, int varInt ) : base( type )
	{
		this.varFloat = varFloat;
		this.varInt = varInt;
	}
}

如上圖,就是繼承了基類Notification實現更多消息載體定義。可以強制轉換類型傳給要調用的委託者,當該委託類型的方法接受到這個基類可以重新轉化成它原來的類。

1.3.1、思考:如果使用Hashtable傳參呢?效率會不會快一點,而且我們不需要太多的創建類。
1.3.2、思考:因爲NotificationCenter中初始化這些監聽者數量就直接new數組,那麼假如我定義了很多這些監聽者類型。是否沒有使用到的監聽者就不需要創建了呢?把數組改成List動態增加,效率是否會提高?
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的消息槽機制。

using UnityEngine;
using System.Collections;

public class ZObject : MonoBehaviour {
	 
	public delegate void SIGNAL(Hashtable args);
	
	public virtual void CONNECT(ref SIGNAL signal, SIGNAL slot){
		signal += slot;
	}
	
	public virtual void DISCONNECT(ref SIGNAL signal, SIGNAL slot){
		signal -= slot;
	}
	
	public void EMIT(SIGNAL signal, Hashtable args){
		if(signal != null)
			signal(args);
	}
}
以上首先聲明一個消息槽機制的基礎對象,它必須可以CONNECT和DISCONNECT、EMIT(發射信號);想象一下,有個信號源向天空發射了一枚煙花信號是救命的,然後很多個人(就是槽)就會接受到這個信號的內容並且調用。

我們定義一個信號發射體,例如一個按鈕:

public class ClassA : ZObject {
	
	public SIGNAL mouseClickSignal;
	
	void OnMouseDown(){
	    EMIT(mouseClickSignal, new Hashtable(){
			{"sender",		gameObject},
			{"position",	Vector3.one},
			{"string", 		"hello world"},
			{"time", 		Time.time}
		});	
	}
}

我們看到,這個按鈕有一個點擊的信號,當點擊時候發射這個信號。

疑問:那麼誰要知道它的信號呢?就是-槽了。對,接下來只要把這個信號把需要獲取這個信號的槽連接起來,形成信號槽系統即可!

public class ControllerB : ZObject {
	  
	public ClassA objectA;
	public ClassA objectB;
	void Awake () {
		
		CONNECT(ref objectA.mouseClickSignal, //sender's signal
		        SLOT_MouseClicked	//receiver's slot method
		        ); 
		
		CONNECT(ref objectB.mouseClickSignal, //sender's signal
		        SLOT_MouseClicked	//receiver's slot method
		        ); 
		
	}
	
	//SLOT method 
	//A slot is a function that is called in response to a particular signal
	void SLOT_MouseClicked(Hashtable args){
		
		GameObject sender = (GameObject)args["sender"];
		string str = args["string"].ToString();
		float _time = (float)args["time"];
		
		Debug.Log("RECEIVE SIGNAL : "+sender.name + " Say:"+str +" at:"+_time + " , call SLOT_MouseClicked");
	}
	
}

如上,我們看到這個槽方法是:

void SLOT_MouseClicked(Hashtable args){
我們甚至還可以新建很多個其他槽方法,來連接這個信號即可。


3.1、思考,這個QT信號槽機制還是很不錯的,很好理解。但是呢,如果切換場景的時候,要把這些信號給DISCONNECT掉就比較麻煩了。因爲它們的信號有可能是分散到各地,唯一要注意就是你在哪裏CONNECT了信號,就應該在這個COMPONENT裏面DISCONNECT掉它。


四、總結:

通過以上3點的C#支持的delegate消息系統強化後,能替代Unity3d的SendMessage這種消耗巨大的方式。


五、補充:

待續




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章