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




实现
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-------------------------------
当然,为了避免用户使用未定义的事件类型,我们再封装一个简单的有效性校验方法:
/// <summary>
/// 事件类型参数(枚举)有效性检查
/// </summary>
public static void ValidCheck(Enum _type)
{
if (_type.GetType().Namespace != "FrameWork.Event")
{
string msg = _type.GetType().Namespace;
msg = "命名空间:" + (string.IsNullOrEmpty(msg) ? "无" : msg);
throw new ArgumentException(string.Format("事件系统(纠错):事件类型必须在事件系统中有定义!Tips【{0}】", msg));
}
}
测试
为了测试这个事件系统,笔者还写了2个脚本,一个用来分发事件,一个用来订阅和响应,篇幅有限,就截两个图意思意思:


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

很显然,在这个事件系统里面,泛型的引入并没有展现泛型的优势:不同类型的逻辑复用;
而且,为了限制用户输入,笔者也是不得已用where对泛型加了诸多限制(如上)。
so,在朋友建议指导下,写成了现在这个样子:Enum类来指向我们的枚举,于是,事件类型的枚举便一样可以多样化了
写在最后
经过一段时间的实际开发中使用,发现了一个容易被忽视的问题,那就是这个事件管理器是静态的,跳场景时如果没有主动清空事件链,就会出现一些诡异的事情。
所以笔者建议如果频繁切换场景呢,事件监听要么写在dontDestory的游戏对象上统一管理,要么呢每个游戏对象监听后在OnDestory里面主动取消监听。
最后呢,笔者决定让这个事件系统能够在转换场景时自动刷新事件管理器,那就是将当前的场景名字作为flag标志位,一旦换场景,下次订阅事件时,就会判断场景是否换了,如果换了就重新new一个事件管理器。当然,旧的管理器里面的事件链也要clear一下。不过这里有个bug,那就是当前场景加载到这个场景本身时候,事件还是会累加一次。(那就要想其他方案,譬如场景卸载事件啥的?)
网友评论