2013年4月20日 星期六

跨越種族(物件)之間的呼喊 - Notification

如果曾經有寫過 Objective - C 的話,對於這個名詞應該不陌生。在 iOS App 裡,Notification 是用來協助不同介面 or 功能之間的事件傳遞,而彼此不用知道對方的存在。C# 裡雖然已經有 Event 這種方式可以使用,但使用方便即彈性仍不如 Notification。而 Unity 中的 BroardcastMessage 也有效能和無法指定某元件的問題。

Notification 的運用層面很廣,不過還是先來講解如何架構 Nofication Center

參考資料:Uniti Wiki Notification Center

和參考資料所不同的是,我並沒有使用 SendMessage 去架構 Notification Center。考量到同一物件的不同元件註冊時可能接受到兩個以上的通知和效能上的問題,我改用了 Interface 的寫法。

Notification 的原理是

物件註冊通知訊息 -> 當有其他物件發出通知訊息 -> 註冊中的物件會接受到訊息

所以只要定義好訊息的功能是什麼,就可以依照不同的訊息來做不同的事

我們先從定義通知訊息的資料開始

// 通知資料定義
public struct Notification {
public object m_sender; // 通知者
public string m_name; // 訊息名稱
public object m_data; // 資料
public Notification(object sender, string name) {
m_sender = sender;
m_name = name;
m_data = null;
}
public Notification(object sender, string name, string data) {
m_sender = sender;
m_name = name;
m_data = data;
}
}
view raw Notification.cs hosted with ❤ by GitHub


發出通知時,我們必須要有

通知者 - 該通知是誰發出的
訊息 - 通知的訊息名稱
資料 - 通知所要傳遞的資料

有了這三項,接受通知者幾乎可以決定要利用什麼資料做什麼樣的事情

接下來,就是要建立一個機制去管理通知訊息和註冊物件
首先,我們需要定義一個 Interface 去決定接受通知的物件入口

// 通知介面定義
public interface INotification {
void OnNotify(Notification notify);
}
view raw Interface.cs hosted with ❤ by GitHub


所有使用通知機制的物件,都要繼承該介面
有了接口後,我們就可以決定如何管理訊息列表和註冊物件

// 通知中心
public class NotificationCenter : MonoBehaviour {
#region Variable
private static NotificationCenter s_defaultCenter = null;
// 註冊通知的物件列表
private Dictionary<string, List<INotification>> m_notifications = new Dictionary<string, List<INotification>>();
#endregion
#region Property
// 保留一個 Singleton 的通知中心
public static NotificationCenter DefaultCenter {
get {
if( s_defaultCenter == null ) {
s_defaultCenter = GameObject.FindObjectOfType(typeof(NotificationCenter)) as NotificationCenter;
if( s_defaultCenter == null ) {
GameObject go = new GameObject("Defalt Notification Center");
s_defaultCenter = go.AddComponent<NotificationCenter>();
}
}
return s_defaultCenter;
}
}
#endregion
#region Public Function
// 新增觀察者,觀察者會接受到通知訊息
public void AddObserver(INotification ob, string name) {
if( string.IsNullOrEmpty(name) ) {
Debug.Log("Null name specified for notification in AddObserver.");
return ;
}
// 如果該訊息尚未建立列表時,建立一個空的新訊息列表
if( !m_notifications.ContainsKey(name) )
m_notifications[name] = new List<INotification>();
List<INotification> list = m_notifications[name];
// 如果該物件尚未加入註冊,則加入訊息列表
if( !list.Contains(ob) )
list.Add(ob);
}
// 移除觀察者
public void RemoveObserver(INotification ob, string name) {
// 檢查該訊息列表是否存在
if( !m_notifications.ContainsKey(name) )
return ;
List<INotification> list = m_notifications[name];
// 防錯,避免裡面存的是空物件
if( list == null )
return ;
// 如果訊息列表註冊該物件,則移除
if( list.Contains(ob) ) list.Remove(ob);
// 假設註冊已空,則移除訊息列表
if( list.Count == 0 ) m_notifications.Remove(name);
}
// 發出通知
public void PostNotification(Notification notify) {
// 通知訊息不能是空的
if( string.IsNullOrEmpty(notify.m_name) ) {
Debug.Log("Null name sent to PostNotification.");
return ;
}
// 檢查是否有該訊息列表
if( !m_notifications.ContainsKey(notify.m_name) )
return ;
List<INotification> list = m_notifications[notify.m_name];
// 防錯,避免空物件
if( list == null )
return ;
// 移除所有的空物件
list.Remove(null);
// 假設訊息列表中註冊物件已空,移除該訊息列表
if( list.Count == 0 ) {
m_notifications.Remove(notify.name);
return ;
}
// 將註冊物件暫存出來
// 為避免註冊物件收到通知同時將自己移除通知
INotification[] obs = list.ToArray();
// 通知註冊物件訊息
foreach( INotification ob in obs )
ob.OnNotify(notify);
}
public void PostNotification(object sender, string name) {
PostNotification(new Notification(sender, name));
}
public void PostNotification(object sender, string name, object data) {
PostNotification(new Notification(sender, name, data));
}
#endregion
}


