上一节我们写了一个简单的引用池,而单一的引用池是没有啥作用的,事件池就是引用池的作用之一
我们当然可以把事件定义在模块的内部,比如把一个打开UI的事件定义在UI模块内,但是随着我们模块的不断增多,如果所有事件都定义在自己的模块内,在后期是很难维护的,我们都不知道一共定义了哪些事件...因此,一个全局的事件管理中心,是很有必要的
事件池
事件池是实际上保存事件的地方,可以添加或者取消订阅事件。
在此之前,我希望我们的委托遵循.NET规范,即应该是如下格式
public delegate void TestEventHandler(Object sender, EventArgs e);
其中sender是事件的发送者,而e则是发送的数据
发送者自然是之后会写到的事件管理器,而这里的数据类型我们需要自定义,提供一个统一的基类
/// 所有事件的基类,会被引用池管理
public abstract class GlobalEventArgs:IReference
{
public abstract int Id { get; set; }
public abstract void Clear();
}
其中ID代表事件类型,不同类型的事件需要有不同的ID。Clear()方法属于IReference,我们会发现这个数据类型继承自引用接口,这说明什么?说明事件池传递的参数,是被引用池管理的!这就是引用池的作用之一,如果没有引用池,那每一次事件的调用我们都需要主动的new一个类型。
接着写好基本的事件池框架
public class EventPool<T> where T : GlobalEventArgs
{
#region Private
/// 事件编码与对应的处理方法,这里的key,就是我们注册在事件数据里的ID,EventHandler<T>是一个(Object,T)类型的委托
private Dictionary<int, EventHandler<T>> m_EventHandlers;
#endregion
#region 构造方法
public EventPool()
{
m_EventHandlers = new Dictionary<int, EventHandler<T>>();
}
#endregion
}
接口方法,添加订阅与取消订阅
public class EventPool<T> where T : GlobalEventArgs
{
#region Public 接口方法
/// 订阅事件
public void Subscribe(int id, EventHandler<T> handler)
{
if (handler == null)
{
throw new Exception("事件处理方法为空,无法订阅...");
}
EventHandler<T> eventHandler = null;
// 检查是否获取处理方法失败或获取到的为空
if (!m_EventHandlers.TryGetValue(id, out eventHandler) || eventHandler == null)
{
m_EventHandlers[id] = handler;
}
// 不为空,就检查处理方法是否重复了
else if (Check(id, handler))
{
Debug.LogError("ID为:" + (SGFEvents)id + "的事件下已存在这个处理方法:" + nameof(handler) + "...");
}
else
{
eventHandler += handler;
m_EventHandlers[id] = eventHandler;
}
}
/// 取消订阅事件
public void Unsubscribe(int id, EventHandler<T> handler)
{
if (handler == null)
{
throw new Exception("事件处理方法为空,无法取消订阅...");
}
if (m_EventHandlers.ContainsKey(id))
{
m_EventHandlers[id] -= handler;
}
}
/// 抛出事件(线程不安全),抛出之后会立刻执行
public void FireNow(object sender, T e)
{
HandleEvent(sender, e);
}
#endregion
#region Private 工具方法
/// 检查某个编码的事件是否存在它对应的处理方法
private bool Check(int id, EventHandler<T> handler)
{
if (handler == null)
{
throw new Exception("事件处理方法为空...");
}
EventHandler<T> handlers = null;
if (!m_EventHandlers.TryGetValue(id, out handlers))
{
return false;
}
if (handlers == null)
{
return false;
}
// 遍历委托里的所有方法
foreach (EventHandler<T> i in handlers.GetInvocationList())
{
if (i == handler)
{
return true;
}
}
return false;
}
/// 事件处理
private void HandleEvent(object sender, T e)
{
// 尝试获取事件的处理方法
int eventId = e.Id;
EventHandler<T> handlers = null;
if (m_EventHandlers.TryGetValue(eventId, out handlers))
{
if (handlers != null)
{
handlers(sender, e);
}
else
{
throw new Exception("事件没有对应处理方法:" + eventId);
}
}
}
#endregion
}
到此为止大体上的内容就已经完成了,我们来写我们的事件管理器
事件管理器
其实事件管理器就是对事件池的一个代理罢了
public class EventManager : ManagerBase
{
#region Private
/// 事件池,维护一个事件池,其实事件管理器就是对事件池的代理
private EventPool<GlobalEventArgs> m_EventPool;
#endregion
#region 构造方法
public EventManager()
{
m_EventPool = new EventPool<GlobalEventArgs>();
}
#endregion
#region Override
public override int Priority
{
get { return ManagerPriority.EventManager.GetHashCode(); }
}
#endregion
#region Public 接口方法
/// 订阅事件
public void Subscribe(SGFEvents id, EventHandler<GlobalEventArgs> handler)
{
// =========== 这一部分是用于DEBUG的,会在Hierachy中显示出当前的引用状态 ==========
#if UNITY_EDITOR
var trans = SGFEntry.Instance.transform.Find("EventManager");
var name = id.ToString();
var temp = trans.Find(name);
if (temp == null)
{
GameObject go = new GameObject();
go.name = name;
go.transform.SetParent(trans);
GameObject go2 = new GameObject();
go2.name = $"{handler.Target}:{handler.Method.Name}";
go2.transform.SetParent(go.transform);
}
else
{
GameObject go2 = new GameObject();
go2.name = $"{handler.Target}:{handler.Method.Name}";
go2.transform.SetParent(temp.transform);
}
#endif
// =========================================================================
m_EventPool.Subscribe(id.GetHashCode(), handler);
}
/// 取消订阅事件
public void Unsubscribe(SGFEvents id, EventHandler<GlobalEventArgs> handler)
{
m_EventPool.Unsubscribe(id.GetHashCode(), handler);
// =========== 这一部分是用于DEBUG的,会在Hierachy中显示出当前的引用状态 ==========
#if UNITY_EDITOR
var trans = SGFEntry.Instance.transform.Find("EventManager");
var group = trans.Find(id.ToString());
var child = group.Find($"{handler.Target}:{handler.Method.Name}");
if (child != null)
{
GameObject.DestroyImmediate(child.gameObject);
}
if (group.childCount <= 0)
{
GameObject.DestroyImmediate(group.gameObject);
}
#endif
// =========================================================================
}
/// 抛出事件(线程不安全)
public void FireNow(object sender, GlobalEventArgs e)
{
m_EventPool.FireNow(sender, e);
}
#endregion
}
完全都是在调用事件池中的方法,单纯只是一个代理类而已
好了让我们先来测试一下,以打开UI为例,首先我们需要有一个打开UI的数据类型,且必须继承自GolbalEventArgs
public class UIOpenEventArgs : GlobalEventArgs
{
/// UI的名字,所有的UI都会注册这个事件,则需要靠名称来确定当前到底打开了哪一个UI
public string uiName;
public string data;
/// 这个ID是事件的ID,会根据事件ID找到相应的处理方法
public override int Id
{
get
{
return SGFEvents.OpenUI.GetHashCode();
}
set { }
}
/// 归还引用后清空数据
public override void Clear()
{
Id = UIRegister.None.GetHashCode();
uiName = string.Empty;
data = string.Empty;
}
}
然后让我们在UI中注册这个事件,打开UI这个事件应该是所有UI都需要的,所以我们直接在UIBase中注册
在此之前,为了事件方便管理,我们把所有事件的ID注册到一个类中
/// 注册所有事件的事件编码
public enum SGFEvents
{
/*================== UI相关 =================*/
OpenUI,
}
更新UIBase
public abstract class UIBase : MonoBehaviour
{
#region Field
/// 事件管理器
private EventManager eventManager;
/// UI的名字,不可重复
public string uiName;
#endregion
#region MonoBehaviour
private void Awake()
{
eventManager = SGFEntry.Instance.GetManager<EventManager>();
eventManager.Subscribe(SGFEvents.OpenUI,AfterShow);
Load();
}
private void OnDestroy()
{
eventManager.Unsubscribe(SGFEvents.OpenUI,AfterShow);
UnLoad();
}
#endregion
#region 工具方法
private void AfterShow(object o, GlobalEventArgs e)
{
var temp = e as UIOpenEventArgs;
if (!temp.uiName.Equals(uiName))
{
return;
}
DoAfterShow(o,temp);
}
#endregion
#region 事件
/// <summary>
/// 会在这个UI显示后调用
/// </summary>
public virtual void DoAfterShow(object o, UIOpenEventArgs e)
{
}
#endregion
}
更新UIManager
public class UIManager : ManagerBase
{
#region Field
/// 事件管理器
private EventManager eventManager;
/// 引用池
private ReferenceManager referenceManager;
#endregion
#region 管理器生命周期
public override void Init()
{
//...
// 初始化管理器
eventManager = SGFEntry.Instance.GetManager<EventManager>();
referenceManager = SGFEntry.Instance.GetManager<ReferenceManager>();
}
#endregion
#region 接口方法
/// <summary>
/// 打开一个UI
/// </summary>
public void Open(UIStruct data, UIOpenEventArgs uiOpenEventArgs)
{
// 如果这个UI还没被加载,那需要先加载
if (!uiLoaded.TryGetValue(data.name,out var ui))
{
LoadUI(data,out ui);
}
// 显示UI
ShowUI(ui);
// 发送打开UI事件
uiOpenEventArgs.uiName = ui.uiName;
eventManager.FireNow(this,uiOpenEventArgs);
}
#endregion
}
以Fixed1这个UI为例
public class Fixed1 : UIBase
{
public override void DoAfterShow(object o, UIOpenEventArgs e)
{
Debug.Log(e.data);
}
}
public class test : MonoBehaviour
{
private ReferenceManager referenceManager;
void Start()
{
referenceManager = SGFEntry.Instance.GetManager<ReferenceManager>();
var tempRef = referenceManager.Acquire<UIOpenEventArgs>();
tempRef.data = "测试:打开UI的事件!";
SGFEntry.Instance.GetManager<UIManager>().Open(UIs.Fixed1, tempRef);
}
}
大体上算是完成了,但会有一些细节可能会影响大家的需求,在我这里,我把所有的打开UI归类为一个委托,不关我打开A还是打开B还是打开C,所有注册了这个事件的页面,都会被执行,所以如果大家只想当前最新打开的页面执行委托,那么在重写DoAfterShow方法时,需要进行额外的判断,根据传入的数据来判断是不是当前页面。