Unity 3D 教你打造自己的EventSystem(事件总线)

开发一个游戏,事件系统无疑是一个神经中枢般的存在,对解耦工程大有裨益。
接下来,笔者将教大家写一个属于自己的事件系统

需求:

事件系统一般有这几个必要功能:

  • 事件订阅(AddListener):用户将要做的事添加到事件系统
  • 事件分发(Invoke):框架在特定时刻分发以触发指定类型的事件
  • 移除事件 (RemoveListener):用户移除指定的事件

本文完成的事件系统,在实际使用画风是这样的:

  • 添加事件监听:

Tips: 由于事件链的 key 是 System.Enum 类型,事件种类(第一个参数)可以配置不同的枚举值哦~

  • 执行事件:


Tips:

  1. 为EventManager 加入了对象池概念,复用池中对象,改善创建对象、回收对象 所产生的内存开销、cpu开销问题。
  2. 使用方法扩展,实现了链式编程风格,更简写法,更好的阅读性。
  • 各种事件移除API:

实现

2019年5月17日补充: 代码有大量更新,建议围观文末给出的 GitHub 仓库。

1、首先定义事件的信息类的基类,利用里式转换实现事件传递信息的多样性:

namespace FrameWork.Event
{
    /// <summary>
    /// 事件信息类基类
    /// </summary>
    public abstract class BaseEventArgs
    {
        public readonly Enum m_Type;
        public readonly GameObject sender;
        public BaseEventArgs(Enum _t, GameObject _sender)
        {
            m_Type = _t;
            sender = _sender;
        }
    }
}

2、然后,我们继承基类并完善特定的事件信息类,示例展示的是鼠标事件信息类:

/// <summary>
    /// 鼠标事件参数类
    /// </summary>
    public class StylusEventArgs : BaseEventArgs
    {
        /// <summary>
        /// 光标下的游戏对象
        /// </summary>
        public readonly GameObject selected;
        /// <summary>
        /// 光标与游戏对象的触碰点
        /// </summary>
        public readonly Vector3 position = default(Vector3);
        /// <summary>
        /// 鼠标或触笔的按键ID
        /// </summary>
        public readonly int buttonID;
        /// <summary>
        /// 鼠标或者触笔事件
        /// </summary>
        /// <param name="_t">事件类型</param>
        /// <param name="_sender">事件发送者</param>
        /// <param name="_selected"></param>
        /// <param name="_position"></param>
        /// <param name="_buttonID"></param>
        public StylusEventArgs(StylusEvent _t, GameObject _sender, GameObject _selected, int _buttonID = -1, Vector3 _position = default(Vector3)) : base(_t, _sender)
        {
            selected = _selected;
            buttonID = _buttonID;
            position = _position;
        }
    }

3、接着,使用枚举列举光标事件项:

namespace FrameWork.Event
{
   /// <summary>
    /// 笔或者鼠标事件类型
    /// </summary>
    public enum StylusEvent
    {
        Enter,
        Press,
        Release,
        Exit
    }
}

4、重点来了,咱们实现一下事件链以及事件链相关功能(添加,执行,移除事件):

using System;
using System.Collections.Generic;
namespace FrameWork.Event
{
    public class EventEntity
    {
        /// <summary>
        /// 事件链
        /// </summary>
        private static Dictionary<Enum, List<Action<BaseEventArgs>>> eventEntitys = null;
        /// <summary>
        /// 当前事件系统的实例
        /// </summary>
        private static EventEntity entity = null;

        private EventEntity()
        {
            InitEvent();
        }

        /// <summary>
        /// 得到指定枚举项的所有事件链
        /// </summary>
        /// <param name="_type">指定枚举项</param>
        /// <returns>事件链</returns>
        private List<Action<BaseEventArgs>> GetEventList(Enum _type)
        {
            if (!eventEntitys.ContainsKey(_type))
            {
                eventEntitys.Add(_type, new List<Action<BaseEventArgs>>());
            }
            return eventEntitys[_type];
        }
        /// <summary>
        /// 添加事件
        /// </summary>
        /// <param name="_type">指定类型</param>
        /// <param name="action">指定事件</param>
        private void AddEvent(Enum _type, Action<BaseEventArgs> action)
        {
            List<Action<BaseEventArgs>> actions = GetEventList(_type);
            if (!actions.Contains(action))
            {
                actions.Add(action);
            }
        }
        /// <summary>
        /// 执行事件
        /// </summary>
        /// <param name="_type">指定事件类型</param>
        /// <param name="args">事件参数</param>
        private void CallEvent(BaseEventArgs args)
        {
            List<Action<BaseEventArgs>> actions = GetEventList(args.m_Type);
            for (int i = actions.Count - 1; i >= 0; --i)
            {
                if (null != actions[i])
                {
                    actions[i](args);
                }
            }
        }
        /// <summary>
        /// 删除指定的事件
        /// </summary>
        /// <param name="_type">指定类型</param>
        /// <param name="action">指定的事件</param>
        private void DelEvent(Enum _type, Action<BaseEventArgs> action)
        {
            List<Action<BaseEventArgs>> actions = GetEventList(_type);
            if (actions.Contains(action))
            {
                actions.Remove(action);
            }
        }
        /// <summary>
        /// 删除指定的事件
        /// </summary>
        /// <param name="action">指定的事件</param>
        private void DelEvent(Action<BaseEventArgs> action)
        {
            if (eventEntitys.Count>0)
            {
                foreach (List<Action<BaseEventArgs>> actions in eventEntitys.Values)
                {
                    if (actions.Contains(action))
                    {
                        actions.Remove(action);
                    }
                }
            }
        }
        /// <summary>
        /// 初始化事件链
        /// </summary>
        private void InitEvent()
        {
            eventEntitys = new Dictionary<Enum, List<Action<BaseEventArgs>>>();
        }
    }  
}

