Unity 遊戲框架搭建 2019 (四十八/四十九) MonoBehaviourSimplify 中的訊息策略完善&關於傳送事件的簡單封裝

涼鞋的筆記發表於2020-05-27

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()
        {
            
        }
    }
}

執行結果如下圖:
006tNc79gy1fzft5birfxj30w40ectar.jpg

選單欄如下圖:
006tNc79gy1fzft5ek7j9j30lk0fkamq.jpg

目錄如下圖:
006tNc79gy1fzft5i5y2ej30gm0dimz9.jpg

到這裡我們可以進行一次匯出了。

關於傳送事件的簡單封裝

在上一篇,我們在 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()
        {
            
        }
    }
}

示例程式碼很簡單,執行的結果如下圖所示:
006tNc79gy1fzft6bnqe4j30wa0a8759.jpg

選單欄如下圖:
006tNc79gy1fzft6ektrqj30ke0hiwtf.jpg

目錄如下圖:
006tNc79gy1fzft6ir1uej30ie0ekmzc.jpg

這樣我們的第十四個示例就完成了,可以進行一次匯出了。

今天的內容就這些,我們下一篇再見,拜拜~

轉載請註明地址:涼鞋的筆記:liangxiegame.com

更多內容

相關文章