什么是事件中心
举一个简单的例子,比如在玩家角色死亡时,需要执行很多的操作,例如禁止玩家的操控,停止敌人的生成,弹出游戏结束的UI等等。玩家角色死亡就是一个事件,而禁止玩家操作,停止敌人生成和弹出结束UI都是对玩家死亡这个事件的响应。
如果不使用事件中心去实现上面的功能,那么我们就需要让角色身上去获取操作脚本的引用,敌人生成脚本的引用和游戏结束UI脚本的引用,在玩家死亡时手动调用他们。这样虽然能实现效果,但是会相当的麻烦,例如怎么获取这些脚本的引用,后续如果再有脚本在玩家死亡时需要执行某些操作,有需要再角色身上添加该脚本的引用已经增加执行代码。其次这样实现不仅仅麻烦,还会导致代码的耦合度极高,每添加一个相应事件的脚本都需要去修改老脚本,导致老脚本和新脚本关联性大大增强,也导致老脚本越来越繁杂。
为了解决以上问题,需要制作一个脚本专门去记录所有的事件已经事件所对应的所有响应,而这个脚本就是事件中心。
事件中心的实现
事件中心的实现方式有很多,网上大部分的实现方式都是通过字符串加多播委托实现的,但这种实现方式我觉得并不好,因为使用字符串作为事件名的话很容易导致拼写错误已经相同事件的注册问题,并且这种实现方式再传入参数时常常时通过一个泛型来决定参数的,这又会导致一个事件和参数没有强关联,会导致调用事件时传入参数类型和事件定义的类型不一致的问题。
对于事件的实现,我非常喜欢unity官方案例中的一个实现,这个案例是我在b站看到一个up讲解时了解到的。链接如下:Unity项目自学秘籍大揭秘——官方FPS项目模板解析 流程剖析与事件机制_哔哩哔哩_bilibili
我的事件便是在此基础上进行修改所实现的,我的代码如下:
public interface IGameEvent { }
public static class GameEventPriority
{
public const int LOW = 10;
public const int NORMAL = 50;
public const int HIGH = 100;
}
public static class EventManager
{
private static readonly Dictionary<Type, SortedDictionary<int, HashSet<Action<IGameEvent>>>> _events = new Dictionary<Type, SortedDictionary<int, HashSet<Action<IGameEvent>>>>();
private static readonly Dictionary<Delegate, (int priority, Action<IGameEvent> action)> _findEvents = new Dictionary<Delegate, (int priority, Action<IGameEvent>)>();
private static readonly Dictionary<object, HashSet<(Type type, Delegate action)>> _targetEvents = new Dictionary<object, HashSet<(Type type, Delegate action)>>();
public static void AddListener<T>(Action<T> action, int priority = GameEventPriority.NORMAL) where T : IGameEvent
{
if (_findEvents.ContainsKey(action))
return;
Type type = typeof(T);
object target = action.Target;
Action<IGameEvent> newAction = (e) => action((T)e);
_findEvents.Add(action, (priority, newAction));
if (!_events.ContainsKey(type))
_events.Add(type, new SortedDictionary<int, HashSet<Action<IGameEvent>>>());
var dict = _events[type];
if (!dict.ContainsKey(priority))
dict.Add(priority, new HashSet<Action<IGameEvent>>());
dict[priority].Add(newAction);
if (!_targetEvents.ContainsKey(target))
_targetEvents.Add(target, new HashSet<(Type type, Delegate action)>());
_targetEvents[target].Add((type, action));
}
public static void RemoveListener<T>(Action<T> action) where T : IGameEvent
{
if (!_findEvents.ContainsKey(action)) return;
var type = typeof(T);
var target = action.Target;
var evt = _findEvents[action];
_events[type][evt.priority].Remove(evt.action);
_targetEvents[target].Remove((type, action));
_findEvents.Remove(action);
}
public static void RemoveTargetEvents(object target)
{
if (_targetEvents.TryGetValue(target, out var hashSet))
{
foreach (var evtInfo in hashSet)
{
var evt = _findEvents[evtInfo.action];
_findEvents.Remove(evtInfo.action);
_events[evtInfo.type][evt.priority].Remove(evt.action);
}
_targetEvents.Remove(target);
}
}
public static void Broadcast<T>(T evt) where T : IGameEvent
{
Type type = typeof(T);
if (_events.TryGetValue(type, out var dict))
{
foreach (var actionList in dict.Values)
{
foreach (var action in actionList)
action?.Invoke(evt);
}
}
}
}
成员变量的定义
_events:这是记录了所有事件已经对应事件的响应回调,键和官方的定义一样,这里主要讲一下值,值是一个有序字典,有序字典的键是事件的优先级,值是一个该优先级的所有回调,而这个回调是使用集合存储的。第一层使用字典很好理解,就是通过对应事件找到其回调。而第二层使用有序字典则是为了在保证事件优先级有序的情况小能快速访问事件回调的元素。第三层使用集合则是为了能加快移除某个事件回调的移除速度。第一层字典的获取,插入和移除速度都是O(1),第二次有序字典的获取和插入速度为O(logn),第三次插入和移除速度都是O(1)。
_findEvents:这个的定义和官方差不多,就是值我使用了值元组,多存储了一个事件的优先级。
_targetEvents:这个字典定义了一个对象上所监听的所有事件及回调,这个是方便一个对象一次性移除自己身上所有事件使用的。
方法实现思路
AddListener
这个方法需要传入一个事件类的委托,优先级默认为Normal可以不传。首先先判断这个事件委托是否已经被添加过,如果已经添加则略过,否则执行逻辑。先获取这个事件的类型,监听的对象和将这个泛型委托包装成IGameEvent的委托。然后将其依次添加进三个成员变量中。
RemoveListener
先判断该委托是否注册过,如果没有注册过则直接返回,否则执行移除逻辑。首先获取事件类型和委托对象,通过_findEvents中再去找到该委托监听时的优先级和被封装后的委托。然后依次去移除。
RemoveTargetEvents
先判断该监听对象是否存在,不存在就返回。然后遍历它下面的所有委托信息,通过委托在_findEvents找到具体的委托信息做移除。最后把对象从_targetEvents中移除。
Broadcast
遍历_events中对应事件集合下的所有元素即可。
使用
使用事件前要定义一个事件
/// <summary>
/// 游戏结束
/// </summary>
public class GameOverEvent : Singleton<GameOverEvent>, IGameEvent
{
public GameState state;
public static GameOverEvent Set(GameState state)
{
Instance.state = state;
return Instance;
}
}
这里我和官方写法不一样,官方是使用一个静态类在内部初始化,我是使用一个单例类去实现,并且对外一个Set方法方便调用者初始化数据。
private void Awake()
{
//监听事件
EventManager.AddListener<GameOverEvent>(Over);
//广播事件
EventManager.Broadcast(GameOverEvent.Set(gameModeHandle.gameState));
//移除事件
EventManager.RemoveListener<GameOverEvent>(Over);
}
private void Over(GameOverEvent param)
{
GameState gameState = param.state;
BattleSceneManager.Instance.canControl = false;
if (gameState == GameState.Victory)
{
Debug.Log("胜利");
StartCoroutine(ClearBattleGround(TeamID.Team2));
DataManager.Instance.gameData.current = DataManager.Instance.currentPoint.index;
}
else
{
Debug.Log("失败");
StartCoroutine(ClearBattleGround(TeamID.Team1));
}
}