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

    运行结果如下图:
    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

    更多内容

  • 相关阅读:
    一、left
    padding溢出
    一、
    Python创建、删除桌面、启动组快捷方式的例子分享
    openstack常见问题解决方法总结
    __attribute__ 详解
    __ATTRIBUTE__ 知多少?
    CentOS如何设置终端显示字符界面区域的大小
    shell使用技巧
    openstack 安全策略权限控制等api接口
  • 原文地址:https://www.cnblogs.com/liangxiegame/p/12973186.html
Copyright © 2020-2023  润新知