UF_OLDL3——事件中心模块

事件中心

事件中心是基于观察者设计模式来设计的,目的是降低程序耦合性,减少程序复杂度,会使用委托,字典

为何要用事件中心

在以前,没有事件中心的时候,
面对一个对象被修改时其它对象要做自动更新的问题:
例如一个怪物死亡,需要让其它对象例如玩家执行什么方法,
我们可能就会让怪物关联这些对象,然后在死亡的方法里调用这些对象

image

但是,这种方法让几个不相关的类之间的耦合度增加了,修改其中一个类,就有可能影响另几个类,这显然不利于我们程序后续的编写和维护
基于观察者设计模式来设计的事件中心可以解决这种一个对象状态改变给其他对象通知的问题

观察者设计模式

观察者模式 | 菜鸟教程 (runoob.com)

观察者模式是一种行为型设计模式,它定义了一种一对多的依赖关系,当一个对象的状态发生改变时,其所有依赖者都会收到通知并自动更新。

当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。
比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。

意图: 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

主要解决: 一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。

何时使用: 一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。

优点:
1、观察者和被观察者是抽象耦合的。
2、建立一套触发机制。

缺点:
1、如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
2、如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
3、观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

事件中心的思路

还是假设我们需要一个怪物对象死亡后,玩家,任务记录,和其它需要执行相对应的方法

假设我们已经有了一个事件中心

  1. 玩家,任务记录,和其它这三个对象,它们需要提前告知事件中心:我们要监听 怪物死亡事件,
  2. 之后怪物对象死亡,怪物将怪物死亡事件告诉给事件中心,
  3. 事件中心接收到怪物死亡事件后,发现玩家,任务记录,和其它这三个对象要在这件事后执行方法
  4. 事件中心告诉玩家,任务记录,和其它这三个对象:应该触发怪物死亡事件之后要执行的方法了
  5. 玩家,任务记录,和其它这三个对象执行怪物对象死亡后需要执行相对应的方法

以上是使用事件中心处理一次一个对象状态改变给其他对象通知,其它的类似的情况也可以用这种思路来进行

可以看到,使用事件中心可以避免几个毫不相关的类关联起来
对象只要向事件中心告知要监听什么事件告知什么事件发生了
之后就由事件中心传达监听者需要执行方法了(事件中心也不需要知道这个方法是什么)
对象和事件中心只需要传达事件和委托,而不需要知道除了自己以外的类要执行什么方法

事件中心模块

知识点:字典,委托

作用:通过一个指定的事件名,设置事件监听委托,当有对象使用该事件名触发事件时,执行该事件名对应的委托里的所有方法