5、为了让这个事件系统好用,我们添加上几个静态方法。

 #region//--------------------------StaticFunction-------------------------------
        /// <summary>
        /// 添加事件监听
        /// </summary>
        /// <param name="_type">事件类型</param>
        /// <param name="action">事件</param>
        public static void AddListener(Enum _type, Action<BaseEventArgs> action)
        {
            ValidCheck(_type);
            if (null != entity)
            {
                entity.AddEvent(_type, action);
            }
            else
            {
                entity = new EventEntity();
                entity.AddEvent(_type, action);
            }
        }
        /// <summary>
        /// 事件分发
        /// </summary>
        /// <param name="_type">事件类型</param>
        /// <param name="args">事件参数</param>
        public static void EventDispatch(BaseEventArgs args)
        {
            if (null != entity)
            {
                entity.CallEvent(args);
            }
        }
        /// <summary>
        /// 移除事件监听
        /// </summary>
        /// <param name="_type">事件类型</param>
        /// <param name="action">事件</param>
        public static void DelListener(Enum _type, Action<BaseEventArgs> action)
        {
            if (null != entity)
            {
                entity.DelEvent(_type, action);
            }
        }
        /// <summary>
        /// 移除事件监听
        /// </summary>
        /// <param name="_type">事件类型</param>
        /// <param name="action">事件</param>
        public static void DelListener(Action<BaseEventArgs> action)
        {
            if (null != entity)
            {
                entity.DelEvent(action);
            }
        }
        /// <summary>
        /// 移除所有事件
        /// </summary>
        public static void RemoveAllListener()
        {
            if (null != entity)
            {
                entity.InitEvent();
            }
        }
        #endregion//--------------------------StaticFunction-------------------------------

测试

为了测试这个事件系统,笔者还写了2个脚本,一个用来分发事件,一个用来订阅和响应,篇幅有限,就截两个图意思意思:


图一、测试用的脚本
图二、动画演示

补充

笔者的事件系统上一个版本是引入了泛型T,代码风格是这样的:
public class EventEntity<T> where T : struct, IConvertible, IComparable, IFormattable
而使用风格则是这样的:

引入了泛型

很显然,在这个事件系统里面,泛型的引入并没有展现泛型的优势:不同类型的逻辑复用;
而且,为了限制用户输入,笔者也是不得已用where对泛型加了诸多限制(如上)。
so,在朋友建议指导下,写成了现在这个样子:Enum类来指向我们的枚举,于是,事件类型的枚举便一样可以多样化了

写在最后

经过一段时间的实际开发中使用,发现了一个容易被忽视的问题,那就是这个事件管理器是静态的,跳场景时如果没有主动清空事件链,就会出现一些诡异的事情。
所以笔者建议如果频繁切换场景,养成一个OnDestory里面使用EventEntity.DelListener-API来主动取消那些事件监听的好习惯是必不可少的呢!

2019年3月23日 更新

  1. 为最大化回归Unity API 风格,EventDispatch 方法名被 Invoke 替换。
  2. 为方便扩展事件枚举,现在 EventManager.AddListener(xx) 第一个参数不再要求必须放到指定命名空间。
  3. 代码上传至 GitHub 。。。

扩展阅读

2019年7月21日 更新 - 关于拆箱装箱:

虽然这个事件总线的设计思路虽然很棒,但前提是装箱不耗性能的情况下。
实际上,在事件总线内部逻辑中未发现装箱拆箱操作,但是在配置参数类和订阅事件的时候会各进行一次装箱操作。
如下图:


Config函数签名中隐含的装箱

AddListene函数签名中隐含的装箱

但是也不用那么担心:

  • 首先,事件的订阅是可枚举出来的,也就是说不会产生那么大的数量级的装箱。
  • 其次,事件的执行也是条件触发式的,而不是说每时每刻都在装箱。
  • 再次,现在的设备性能,运算水平,这点毛毛雨的性能损耗也上不了台面!
  • 最后,相对于即得的优惠和明面上的装箱性能损耗,还是能心安的用的,仅供参考。

只希望吧,这个装箱损耗能被解决掉就好咯~~

2019年7月21日 更新 - 关于Enum 拆箱装箱的解决方案的一点建议:

如果实在是纠结难受于上面提到的装箱拆箱性能损耗,可以将事件链中的可以由 Enum 改回到 int ,实际使用中的变化仅仅是传入枚举时强转为 int ,当然,此时这些个枚举就必须指定独一无二的int 值了,手动指定也行,但为了方便这个指定的动作可以写成配置工具,自动配置哈。

最后编辑于 :
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容