事件系统EventManager

背景

  事件系统主要目的是实现各模块之间的解耦,使各模块独立性更高,便于项目维护和调试。通常在调用某个实例的方法时,必须先获得这个实例的引用或者新实例化一个对象,低耦合度的框架结构希望程序本身不去关注被调用的方法所依托的实例对象是否存在。
  简而言之,事件触发方不需要关心接收方的逻辑,只需要完成事件的触发即可。而接收方不需要关心事件是否被正确触发,只需要知道事件被触发了。这样解耦了触发方和接收方,双方都可以脱离对方进行独立调试。

使用

功能简介

  • 基础功能:订阅、取消订阅、派发
  • 支持多参数派发、允许同名间不同参数个数的事件
  • 支持事件系统支持事件递归派发
  • 支持派发中订阅和取消订阅新事件
  • 事件派发成环检测
  • 事件优先度功能,数值高则优先度高,先被派发。

定义事件

每一个事件都是由事件枚举EventName定义的。
事件定义枚举类:\Assets\Scripts\Core\Manager\EventManager\EventName.cs

1
2
3
4
5
public enum EventName
{
TestEvent, // 测试事件名
OnMouseLeftClick, // 鼠标左键点击事件
}

订阅事件

  事件订阅方(监听者),需要向事件系统EventManager订阅事件,也称为 监听 这个事件,传入的这个函数称为 订阅函数,当这个事件被派发时,所有订阅了这个事件的订阅方都会被通知到:

订阅不带参数的事件

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Receiver
{
private void Start()
{
// EventManager.Instance.AddListener(EventName.事件名, 函数action);
EventManager.Instance.AddListener(EventName.TestEvent, Test);
}

private void Test()
{
print("Test");
}
}

订阅带参数的事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Receiver
{
/// <summary>
/// 在对象实例化时订阅事件
/// </summary>
private void Start()
{
// EventManager.Instance.AddListener<参数类型>(EventName.事件名, 函数action);
EventManager.Instance.AddListener<int>(EventName.TestEvent, Test1);
// 多个参数的
EventManager.Instance.AddListener<int,int>(EventName.TestEvent, Test2);
}

private void Test1(int param)
{
print("Test" + param);
}

private void Test2(int param1, int param2)
{
print("Test" + param1 + param2);
}
}

设置订阅的优先度

  事件系统的一大痛点就是,事件触发的无序性。比如说,A和B都订阅了事件,当事件派发时,我们只知道A和B的事件都被触发了,但是我们并不控制A和B的事件,哪个先被触发。如果我们有特定的时序需求时,比如先算数据后刷新显示,我们就需要解决这个痛点,提高事件系统的功能覆盖性。

1
2
3
4
5
6
7
8
9
10
// A比B的事件先被触发
public class A
{
// EventManager.Instance.AddListener(EventName.事件名, 函数, 优先度int = 0);
EventManager.Instance.AddListener(EventName.TestEvent, Test, 1);
}
public class B
{
EventManager.Instance.AddListener(EventName.TestEvent, Test, 0);
}

移除订阅的事件

也称为 取消事件的监听

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Receiver
{
// ……订阅事件

/// <summary>
/// 在对象销毁时移除监听
/// </summary>
private void OnDestroy()
{
// EventManager.Instance.RemoveListener(EventName.事件名, 函数);
EventManager.Instance.RemoveListener(EventName.TestEvent, Test);
}
}

派发事件

事件派发方,需要派发这个事件。

派发不带参数的事件

1
2
3
4
5
public class Dispatcher
{
// EventManager.Instance.Dispatch(EventName.事件名);
EventManager.Instance.Dispatch(EventName.TestEvent);
}

派发带参数的事件

1
2
3
4
5
6
7
8
9
public class Dispatcher
{
// EventManager.Instance.Dispatch(EventName.事件名,参数1,参数2);
var param1 = 100;
var param2 = 200;
EventManager.Instance.Dispatch<int, int>(EventName.TestEvent, param1, param2);
// 自动识别EventManager中的重载函数,可以简写:
EventManager.Instance.Dispatch(EventName.TestEvent, param, param2);
}

