为什么使用消息机制

在Unity中Hierarchy面板中,若A的子级是B,A可以通过transform.Find去找到B所在的组件

但如果B要去访问A的组件,我们应该怎么办呢?

Unity采用了消息机制的方案,但是由于使用了反射,性能会很差。

所以我们可以自己封装一个消息机制。

一个简易的消息机制

原理:用字典存储事件。注册时,将事件添加到字典中。撤销时,从字典中把对应移除。使用时,直接从字典中找出来并调用。

using System;
using System.Collections.Generic;
using UnityEngine;
​
namespace DYFramework
{
    public class Example : MonoBehaviour
    {
        private static Dictionary<string, Action<object>> _notifyMsg = new Dictionary<string, Action<object>>();
​
        public static void Register(string key, Action<object> callback)
        {
            _notifyMsg[key] = callback;
        }
​
        public static void Unregister(string key)
        {
            _notifyMsg.Remove(key);
        }
​
        public static void SendMsg(string key, object obj)
        {
            _notifyMsg[key]?.Invoke(obj);
        }
​
        private void Start()
        {
            Example.Register("消息1", (obj) =>
            {
                Debug.LogFormat("消息;{0}", obj);
            });
            Example.SendMsg("消息1", "hello world"); // 输出:消息;hello world
            
            Example.Unregister("消息1");
            Example.SendMsg("消息1", "hello"); // 报错:KeyNotFoundException: The given key '消息1' was not present in the dictionary.
        }
    }
}

进一步完善

在上面的代码中,我们会遇到两个问题,第一个问题是不能重复注册。第二个问题就是不能重复撤销。

问题的核心点,是因为字典的键唯一。怎么解决呢?

那就是通过事件的 += 和 -= 注册

using System;
using System.Collections.Generic;
using UnityEngine;
​
namespace DYFramework
{
    public class Example : MonoBehaviour
    {
        private static Dictionary<string, Action<object>> _registeredMsgs = new Dictionary<string, Action<object>>();
​
        public static void Register(string msgName, Action<object> onMsgReceived)
        {
            if (!_registeredMsgs.ContainsKey(msgName))
            { // 第一次进来,设置一个空函数
                _registeredMsgs[msgName] = _ => { };
            }
            _registeredMsgs[msgName] += onMsgReceived;
        }
​
        public static void UnRegisterAll(string msgName)
        { // 注销所有注册的事件
            _registeredMsgs.Remove(msgName);
        }
​
        public static void UnRegister(string msgName, Action<object> onMsgReceived)
        { // 注册指定的事件
            if (_registeredMsgs.ContainsKey(msgName))
            {
                _registeredMsgs[msgName] -= onMsgReceived;
            }
            
        }
​
        public static void SendMsg(string key, object obj)
        {
            _registeredMsgs[key]?.Invoke(obj);
        }
​
        private void Start()
        {
            Example.Register("消息1", OnMsgReceived);
            Example.Register("消息1", OnMsgReceived);
            Example.SendMsg("消息1", "hello world"); // 输出:消息1:hello world 消息1:hello world (打印两遍)
            
            Example.UnRegister("消息1", OnMsgReceived);
            
            Example.SendMsg("消息1", "hello"); // 注册了一个,还有一个 输出:消息1:hello
        }
​
        public void OnMsgReceived(object data)
        {
            Debug.LogFormat("消息1:{0}", data);
        }
    }
}

如何用到框架中

using System;
using System.Collections.Generic;
namespace DYFramework
{
    public class MsgDispatcher
    {
        private static Dictionary<string, Action<object>> _registeredMsgs = new Dictionary<string, Action<object>>();
​
        public static void Register(string msgName, Action<object> onMsgReceived)
        {
            if (!_registeredMsgs.ContainsKey(msgName))
            { // 第一次进来,设置一个空函数
                _registeredMsgs[msgName] = _ => { };
            }
            _registeredMsgs[msgName] += onMsgReceived;
        }
​
        public static void UnRegisterAll(string msgName)
        { // 注销所有注册的事件
            _registeredMsgs.Remove(msgName);
        }
​
        public static void UnRegister(string msgName, Action<object> onMsgReceived)
        { // 注册指定的事件
            if (_registeredMsgs.ContainsKey(msgName))
            {
                _registeredMsgs[msgName] -= onMsgReceived;
            }
            
        }
​
        public static void SendMsg(string key, object obj)
        {
            _registeredMsgs[key]?.Invoke(obj);
        }
    }
}