程式碼看起來很長,但最主要是做四件事情

1.建立通知中心的 Singleton
2.註冊接受該訊息的物件
3.移除接受該訊息物件的註冊
4.發出通知

有這四個功能後,使用上就非常簡單

使用範例
using UnityEngine;
using System.Collections;
public class DoGameOver : MonoBehaviour, INotification {
#region Behaviour
private void Awake() {
NotificationCenter.DefaultCenter.AddObserver(this, "SaveGame Finish");
}
private void Start () {
// Hey, go save game
NotificationCenter.DefaultCenter.PostNotification(this, "SaveGame");
}
public void OnNotify(Notification notify) {
if( !this.enabled )
return ;
if( notify.m_name == "SaveGame Finish" ) {
Debug.Log("Game Exit");
}
}
#endregion
}
view raw DoGameOver.cs hosted with ❤ by GitHub

using UnityEngine;
using System.Collections;
public class DoSaveGame : MonoBehaviour, INotification {
#region Coroutine
IEnumerator UploadSaveData() {
Debug.Log("Save Now ...");
yield return new WaitForSeconds(5.0f);
Debug.Log("Save Finish!");
NotificationCenter.DefaultCenter.PostNotification(this, "SaveGame Finish");
}
#endregion
#region Behaviour
private void Awake() {
NotificationCenter.DefaultCenter.AddObserver(this, "SaveGame");
}
public void OnNotify(Notification notify) {
if( !this.enabled )
return ;
if( notify.m_name == "SaveGame" ) {
StartCoroutine("UploadSaveData");
}
}
#endregion
}
view raw DoSaveGame.cs hosted with ❤ by GitHub


把這兩個 Component 掛到物件上後

DoGameOver 會發出「儲存遊戲」通知,DoSaveGame 接受到 5 秒後發出「遊戲儲存完畢」通知,DoGameOver 接到「遊戲儲存完畢」後離開遊戲。



Notification 運用得好的話,在程式之間的合作也能獲得極大的助益

現實中的行為就會像是

A 負責寫「儲存遊戲」的功能,B 只要發出「儲存遊戲」的通知,A 接到通知後儲存完遊戲,再發出「儲存遊戲完畢」,B 等待「儲存遊戲完畢」的通知後,繼續做別的事情。程式們只要事先定好通知規則和資料即可。

3 則留言:

  1. 出現找不到型別或命名空間名稱 INotification ,請問遺漏了什麼
    謝謝

    回覆刪除
    回覆
    1. 你應該是少了 INotification 的定義宣告,上面有個「通知介面定義」短短三行,放在 NotificationCenter 定義宣告的上面就可以了

      刪除