zoukankan      html  css  js  c++  java
  • 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

    更多内容

  • 相关阅读:
    Atitit.播放系统规划新版本 v4 q18 and 最近版本回顾
    Atitit.播放系统规划新版本 v4 q18 and 最近版本回顾
    atitit.极光消息推送服务器端开发实现推送  jpush v3. 总结o7p
    atitit.极光消息推送服务器端开发实现推送  jpush v3. 总结o7p
    Atitit.文件搜索工具 attilax 总结
    Atitit.文件搜索工具 attilax 总结
    Atitit.软件命名空间  包的命名统计 及命名表(2000个名称) 方案java package
    Atitit.软件命名空间  包的命名统计 及命名表(2000个名称) 方案java package
    Atitit..状态机与词法分析  通用分词器 分词引擎的设计与实现 attilax总结
    Atitit..状态机与词法分析  通用分词器 分词引擎的设计与实现 attilax总结
  • 原文地址:https://www.cnblogs.com/liangxiegame/p/12973186.html
Copyright © 2011-2022 走看看