但是这种注册不灵活,因为只能注册带参object的方法,如果想要注册一个无参的方法

using System;
using System.Collections.Generic;
using UnityEngine;
​
namespace DYFramework
{
    public abstract partial class MonoBehaviorSimplify : MonoBehaviour
    {
        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()
        {
            // 这样操作,其他脚本继承了MonoBehaviorSimplify就无法使用OnDestroy()方法了,怎么办?
            OnBeforeDestroy(); // 添加一个抽象方法
            foreach (var record in _mMsgRegisterRecorder)
            {
                MsgDispatcher.UnRegister(record.Key, record.Value);
            }
            _mMsgRegisterRecorder.Clear();
        }
​
        // 如果方法中添加了abstract关键子,那么类也要添加abstract关键字,不然会报错
        protected abstract void OnBeforeDestroy(); 
    }
}

如何用到框架中2--进一步完善(使用列表)

我们是使⽤字典进⾏注册消息的记录的,使⽤字典就要保证字典中的 key 是唯⼀的。⽽我们很可能在⼀个脚本中对⼀个key注册多次,这样⽤字典这个数据结构就显得不合理了。

相⽐字典,List 更合适,因为我们有可能有重复的内容,⽽字典更适合做⼀些查询⼯作,但是 List 并不⽀持键值对,怎么办呢?

封装一个类

using System;
using System.Collections.Generic;
using UnityEngine;
​
namespace DYFramework
{
    /// <summary>
    /// 集成消息机制
    /// </summary>
    public abstract partial class MonoBehaviorSimplify : MonoBehaviour
    {
        
        List<MsgRecord> _mMsgRegisterRecorder = 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);
            _mMsgRegisterRecorder.Add(new MsgRecord() { Name = msgName, OnMsgReceived = onMsgReceived });
        }
​
        private void OnDestroy()
        {
            OnBeforeDestroy(); 
            foreach (var record in _mMsgRegisterRecorder)
            {
                MsgDispatcher.UnRegister(record.Name, record.OnMsgReceived);
            }
            _mMsgRegisterRecorder.Clear();
        }
​
        protected abstract void OnBeforeDestroy(); 
    }
}

使用

using UnityEngine;
​
namespace DYFramework
{
    public class Example : MonoBehaviorSimplify
    {
        private void Start()
        {
            RegisterMsg("DO", OnMsgReceived);
            RegisterMsg("DO", OnMsgReceived);
            RegisterMsg("DO", OnMsgReceived);
            MsgDispatcher.SendMsg("DO", "hellowo"); // 输出三次 hellowo
        }
​
        public void OnMsgReceived(object data)
        {
            Debug.LogFormat("消息1:{0}", data);
        }
​
        protected override void OnBeforeDestroy()
        {
            
        }
    }
}

如何用到框架中2--进一步完善(添加对象池)

我们每次注册消息,都要 new ⼀个 MsgRecord 对象出来。⽽我们在注销的时候,对这个对象是什么都没有做的。

这样会造成⼀个性能问题,这个性能问题主要是有 new 时候寻址造成的(也会增加GC的压力)

我们要做的,就是减少 new 的发⽣次数,要想减少,就得让我们的MsgRecord 能够回收利⽤

所以我们准备一个对象池。

如果对象池中没有,再去创建。如果对象池有,那就从对象池中拿。在销毁时,放回对象池中

using System;
using System.Collections.Generic;
using UnityEngine;
​
namespace DYFramework
{
    /// <summary>
    /// 集成消息机制
    /// </summary>
    public abstract partial class MonoBehaviorSimplify : MonoBehaviour
    {
        
