开发一个游戏,事件系统无疑是一个神经中枢般的存在,对解耦工程大有裨益。
接下来,笔者将教大家写一个属于自己的事件系统
需求:
事件系统一般有这几个必要功能:
- 事件订阅(AddListener):用户将要做的事添加到事件系统
- 事件分发(Invoke):框架在特定时刻分发以触发指定类型的事件
- 移除事件 (RemoveListener):用户移除指定的事件
本文完成的事件系统,在实际使用画风是这样的:
- 添加事件监听:

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

Tips:
- 为EventManager 加入了对象池概念,复用池中对象,改善创建对象、回收对象 所产生的内存开销、cpu开销问题。
- 使用方法扩展,实现了链式编程风格,更简写法,更好的阅读性。
- 各种事件移除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日 更新
- 为最大化回归Unity API 风格,EventDispatch 方法名被 Invoke 替换。
- 为方便扩展事件枚举,现在 EventManager.AddListener(xx) 第一个参数不再要求必须放到指定命名空间。
- 代码上传至 GitHub 。。。
扩展阅读
2019年7月21日 更新 - 关于拆箱装箱:
虽然这个事件总线的设计思路虽然很棒,但前提是装箱不耗性能的情况下。
实际上,在事件总线内部逻辑中未发现装箱拆箱操作,但是在配置参数类和订阅事件的时候会各进行一次装箱操作。
如下图:


但是也不用那么担心:
- 首先,事件的订阅是可枚举出来的,也就是说不会产生那么大的数量级的装箱。
- 其次,事件的执行也是条件触发式的,而不是说每时每刻都在装箱。
- 再次,现在的设备性能,运算水平,这点毛毛雨的性能损耗也上不了台面!
- 最后,相对于即得的优惠和明面上的装箱性能损耗,还是能心安的用的,仅供参考。
只希望吧,这个装箱损耗能被解决掉就好咯~~
2019年7月21日 更新 - 关于Enum 拆箱装箱的解决方案的一点建议:
如果实在是纠结难受于上面提到的装箱拆箱性能损耗,可以将事件链中的可以由 Enum 改回到 int ,实际使用中的变化仅仅是传入枚举时强转为 int ,当然,此时这些个枚举就必须指定独一无二的int 值了,手动指定也行,但为了方便这个指定的动作可以写成配置工具,自动配置哈。