zoukankan      html  css  js  c++  java
  • 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 树下。那么这种情况很可能就是跨模块通信了,这种情况下,还是推荐用消息机制。

    所以,我们可以试试使用消息机制来解决我们的问题。

    可是我们目前手里没有消息机制…

    那就造一个吧。

    消息机制要用到的知识:

    1. List 或 LinkedList 或者自己实现的链表。
    2. Dictionary
    3. 委托

    关于第一条,我们选择 List 就好了,不过为了有更高的效率,我们最后会升级成链表。第三条,我们选择 Action,因为这是我们接触过的,以后也是用的比较多的。

    而一般的消息机制会提供三个 API。

    1. 注册事件
    2. 注销事件
    3. 发送事件

    我们先试着设计一下,假如我们想这样使用我们的 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");
            }
        }
    }
    

    菜单执行结果如下
    image.png

    哈哈哈,报错啦,不过我们发现,第一次消息发送成功了,但是第二次发送的时候报错了。是因为消息进行注销了,也就是字典里没有消息名了,这时候直接从字典里取值当然会报错。

    这个问题我们留在下一篇解决,在下一篇,我们要讲解关于这个消息机制的完善。

    第十二个示例还没有完成。

    集成到 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

    更多内容

  • 相关阅读:
    Java Number & Math 类
    excel小技巧-转置(表的横向纵向倒过来)
    excel小技巧-提取填充
    excel小技巧-分列
    Java switch case语句
    if...else
    12.15个人总结
    12.9个人总结
    11.29个人总结
    11.25日个人总结
  • 原文地址:https://www.cnblogs.com/liangxiegame/p/12855425.html
Copyright © 2011-2022 走看看