注意事项

  1. 同名事件,但参数个数不同,视为不同事件,派发时,只派发对于参数个数的事件。
  2. 如果派发中,新订阅了一个监听者,该监听者会在本次派发中也被派发。
  3. 如果派发中,新订阅了一个监听者,且优先度高于当前派发到的监听者的优先度,则该新监听者会插队到后面派发到的监听者前面,优先被派发。
  4. 事件派发时,只会派发对应参数个数的事件,举例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 订阅不带参数的Test0,和带1个参数的Test1及Test11,和带2个参数的Test2
    EventManager.Instance.AddListener(EventName.TestEvent, Test0);
    EventManager.Instance.AddListener<int>(EventName.TestEvent, Test1);
    EventManager.Instance.AddListener<int,int>(EventName.TestEvent, Test2);

    func Test0();
    func Test1(int int1);
    func Test2(int int1, int int2);

    // 派发事件时
    EventManager.Instance.Dispatch(EventName.TestEvent); // 只有Test0触发(通知)
    EventManager.Instance.Dispatch(EventName.TestEvent, 1); // 只有Test1触发
    EventManager.Instance.Dispatch(EventName.TestEvent, 1, 1); // 只有Test2触发
  5. 定义事件时,加上事件含义,建议加上参数数量和类型,方便调试
    1
    2
    3
    4
    public enum EventName
    {
    OnMouseLeftClick,// (float,float),鼠标左键点击时,传入点击的坐标x和y
    }

内部实现

外部调用

事件系统是 观察者模式 的具体运用,我们需要将系统设为单例,确保外部能直接访问:

1
public class EventManager : BaseSingleTon<EventManager>

事件列表

  系统内维护着一个 总字典 Dictionary<EventName, List<(Delegate listener, int priority)>>,含义是 总字典<事件名,事件列表List <订阅函数,优先级>,每一个事件名对应一个 事件列表。
声明不同参数的委托Delegate:

1
2
3
4
// 声明不带参数的委托类型
public delegate void YuEvent();
// 声明带一个参数的委托类型
public delegate void YuEvent<in T1>(T1 t1);

派发事件时,根据被调用EventManager.Instance中重载的不同Dispatch(……)函数,对事件列表List进行遍历对应参数个数的订阅函数,通过 定义的委托类型 来实现 对应参数个数的订阅函数 的判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public delegate void YuEvent<T>(); // 定义带一个参数的委托类型

/// <summary>
/// 带一个参数的事件 的派发
/// </summary>
public void Dispatch<T>(EventName 事件名, T param)
{
foreach (var (订阅函数, 优先度) in 总字典[事件名]) // 总字典[事件名] -> 事件列表List
{
if (订阅函数 is YuEvent<T> 委托) // 用委托类型,过滤订阅函数
{
委托.Invoke(param); // 派发符合委托类型的订阅函数
continue;
}
}
}

优先度

  事件系统的优先度功能是基于,事件列表List内,事件的下标顺序,去实现的。订阅事件时维护事件列表的有序性,派发事件时按下标顺序派发订阅的函数。
  基于事件列表被我们维护为 有序列表,这个目的,以及List可以进行 随机访问 的特性。可以通过 二分查找 算法,对订阅事件时的效率进行优化,将插入订阅函数的时间复杂度从O(n)降为O(log n)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// <summary>
/// 订阅事件
/// </summary>
public void AddListener(EventName 事件名, YuEvent 订阅函数, int 优先度 = 0)
{
OnListenerAdding(事件名, 订阅函数, 优先度);
}

/// <summary>
/// 订阅事件时,用二分查找算法,依据优先度,获取该订阅函数的插入位置
/// </summary>
private int FindInsertIndex(EventName 事件名, int 优先度)
{
// ……二分查找算法的具体实现
return 在列表中应该插入的下标位置;
}