Unity 遊戲框架搭建 2019 (四十八/四十九) MonoBehaviourSimplify 中的消息策略完善&關於發送事件的簡單封裝
MonoBehaviourSimplify 中的消息策略完善
在上一篇,筆者說,MonoBehaviourSimplify 中的消息策略還有一些小問題。我們在這篇試着解決一下。
先貼出來代碼:
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()
{
}
}
}
我們是使用字典進行註冊消息的記錄的,使用字典就要保證字典中的 key 是唯一的。而我們很可能在一個腳本中對一個關鍵字註冊多次,這樣用字典這個數據結構就顯得不合理了。
相比字典,List 更合適,因為我們有有可能有重複的內容,而字典更適合做一些查詢工作,但是 List 並不支持鍵值對,怎麼辦呢?
我們只好創建一個結構來存儲我們的消息名和對應的委託,這個結構是一個類叫做 MsgRecord
消息策略部分的代碼如下:
public abstract partial class MonoBehaviourSimplify
{
List<MsgRecord> mMsgRecorder = new List<MsgRecord>();
private class MsgRecord
{
public string Name;
public Action<object> OnMsgReceived;
}
protected void RegisterMsg(string msgName, Action<object> onMsgReceived)
{
MsgDispatcher.Register(msgName, onMsgReceived);
mMsgRecorder.Add(new MsgRecord
{
Name = msgName,
OnMsgReceived = onMsgReceived
});
}
private void OnDestroy()
{
OnBeforeDestroy();
foreach (var msgRecord in mMsgRecorder)
{
MsgDispatcher.UnRegister(msgRecord.Name,msgRecord.OnMsgReceived);
}
mMsgRecorder.Clear();
}
protected abstract void OnBeforeDestroy();
}
代碼比較簡單。
而我們的示例代碼,如下,增加了一行重複註冊的代碼。
public class B : MonoBehaviourSimplify
{
private void Awake()
{
RegisterMsg("Do", DoSomething);
RegisterMsg("Do", DoSomething);
RegisterMsg("DO1", _ => { });
RegisterMsg("DO2", _ => { });
RegisterMsg("DO3", _ => { });
}
void DoSomething(object data)
{
// do something
}
protected override void OnBeforeDestroy()
{
}
}
而我們的 MonoBehaviourSimplify 內部實現發生了天翻地覆的變化,也沒有對我們的示例代碼產生一點影響,這叫封裝。
那麼到這裡,我們的消息策略還有問題嗎?
還有的,問題在創建 MsgRecord 的部分。
如下:
mMsgRecorder.Add(new MsgRecord
{
Name = msgName,
OnMsgReceived = onMsgReceived
});
我們每次註冊消息,都要 new 一個 MsgRecord 對象出來,而我們在註銷的時候,對這個對象是什麼都沒有做的,註銷的代碼如下:
foreach (var msgRecord in mMsgRecorder)
{
MsgDispatcher.UnRegister(msgRecord.Name,msgRecord.OnMsgReceived);
}
這樣會造成一個性能問題,這個性能問題主要是有 new 時候尋址造成的,具體原因自行搜索,當然在本專欄的後邊還是會介紹的。我們要做的,就是減少 new 的發生次數,要想減少,就得讓我們的 MsgRecord 能夠回收利用。
如何回收利用呢,答案是維護一個容器,比如 List 或者 Queue、Stack 等,也就是傳說中的對象池。由於我們的 MsgRecord 的作用僅僅是作為一個存儲結構而已,而存儲的順序也不是很重要,所以我們就用做簡單的 Stack 結構,也就是棧,來作為 MsgRecord 對象池的容器。
其實現如下:
private class MsgRecord
{
static Stack<MsgRecord> mMsgRecordPool = new Stack<MsgRecord>();
public static MsgRecord Allocate()
{
if (mMsgRecordPool.Count > 0)
{
return mMsgRecordPool.Pop();
}
return new MsgRecord();
}
public void Recycle()
{
Name = null;
OnMsgReceived = null;
mMsgRecordPool.Push(this);
}
public string Name;
public Action<object> OnMsgReceived;
}
由於這個對象池只給 MsgRecord 用,所以就在 MsgRecord 內部實現了。
Allocate 是申請,也就是獲取對象。Recycle 就是回收,當不用的時候調用一下就好了。
原理很簡單。而 mMsgRecordPool 之所以設置成了 private 訪問權限,是因為,不希望被外部訪問到。對於一個類的設計來講,MsgRecord 是一個非常合格的類了。
應用到我們的消息策略的代碼如下:
protected void RegisterMsg(string msgName, Action<object> onMsgReceived)
{
MsgDispatcher.Register(msgName, onMsgReceived);
//
var msgRecord = MsgRecord.Allocate();
msgRecord.Name = msgName;
msgRecord.OnMsgReceived = onMsgReceived;
mMsgRecorder.Add(msgRecord);
}
private void OnDestroy()
{
OnBeforeDestroy();
foreach (var msgRecord in mMsgRecorder)
{
MsgDispatcher.UnRegister(msgRecord.Name,msgRecord.OnMsgReceived);
//
msgRecord.Recycle();
}
mMsgRecorder.Clear();
}
我們發現,在申請對象部分可以簡化成如下:
// var msgRecord = MsgRecord.Allocate();
//
// msgRecord.Name = msgName;
// msgRecord.OnMsgReceived = onMsgReceived;
//
// mMsgRecorder.Add(msgRecord);
mMsgRecorder.Add(MsgRecord.Allocate(msgName, onMsgReceived));
只需要向 MsgRecord.Allocate 增加參數,代碼如下:
public static MsgRecord Allocate(string msgName,Action<object> onMsgReceived)
{
MsgRecord retMsgRecord = null;
retMsgRecord = mMsgRecordPool.Count > 0 ? mMsgRecordPool.Pop() : new MsgRecord();
retMsgRecord.Name = msgName;
retMsgRecord.OnMsgReceived = onMsgReceived;
return retMsgRecord;
}
代碼不難,那麼到這裡,我們的完整的第十三個示例就寫完了。
完整示例代碼如下:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace QFramework
{
public abstract partial class MonoBehaviourSimplify
{
List<MsgRecord> mMsgRecorder = new List<MsgRecord>();
private class MsgRecord
{
private static readonly Stack<MsgRecord> mMsgRecordPool = new Stack<MsgRecord>();
public static MsgRecord Allocate(string msgName,Action<object> onMsgReceived)
{
MsgRecord retMsgRecord = null;
retMsgRecord = mMsgRecordPool.Count > 0 ? mMsgRecordPool.Pop() : new MsgRecord();
retMsgRecord.Name = msgName;
retMsgRecord.OnMsgReceived = onMsgReceived;
return retMsgRecord;
}
public void Recycle()
{
Name = null;
OnMsgReceived = null;
mMsgRecordPool.Push(this);
}
public string Name;
public Action<object> OnMsgReceived;
}
protected void RegisterMsg(string msgName, Action<object> onMsgReceived)
{
MsgDispatcher.Register(msgName, onMsgReceived);
mMsgRecorder.Add(MsgRecord.Allocate(msgName, onMsgReceived));
}
private void OnDestroy()
{
OnBeforeDestroy();
foreach (var msgRecord in mMsgRecorder)
{
MsgDispatcher.UnRegister(msgRecord.Name,msgRecord.OnMsgReceived);
msgRecord.Recycle();
}
mMsgRecorder.Clear();
}
protected abstract void OnBeforeDestroy();
}
public class MsgDistapcherInMonoBehaviourSimplify : MonoBehaviourSimplify
{
#if UNITY_EDITOR
[UnityEditor.MenuItem("QFramework/13.消息機制集成到 MonoBehaviourSimplify", false, 14)]
private static void MenuClicked()
{
UnityEditor.EditorApplication.isPlaying = true;
new GameObject("MsgReceiverObj")
.AddComponent<MsgDistapcherInMonoBehaviourSimplify>();
}
#endif
private void Awake()
{
RegisterMsg("Do", DoSomething);
RegisterMsg("Do", DoSomething);
RegisterMsg("DO1", _ => { });
RegisterMsg("DO2", _ => { });
RegisterMsg("DO3", _ => { });
}
private IEnumerator Start()
{
MsgDispatcher.Send("Do","hello");
yield return new WaitForSeconds(1.0f);
MsgDispatcher.Send("Do","hello1");
}
void DoSomething(object data)
{
// do something
Debug.LogFormat("Received Do msg:{0}",data);
}
protected override void OnBeforeDestroy()
{
}
}
}
運行結果如下圖:
菜單欄如下圖:
目錄如下圖:
到這裡我們可以進行一次導出了。
關於發送事件的簡單封裝
在上一篇,我們在 MonoBehaviourSimplify 中集成了消息功能。而在做消息功能的過程中,又接觸了對象池實現了一個非常簡單版本。
今天呢我們在接着學習。
我們先回顧下 MonoBehaviourSimplify 中關於消息功能的使用方法。
註冊消息,直接用 RegisterMsg,而註銷則在 OnDestroy 的時候統一進行註銷。
那麼單獨註銷時候怎麼辦呢?這是第一個問題。
第二個問題是,發送消息,我們使用的是 MsgDispatcher.Send 這個方法。
和我們的註冊消息的方法不是統一的。這是第二個問題。
第一個問題
第一個問題解決很簡單,只要增加針對一個消息註銷的方法就好了。
代碼如下:
public partial class MonoBehaviourSimplify
{
protected void UnRegisterMsg(string msgName)
{
var selectedRecords = mMsgRecorder.FindAll(recorder => recorder.Name == msgName);
selectedRecords.ForEach(selectRecord =>
{
MsgDispatcher.UnRegister(selectRecord.Name, selectRecord.OnMsgReceived);
mMsgRecorder.Remove(selectRecord);
selectRecord.Recycle();
});
selectedRecords.Clear();
}
protected void UnRegisterMsg(string msgName, Action<object> onMsgReceived)
{
var selectedRecords = mMsgRecorder.FindAll(recorder => recorder.Name == msgName && recorder.OnMsgReceived == onMsgReceived);
selectedRecords.ForEach(selectRecord =>
{
MsgDispatcher.UnRegister(selectRecord.Name, selectRecord.OnMsgReceived);
mMsgRecorder.Remove(selectRecord);
selectRecord.Recycle();
});
selectedRecords.Clear();
}
}
FindAll 是一個查詢方法,在 mMsgRecorder 內查詢出所有符合條件的項。代碼沒有太大的難度。
不過在使用上要注意一下,如果是要重複註冊並且需要註銷的消息,最好是用成員方法來接收,而不是用委託接收,原因是如果是單獨註銷這類消息的時候,最好是用上邊代碼的第二種註銷方法,用第一種的話,可能把當前腳本之前註冊的同名消息都會註銷掉。不過這是極少數的情況,一般筆者些項目根本用不到單獨註銷,而是全部交給了 OnDestroy 處理。
這樣第一個問題算是解決了
接下來是我們第二個問題。
第二個問題:
第二個問題是 API 不統一的問題。這個問題要解決起來很簡單。只要實現一個 Send 方法就好了,而 Send 中主要邏輯有 MsgDispatcher.Send 完成。
代碼如下:
protected void SendMsg(string msgName, object data)
{
MsgDispatcher.Send(msgName, data);
}
到此呢,我們的 API 就統一了。而第十四個示例也就算 OK 了。
全部代碼如下:
using System;
using UnityEngine;
namespace QFramework
{
public partial class MonoBehaviourSimplify
{
protected void UnRegisterMsg(string msgName)
{
var selectedRecords = mMsgRecorder.FindAll(recorder => recorder.Name == msgName);
selectedRecords.ForEach(selectRecord =>
{
MsgDispatcher.UnRegister(selectRecord.Name, selectRecord.OnMsgReceived);
mMsgRecorder.Remove(selectRecord);
});
selectedRecords.Clear();
}
protected void UnRegisterMsg(string msgName, Action<object> onMsgReceived)
{
var selectedRecords = mMsgRecorder.FindAll(recorder =>
recorder.Name == msgName && recorder.OnMsgReceived == onMsgReceived);
selectedRecords.ForEach(selectRecord =>
{
MsgDispatcher.UnRegister(selectRecord.Name, selectRecord.OnMsgReceived);
mMsgRecorder.Remove(selectRecord);
});
selectedRecords.Clear();
}
protected void SendMsg(string msgName, object data)
{
MsgDispatcher.Send(msgName, data);
}
}
public class UnifyAPIStyle : MonoBehaviourSimplify
{
#if UNITY_EDITOR
[UnityEditor.MenuItem("QFramework/14.統一 API 風格", false, 14)]
private static void MenuClicked()
{
UnityEditor.EditorApplication.isPlaying = true;
new GameObject("MsgReceiverObj")
.AddComponent<UnifyAPIStyle>();
}
#endif
private void Awake()
{
RegisterMsg("OK", data =>
{
Debug.Log(data);
UnRegisterMsg("OK");
});
}
private void Start()
{
SendMsg("OK","hello");
SendMsg("OK","hello");
}
protected override void OnBeforeDestroy()
{
}
}
}
示例代碼很簡單,執行的結果如下圖所示:
菜單欄如下圖:
目錄如下圖:
這樣我們的第十四個示例就完成了,可以進行一次導出了。
今天的內容就這些,我們下一篇再見,拜拜~
轉載請註明地址:涼鞋的筆記:liangxiegame.com
更多內容
-
QFramework 地址://github.com/liangxiegame/QFramework
-
QQ 交流群:623597263
-
Unity 進階小班:
- 主要訓練內容:
- 框架搭建訓練(第一年)
- 跟着案例學 Shader(第一年)
- 副業的孵化(第二年、第三年)
- 權益、授課形式等具體詳情請查看《小班產品手冊》://liangxiegame.com/master/intro
- 主要訓練內容:
-
關注公眾號:liangxiegame 獲取第一時間更新通知及更多的免費內容。