        List<MsgRecord> _mMsgRegisterRecorder = new List<MsgRecord>();
        private class MsgRecord
        {
            static readonly Stack<MsgRecord> _msgRecordPool = new Stack<MsgRecord>();
            public string Name;
            public Action<object> OnMsgReceived;
​
            public static MsgRecord Allocate(string name, Action<object> onMsgReceived)
            {
                MsgRecord record = null;
                if (_msgRecordPool.Count > 0)
                { // 对象池中有
                    record = _msgRecordPool.Pop();
                }
                else
                { // 对象池中没有在创建
                    record = new MsgRecord();
                }
                record.Name = name;
                record.OnMsgReceived = onMsgReceived;
                return record;
            }
​
            public void Recycle()
            {
                Name = null;
                OnMsgReceived = null;
                _msgRecordPool.Push(this);
            }
        }
​
        protected void RegisterMsg(string msgName, Action<object> onMsgReceived)
        {
            MsgDispatcher.Register(msgName, onMsgReceived);
            _mMsgRegisterRecorder.Add(MsgRecord.Allocate(msgName, onMsgReceived));
        }
​
        private void OnDestroy()
        {
            OnBeforeDestroy(); 
            foreach (var record in _mMsgRegisterRecorder)
            {
                MsgDispatcher.UnRegister(record.Name, record.OnMsgReceived);
                record.Recycle();
            }
            _mMsgRegisterRecorder.Clear();
        }
​
        protected abstract void OnBeforeDestroy(); 
    }
}

使用

using System.Collections;
using UnityEngine;
​
namespace DYFramework
{
    public class Example : MonoBehaviorSimplify
    {
        private void Awake()
        {
            RegisterMsg("DO", OnMsgReceived);
            RegisterMsg("DO", OnMsgReceived);
        }
​
        private IEnumerator Start()
        {
            MsgDispatcher.SendMsg("DO", "hellowo"); 
            yield return new WaitForSeconds(1f);
            MsgDispatcher.SendMsg("DO", "hellowo"); // 输出三次 hellowo
        }
​
        public void OnMsgReceived(object data)
        {
            Debug.LogFormat("消息1:{0}", data);
        }
​
        protected override void OnBeforeDestroy()
        {
            
        }
    }
}

如何用到框架中3--进一步完善(遇到的两个问题)

  • 第一个问题

注册消息,直接⽤ RegisterMsg,⽽注销则在 OnDestroy 的时候统⼀进⾏注销。那么单独注销时候怎么办呢?

解决思路:

注销对应键中的所有事件:先从列表中找出所有键名一样的,遍历它,移除字典中的并回收对象

public partial class MonoBehaviorSimplify : MonoBehaviour
    {
        /// <summary>
        /// 注销这个键中的所有事件
        /// </summary>
        /// <param name="msgName">键名</param>
        protected void UnRegisterMsg(string msgName)
        {
            List<MsgRecord> msgRecords = _mMsgRegisterRecorder.FindAll(record=> record.Name == msgName);
            foreach (var record in msgRecords)
            {
                MsgDispatcher.UnRegister(record.Name, record.OnMsgReceived);
                _mMsgRegisterRecorder.Remove(record);
                record.Recycle();
            }
            msgRecords.Clear();
        }
​
        /// <summary>
        /// 注销对应键的指定事件
        /// </summary>
        /// <param name="msgName"></param>
        /// <param name="onMsgReceived"></param>
        protected void UnRegisterMsg(string msgName, Action<object> onMsgReceived)
        {
            
            var msgRecords = _mMsgRegisterRecorder.FindAll(record => record.Name == msgName && record.OnMsgReceived == onMsgReceived);
            foreach (var record in msgRecords)
            {
                MsgDispatcher.UnRegister(record.Name, record.OnMsgReceived);
                _mMsgRegisterRecorder.Remove(record);
                record.Recycle();
            }
            msgRecords.Clear();
            
        }
        
        
    }
  • 第二个问题

第⼆个问题是 API 不统⼀的问题。这个问题要解决起来很简单。

protected void SendMsg(string msgName, Action<object> onMsgReceived)
{
    MsgDispatcher.SendMsg(msgName, onMsgReceived);
}