使用方法:

  1. 添加事件监听,传入事件名和用来处理事件的委托函数(需要object类型参数,传入触发事件者的消息,用于监听者区分不同对象触发的消息
  2. 移除事件监听,要注意!至少要在对象销毁生命函数里设置移除对应的事件监听,否则在监听者被销毁后,还执行其委托会造成内存泄漏等问题!
  3. 触发事件,触发者需要传入自己的消息(object)提供给监听者,若字典内存在该事件名,就执行该事件名对应的委托
  4. 清空事件中心,一般用于过场景时

注意:
添加事件监听一定要配套移除事件监听,至少要在对象销毁生命函数里设置移除对应的事件监听,否则在监听者被销毁后,还执行其委托会造成内存泄漏等问题! 这意味着,事件中心模块使用匿名函数必须慎重!因为匿名函数不能移除,只能全部清理! 要注意确认输入的事件名是否正确!以确保事件正常触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
using System.Collections.Generic;
using UnityEngine.Events;

/// <summary>
/// 事件中心模块
/// </summary>
public class EventCenter : BaseManager<EventCenter>
{
/// <summary>
/// 存储事件名字和监听事件的执行委托的字典,键为事件名字,值为监听这个事件对应的委托函数们
/// </summary>
private Dictionary<string, UnityAction<object>> eventDic = new Dictionary<string, UnityAction<object>>();

/// <summary>
/// 添加事件监听,必须要设置对应的移除事件监听!至少要在被销毁的生命周期内执行!以免出现问题
/// </summary>
/// <param name="eventName">事件的名字</param>
/// <param name="action">准备用来处理事件的委托函数(需要object参数,用于传入触发事件者的消息)</param>
public void AddEventListener(string eventName, UnityAction<object> action)
{
//已经存在该事件,就直接把传入的方法加进对应的委托内
if (eventDic.ContainsKey(eventName))
{
eventDic[eventName] += action;
}
//没有该事件,在字典内直接添加键值对
else
{
eventDic.Add(eventName, action);
}
}

/// <summary>
/// 事件触发
/// </summary>
/// <param name="eventName">哪一个名字的事件触发了</param>
/// <param name="info">传入自己的相关消息,可以用于监听者区分不同对象</param>
public void EventTrigger(string eventName, object info)
{
//若字典内存在这个事件,就执行其中的所有事件
if (eventDic.ContainsKey(eventName))
{
eventDic[eventName].Invoke(info);
}
}

/// <summary>
/// 移除对应的事件监听
/// </summary>
/// <param name="eventName">事件名</param>
/// <param name="action">要移除的方法(需要object参数)</param>
public void RemoveEventListener(string eventName, UnityAction<object> action)
{
if (eventDic.ContainsKey(eventName))
eventDic[eventName] -= action;
}

/// <summary>
/// 清空事件中心,主要用在切换场景时
/// </summary>
public void Clear()
{
eventDic.Clear();
}
}

优化装箱拆箱的事件中心模块

在原来的事件中心模块内,触发事件者要上传的所用的参数是object,这不可避免的会遇到装箱拆箱的问题(值类型和引用类型之间的问题)
因此我们需要利用泛型来解决我们的方法

由于新的泛型方法实现和原来的旧事件中心模块实现方法有冲突(两者的数据不能共通,原事件触发方法被其泛型方法屏蔽),两者难以共存,
因此在原有的基础上重写了事件中心模块,和原来的事件中心模块并不兼容!

使用方法:

  1. 添加泛型事件监听,传入事件名和用来处理事件的委托函数(由泛型参数决定传入触发事件者的消息参数的类型,用于监听者区分不同对象触发的消息
  2. 添加无参事件监听,传入事件名和用来处理事件的委托函数(触发者不需要传入任何参数)
  3. 移除泛型事件监听, 传入事件名和用来处理事件的委托函数(由泛型参数决定传入触发事件者的消息参数的类型,用于监听者区分不同对象触发的消息)
  4. 移除无参事件监听,传入事件名和用来处理事件的委托函数
    注意!至少要在对象销毁生命函数里设置移除对应的事件监听,否则在监听者被销毁后,还执行其委托会造成内存泄漏等问题
  5. 触发泛型事件,触发者需要传入自己的消息(类型由泛型决定)提供给监听者,若字典内存在该事件名,就执行该事件名对应的委托
  6. 触发无参事件,触发者不需要传入任何参数,若字典内存在该事件名,就执行该事件名对应的委托
  7. 清空事件中心,一般用于过场景时

注意:

  1. 添加事件监听一定要配套移除事件监听,至少要在对象销毁生命函数里设置移除对应的事件监听,
    否则在监听者被销毁后,还执行其委托会造成内存泄漏等问题!
    这意味着,事件中心模块使用匿名函数必须慎重!因为匿名函数不能移除,只能全部清理!
  2. 要注意确认输入的事件名是否正确!以确保事件正常触发
  3. 注意!一个事件所用的监听函数一定是对应唯一一个参数类型(哪怕是无参的)
    或者说,对于同一事件,所有监听者和所有上传者所使用的泛型参数必须一致(无参的也必须都是无参)
    否则,触发者上传事件后执行监听委托时会出现报错!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
using System.Collections.Generic;
using UnityEngine.Events;

/// <summary>
/// 空接口,用于作为两种事件的父类,使两种事件在事件中心的字典内作为值时,可以使用该类型替换,使两者可以一起被存储于一个字典内
/// </summary>
public interface IEventInfo { }

/// <summary>
/// 泛型事件类,让传入事件中心的监听函数可以使用泛型来决定触发者传入的参数的类型,而不使用object,避免装箱拆箱,需要继承非泛型接口
/// </summary>
/// <typeparam name="T">触发者传入的参数类型</typeparam>
public class EventInfo<T> : IEventInfo
{
public UnityAction<T> actions;

public EventInfo(UnityAction<T> action)
{
actions += action;
}
}

/// <summary>
/// 无参事件类,用于不需要触发者传入消息参数的监听函数,需要继承和泛型事件类相同的父接口
/// </summary>
public class EventInfo : IEventInfo
{
public UnityAction actions;
public EventInfo(UnityAction action)
{
actions += action;
}
}

/// <summary>
/// 事件中心模块
/// </summary>
public class EventCenter : BaseManager<EventCenter>
{
/// <summary>
/// 存储事件名字和泛型监听事件的执行委托的字典,键为事件名字,值为监听这个事件对应的泛型委托函数们
/// </summary>
private Dictionary<string, IEventInfo> eventDic = new Dictionary<string, IEventInfo>();

/// <summary>
/// 添加泛型事件监听,使用泛型来决定触发者需要传入参数的类型,注意要设置对应的移除事件监听!
/// </summary>
/// <typeparam name="T">触发者要传入的消息参数的类型</typeparam>
/// <param name="eventName">事件的名字</param>
/// <param name="action">准备用来处理事件的委托函数(由泛型参数决定类型,用于传入触发事件者的消息)</param>
public void AddEventListener<T>(string eventName, UnityAction<T> action)
{
//已经存在该事件,就直接把传入的方法加进对应的委托内
if (eventDic.ContainsKey(eventName))
{
(eventDic[eventName] as EventInfo<T>).actions += action;
}
//没有该事件,在字典内直接添加键值对
else
{
eventDic.Add(eventName, new EventInfo<T>(action));
}
}

/// <summary>
/// 添加无参事件监听,触发者不需要传入参数,必须要设置对应的移除事件监听!至少要在被销毁的生命周期内执行!以免出现问题
/// </summary>
/// <param name="eventName">事件的名字</param>
/// <param name="action">准备用来处理事件的委托函数(触发者不传入参数)</param>
public void AddEventListener(string eventName, UnityAction action)
{
if (eventDic.ContainsKey(eventName))
{
(eventDic[eventName] as EventInfo).actions += action;
}
else
{
eventDic.Add(eventName, new EventInfo(action));
}
}

/// <summary>
/// 泛型事件触发
/// </summary>
/// <typeparam name="T">触发者传入的消息参数类型</typeparam>
/// <param name="eventName">哪一个名字的事件触发了</param>
/// <param name="info">传入自己的相关消息,可以用于监听者区分不同对象</param>
public void EventTrigger<T>(string eventName, T info)
{
//若字典内存在这个事件,就执行其中的所有事件
if (eventDic.ContainsKey(eventName))
{
(eventDic[eventName] as EventInfo<T>).actions?.Invoke(info);
}
}

/// <summary>
/// 无参事件触发,触发者不需要传入参数的重载
/// </summary>
/// <param name="eventName">哪一个名字的事件触发了</param>
public void EventTrigger(string eventName)
{
//若字典内存在这个事件,就执行其中的所有事件
if (eventDic.ContainsKey(eventName))
{
(eventDic[eventName] as EventInfo).actions?.Invoke();
}
}

/// <summary>
/// 移除对应的泛型事件监听
/// </summary>
/// <typeparam name="T">触发者要传入的消息参数类型</typeparam>
/// <param name="eventName">事件名</param>
/// <param name="action">要移除的方法(参数由泛型类型决定)</param>
public void RemoveEventListener<T>(string eventName, UnityAction<T> action)
{
if (eventDic.ContainsKey(eventName))
(eventDic[eventName] as EventInfo<T>).actions -= action;
}

/// <summary>
/// 移除对应的无参事件监听
/// </summary>
/// <param name="eventName">事件名</param>
/// <param name="action">要移除的方法(不需要触发者传入参数的方法)</param>
public void RemoveEventListener(string eventName, UnityAction action)
{
if (eventDic.ContainsKey(eventName))
(eventDic[eventName] as EventInfo).actions -= action;
}

/// <summary>
/// 清空事件中心,主要用在切换场景时
/// </summary>
public void Clear()
{
eventDic.Clear();
}

#region 委托参数使用object的旧实现(全部废弃)
/// <summary>
/// 存储事件名字和监听事件的执行委托的字典,键为事件名字,值为监听这个事件对应的委托函数们
/// </summary>
//private Dictionary<string, UnityAction<object>> eventDic = new Dictionary<string, UnityAction<object>>();

/// <summary>
/// 添加事件监听,必须要设置对应的移除事件监听!至少要在被销毁的生命周期内执行!以免出现问题
/// </summary>
/// <param name="eventName">事件的名字</param>
/// <param name="action">准备用来处理事件的委托函数(需要object参数,用于传入触发事件者的消息)</param>
//public void AddEventListener(string eventName, UnityAction<object> action)
//{
// //已经存在该事件,就直接把传入的方法加进对应的委托内
// if (eventDic.ContainsKey(eventName))
// {
// eventDic[eventName] += action;
// }
// //没有该事件,在字典内直接添加键值对
// else
// {
// eventDic.Add(eventName, action);
// }
//}

/// <summary>
/// 事件触发
/// </summary>
/// <param name="eventName">哪一个名字的事件触发了</param>
/// <param name="info">传入自己的相关消息,可以用于监听者区分不同对象</param>
//public void EventTrigger(string eventName, object info)
//{
// //若字典内存在这个事件,就执行其中的所有事件
// if (eventDic.ContainsKey(eventName))
// {
// eventDic[eventName]?.Invoke(info);
// }
//}

/// <summary>
/// 移除对应的事件监听
/// </summary>
/// <param name="eventName">事件名</param>
/// <param name="action">要移除的方法(需要object参数)</param>
//public void RemoveEventListener(string eventName, UnityAction<object> action)
//{
// if (eventDic.ContainsKey(eventName))
// eventDic[eventName] -= action;
//}

/// <summary>
/// 清空事件中心,主要用在切换场景时
/// </summary>
//public void Clear()
//{
// eventDic.Clear();
//}
#endregion
}