Unity 遊戲框架搭建 2019 (四十六) 簡易消息機制 & 集成到 MonoBehaviourSimplify 里
在上一篇,我們接觸了單例,使用單例解決了我們腳本之間訪問的問題。
腳本之間訪問其實有更好的方式。
我們先分下腳本訪問腳本的幾種形式。
第一種,A GameObject 是 B GameObject 的 Parent,或者是中間隔著幾個層級的 Parent。
那這種情況下,如果 A 腳本想調用 B 腳本的方法,直接通過 transform.Find(「XXX/YYY/ZZZ」).GetComponent<B>().DoSomething() 就可以了。
但是如果是 B 腳本想調用 A 腳本的方法,比較好的方式呢,是在 B 腳本中聲明委託,然後在 A 中註冊特定方法。當 B 想調用 A 腳本的方法的時候,通過委託通知就好。
除了使用委託,也可以使用消息機制,Unity 本身實現了一套消息機制,比如在 B 腳本中可以使用, this.SendMessageUpward(「MethedName」) 這樣的方式。不過這種方式由於是使用字元串,並且可能用到了反射,所以網上大部分部落格都不太推薦使用,但是也算是個不錯的方式。
第二種情況呢是,A GameObject 和 B GameObject 是同級的,比如他們有共同的 Parent。這種情況下,筆者還是推薦用消息機制,不過不是 Unity 自帶的消息機制,而是自己實現的消息機制。
第三種情況是,A GameObject 和 B GameObject 不在同一個 GameObject 樹下。那麼這種情況很可能就是跨模組通訊了,這種情況下,還是推薦用消息機制。
所以,我們可以試試使用消息機制來解決我們的問題。
可是我們目前手裡沒有消息機制…
那就造一個吧。
消息機制要用到的知識:
- List 或 LinkedList 或者自己實現的鏈表。
- Dictionary
- 委託
關於第一條,我們選擇 List 就好了,不過為了有更高的效率,我們最後會升級成鏈表。第三條,我們選擇 Action,因為這是我們接觸過的,以後也是用的比較多的。
而一般的消息機制會提供三個 API。
- 註冊事件
- 註銷事件
- 發送事件
我們先試著設計一下,假如我們想這樣使用我們的 API
MsgDispatcher.Register("消息名",(obj)=>{ /* 處理消息 */ });
MsgDispatcher.Send("消息名","消息內容");
MsgDispatcher.UnRegister("消息名");
首先事件名,是一個字元串類型的,而事件名要對應一個委託。我們聲明一個靜態的字典變數就好了。
private static Dictionary<string, Action<object>> RegisteredMsgs = new Dictionary<string, Action<object>>();
為什麼是靜態的呢?因為,我們的消息機制呢不需要創建實例,而消息是要在整個項目內之間通訊的,也就是全局的消息。全局的消息就需要放在唯一容器里註冊。而這個容器就是我們的這個字典變數。
我們先實現註冊事件功能。
public static void Register(string msgName, Action<object> onMsgReceived)
{
RegisteredMsgs.Add(msgName, onMsgReceived);
}
非常簡單。
我們再實現註銷功能。
public static void UnRegister(string msgName)
{
RegisteredMsgs.Remove(msgName);
}
也非常簡單。
再實現發送功能。
public static void Send(string msgName, object data)
{
RegisteredMsgs[msgName](data);
}
非常簡單。
第十二個示例程式碼如下:
using System;
using System.Collections.Generic;
using UnityEngine;
namespace QFramework
{
public class MsgDispatcher
{
private static Dictionary<string, Action<object>> RegisteredMsgs = new Dictionary<string, Action<object>>();
public static void Register(string msgName, Action<object> onMsgReceived)
{
RegisteredMsgs.Add(msgName, onMsgReceived);
}
public static void UnRegister(string msgName)
{
RegisteredMsgs.Remove(msgName);
}
public static void Send(string msgName, object data)
{
RegisteredMsgs[msgName](data);
}
#if UNITY_EDITOR
[UnityEditor.MenuItem("QFramework/12.簡易消息機制", false, 13)]
#endif
private static void MenuClicked()
{
Register("消息1", data => { Debug.LogFormat("消息1:{0}", data); });
Send("消息1", "hello world");
UnRegister("消息1");
Send("消息1", "hello");
}
}
}
菜單執行結果如下
哈哈哈,報錯啦,不過我們發現,第一次消息發送成功了,但是第二次發送的時候報錯了。是因為消息進行註銷了,也就是字典里沒有消息名了,這時候直接從字典里取值當然會報錯。
這個問題我們留在下一篇解決,在下一篇,我們要講解關於這個消息機制的完善。
第十二個示例還沒有完成。
集成到 MonoBehaviourSimplify 里。
還記得我們的簡易消息機制是為了解決什麼問題誕生的嘛?
是為了解決腳本間訪問的問題。
我們回過頭再看下 A 腳本如果想訪問 B 腳本,使用消息機制,如何實現。
程式碼如下:
public class A : MonoBehaviour
{
void Update()
{
if(Input.GetMouseButtonDown(0))
{
MsgDispatcher.Send("DO","ok");
}
}
}
public class B : MonoBehaviour
{
void Awake()
{
MsgDispatcher.Register("DO",DoSomething);
}
void DoSomething(object data)
{
// do something
}
void OnDestroy()
{
MsgDispatcher.UnRegiter("DO",DoSomething);
}
}
用法還是很簡單的。
不過假如我們的 B 腳本註冊了非常多的消息,程式碼會變成如下:
public class B : MonoBehaviour
{
void Awake()
{
MsgDispatcher.Register("DO",DoSomething);
MsgDispatcher.Register("DO1",MsgReceiver);
MsgDispatcher.Register("DO2",MsgReceiver1);
MsgDispatcher.Register("DO3",MsgReceiver2);
}
void DoSomething(object data)
{
// do something
}
...
void OnDestroy()
{
MsgDispatcher.UnRegiter("DO",DoSomething);
MsgDispatcher.UnRegiter("DO1",MsgReceiver);
MsgDispatcher.UnRegiter("DO2",MsgReceiver1);
MsgDispatcher.UnRegiter("DO3",MsgReceiver2);
}
}
每次註冊一個消息,對應地,在 OnDestroy 操作的時候就要註銷一個事件。這個非常像我們寫 C++ 的時候遵循的一個記憶體管理法則,每次申請記憶體就要在析構方法里進行釋放。
而這樣使用消息機制,初學者非常容易忘記消息的註銷,從而導致引用異常等等。
那麼如何解決呢?
用一個 Dictionary 記錄這個腳本中已經註冊過的消息,以及消息名對應的回調。
程式碼如下:
public class B : MonoBehaviour
{
Dictionary<string,Action<object>> mMsgRegisterRecorder = new Dictionary<string,Action<object>>();
void Awake()
{
MsgDispatcher.Register("DO",DoSomething);
mMsgRegisterRecorder.Add("DO",DoSomething);
MsgDispatcher.Register("DO1",MsgReceiver);
mMsgRegisterRecorder.Add("DO1",MsgReceiver);
MsgDispatcher.Register("DO2",MsgReceiver1);
mMsgRegisterRecorder.Add("DO2",MsgReceiver1);
MsgDispatcher.Register("DO3",MsgReceiver2);
mMsgRegisterRecorder.Add("DO3",MsgReceiver2);
}
void DoSomething(object data)
{
// do something
}
...
void OnDestroy()
{
foreach (var keyValuePair in mMsgRegisterRecorder)
{
MsgDispatcher.UnRegister(keyValuePair.Key,keyValuePair.Value);
}
mMsgRegisterRecorder.Clear();
}
}
這樣,不管註冊了多少個消息,只要在 OnDestroy 的時候, 進行一個遍歷,這樣消息就全部註銷掉了。
但是這樣寫的話註冊,就變得麻煩了,每次註冊要先兩行程式碼。
MsgDispatcher.Register("DO3",MsgReceiver2);
mMsgRegisterRecorder.Add("DO3",MsgReceiver2);
把兩行提取成一個方法就好了。
提取的方法,程式碼如下:
private void RegisterMsg(string msgName, Action<object> onMsgReceived)
{
MsgDispatcher.Register(msgName, onMsgReceived);
mMsgRegisterRecorder.Add(msgName, onMsgReceived);
}
而註冊消息的程式碼就會變成如下:
private void Awake()
{
RegisterMsg("Do",DoSomething);
RegisterMsg("DO1",MsgReceiver);
RegisterMsg("DO2", _=>{ });
RegisterMsg("DO3", _=>{ });
}
是不是精簡了很多,而且也可以註冊 Lambda 表達式了。
不過我們看下現在的 B 腳本全部程式碼:
public class B : MonoBehaviour
{
Dictionary<string, Action<object>> mMsgRegisterRecorder = new Dictionary<string, Action<object>>();
private void Awake()
{
RegisterMsg("Do",DoSomething);
RegisterMsg("DO1",_=>{ });
RegisterMsg("DO2", _=>{ });
RegisterMsg("DO3", _=>{ });
}
private void RegisterMsg(string msgName, Action<object> onMsgReceived)
{
MsgDispatcher.Register(msgName, onMsgReceived);
mMsgRegisterRecorder.Add(msgName, onMsgReceived);
}
void DoSomething(object data)
{
// do something
}
private void OnDestroy()
{
foreach (var keyValuePair in mMsgRegisterRecorder)
{
MsgDispatcher.UnRegister(keyValuePair.Key,keyValuePair.Value);
}
mMsgRegisterRecorder.Clear();
}
}
目前,每個要使用相同消息策略的腳本,都實現如上的程式碼,會產生很多的重複程式碼。所以這裡我們要開始考慮如何讓這個消息註冊/註銷的策略進行復用。首先用靜態方法是不可能了,因為這個策略是有狀態的(成員變數)。所以以我們目前掌握的知識來看,只能用繼承的方式了。
繼承也有兩種,一種是繼承一個新類,另一種是繼承到 MonoBehaviourSimplify 里。
筆者選擇後者,這樣我們的腳本只要繼承 MonoBehaviourSimplify 就會獲得 API 簡化和消息功能了,一舉多得,而且很方便。
集成後的程式碼,也就是第十三個示例的程式碼如下:
using System;
using System.Collections.Generic;
namespace QFramework
{
public abstract partial class MonoBehaviourSimplify
{
Dictionary<string, Action<object>> mMsgRegisterRecorder = new Dictionary<string, Action<object>>();
protected void RegisterMsg(string msgName, Action<object> onMsgReceived)
{
MsgDispatcher.Register(msgName, onMsgReceived);
mMsgRegisterRecorder.Add(msgName, onMsgReceived);
}
private void OnDestroy()
{
OnBeforeDestroy();
foreach (var keyValuePair in mMsgRegisterRecorder)
{
MsgDispatcher.UnRegister(keyValuePair.Key,keyValuePair.Value);
}
mMsgRegisterRecorder.Clear();
}
protected abstract void OnBeforeDestroy();
}
public class B : MonoBehaviourSimplify
{
private void Awake()
{
RegisterMsg("Do", DoSomething);
RegisterMsg("DO1", _ => { });
RegisterMsg("DO2", _ => { });
RegisterMsg("DO3", _ => { });
}
void DoSomething(object data)
{
// do something
}
protected override void OnBeforeDestroy()
{
}
}
}
在以上程式碼里,筆者把 MonoBehaviourSimplify 添加了 abstract 關鍵字,這樣用戶在使用 MonoBehaviourSimplify 的時候就不能自己創建出來實例了。
而又添加了如下抽象方法:
protected abstract void OnBeforeDestroy();
做這步的目的呢,是為了提醒子類不要覆寫了 OnDestroy。提醒是怎麼做到的呢。
我們通過分析可以得出,使用 MonoBehaviourSimplify 的情況有兩種。
一種是,在寫腳本之前就想好了這個腳本要繼承 MonoBehaviourSimplify,但是繼承之後,編譯會報錯,因為有一個抽象方法,必須實現,也就是 OnBeforeDestroy。那麼實現了這個,用戶就會知道設計 MonoBehaviourSimplify 的人,是推薦用 OnBeforeDestroy 來做卸載邏輯的,並不推薦用 OnDestroy。這是第一種。
第二種呢,腳本本來就有了,但是在中途想要換成繼承 MonoBehaviourSimplify,繼承了之後,同樣報錯了,報錯了之後發現 MonoBehaviourSimplify 推薦用 OnBeforeDestroy 來做卸載邏輯,這時候如果以前的腳本已經有了 OnDestroy 邏輯,用戶就會把 OnDestroy 的邏輯遷移到 OnBeforeDestroy 里。這樣也算達到了一個提醒的作用。
這就是 OnBeforeDestroy 的設計初衷,而 abstract 關鍵字,就應該這樣用。
但是到這裡呢,這套策略還是有一點小問題的。這個小問題就留在下一篇講了。
今天的內容就這些,我們下一篇再見。
轉載請註明地址:涼鞋的筆記:liangxiegame.com
更多內容
-
QFramework 地址://github.com/liangxiegame/QFramework
-
QQ 交流群:623597263
-
Unity 進階小班:
- 主要訓練內容:
- 框架搭建訓練(第一年)
- 跟著案例學 Shader(第一年)
- 副業的孵化(第二年、第三年)
- 權益、授課形式等具體詳情請查看《小班產品手冊》://liangxiegame.com/master/intro
- 主要訓練內容:
-
關注公眾號:liangxiegame 獲取第一時間更新通知及更多的免費內容。