前言
Gameframework事件模块理解起来还是比较难的,但是写的真的很棒(๑•̀ㅂ•́)و✧,详细品读一天了,现在准备和大家分享一下,事件模块到底如何写比较好。
1.正常的事件模块
要知道Gameframework事件模块到底哪里好,这里先按照正常思路尝试去设计一个事件模块,首先模拟出一个场景需要用到事件触发机制,就把网络消息分拨的场景作为例子吧,具体代码如下:
public enum CsProtocol : uint
{
cs_login_result = 1,
cs_register_result = 2,
cs_exchange_result = 3,
cs_chat_result = 4,
cs_purchase_result = 5,
}
public struct GmMessage
{
public CsProtocol Protocol;//协议
public byte Code;//状态码
public uint oParam;//扩展参数1
public uint tParam;//扩展参数2
public byte[] Binary;//字节流
}
public class LoginUIForm : UIForm
{
void Awake()
{
Globe.LoginUiForm = this;
SocketManager.instance.AddListener(CsProtocol.cs_login_result,ReceiveLoginResult);
}
void OnDestroy()
{
Globe.LoginUiForm = null;
SocketManager.instance.RemoveListener(CsProtocol.cs_login_result);
}
public void ReceiveLoginResult(GmMessage gmMessage)
{
Console.WriteLine("登陆模块消息");
}
}
public class RegisterUIForm : UIForm
{
void Awake()
{
Globe.RegisterUiForm = this;
SocketManager.instance.AddListener(CsProtocol.cs_register_result,ReceiveRegisterResult);
}
void OnDestroy()
{
Globe.RegisterUiForm = null;
SocketManager.instance.RemoveListener(CsProtocol.cs_register_result);
}
public void ReceiveRegisterResult(GmMessage gmMessage)
{
Console.WriteLine("注册模块消息");
}
}
public class GameShopUIForm : UIForm
{
void Awake()
{
Globe.GameShopUiForm = this;
SocketManager.instance.AddListener(CsProtocol.cs_purchase_result,ReceivePurchaseResult);
}
void OnDestroy()
{
Globe.GameShopUiForm = null;
SocketManager.instance.RemoveListener(CsProtocol.cs_purchase_result);
}
public void ReceivePurchaseResult(GmMessage gmMessage)
{
Console.WriteLine("购买模块消息");
}
}
public class ChatRoomUIForm : UIForm
{
void Awake()
{
Globe.ChatRoomUiForm = this;
SocketManager.instance.AddListener(CsProtocol.cs_chat_result,ReceiveChatResut);
}
void OnDestroy()
{
Globe.ChatRoomUiForm = null;
SocketManager.instance.RemoveListener(CsProtocol.cs_chat_result);
}
public void ReceiveChatResut(GmMessage gmMessage)
{
Console.WriteLine("聊天模块消息");
}
}
public class ExchangeGoodsUIForm : UIForm
{
void Awake()
{
Globe.ExchangeGoodsUiForm = this;
SocketManager.instance.AddListener(CsProtocol.cs_exchange_result,ReceiveExchangeResult);
}
void OnDestroy()
{
Globe.ExchangeGoodsUiForm = null;
SocketManager.instance.RemoveListener(CsProtocol.cs_exchange_result);
}
public void ReceiveExchangeResult(GmMessage gmMessage)
{
Console.WriteLine("兑换模块消息");
}
}
public static class Globe
{
public static LoginUIForm LoginUiForm = null;
public static RegisterUIForm RegisterUiForm = null;
public static GameShopUIForm GameShopUiForm = null;
public static ChatRoomUIForm ChatRoomUiForm = null;
public static ExchangeGoodsUIForm ExchangeGoodsUiForm = null;
}
public class SocketManager
{
public static readonly SocketManager instance = new SocketManager();
public static Dictionary<CsProtocol,Action<GmMessage>> MessageProcessor
= new Dictionary<CsProtocol, Action<GmMessage>>();
public void ReceiveMessage(GmMessage gmMessage)//消息监听主入口
{
try
{
Action<GmMessage> netFun = null;
if(MessageProcessor.TryGetValue(gmMessage.Protocol, out netFun))
netFun?.Invoke(gmMessage);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
public void AddListener(CsProtocol csProtocol, Action<GmMessage> netFun)
{
if(!MessageProcessor.ContainsKey(csProtocol))
MessageProcessor.Add(csProtocol, netFun);
else
Console.WriteLine("已经注册相关协议,请勿重复");
}
public void RemoveListener(CsProtocol csProtocol)
{
if (!MessageProcessor.Remove(csProtocol))
Console.WriteLine("移除失败,没有存在此协议");
}
}
这里就是使用事件方式构成的消息监听方案,看着好像没什么问题,但其实是有三个问题:1.参数必须要一致的,它们都是使用GmMessage结构体作为参数(但是GmMessage有时只需要用到CsProtocol参数,其他参数都不需要,比如登陆失败时直接有一个协议参数即可,多余参数都被无视了)。2.事件模块无法重复利用,万一要定义人物动作触发事件机制,需要重新写一个类似的事件触发代码,所以上面代码只是用于网络消息监听,这样看来上面代码连事件模块都称不上。3.万一参数不是靠服务器发过来, 需要内部处理怎么办?(比如人物动作触发机制,每个参数值需要不同,参数可以有动作速度,在循环里面监听动作触发事件,难道把速度值写死?比如actionFun?.Invoke(1)),这三点仔细思考以后确实比较致命。以上代码的例子,其实来自于笔者写的观察者模式,看来迎娶白富美太难了,真是路漫漫其修远兮,如果有兴趣可以去看看观察者模式文章,具体传送门如下:
https://blog.csdn.net/m0_37920739/article/details/104504114
2.Gameframework事件模块
于是今天的主角来了,Gameframework事件模块来拯救各位于水火之中,介绍一下框架是如何把事件模块搭建起来,首先我们可以从EventPool脚本入手,具体代码如下所示:
using System;
using System.Collections.Generic;
namespace GameFramework
{
/// <summary>
/// 事件池。
/// </summary>
/// <typeparam name="T">事件类型。</typeparam>
internal sealed partial class EventPool<T> where T : BaseEventArgs
{
private readonly GameFrameworkMultiDictionary<int, EventHandler<T>> m_EventHandlers;
private readonly Queue<Event> m_Events;
private readonly Dictionary<object, LinkedListNode<EventHandler<T>>> m_CachedNodes;
private readonly Dictionary<object, LinkedListNode<EventHandler<T>>> m_TempNodes;
private readonly EventPoolMode m_EventPoolMode;
private EventHandler<T> m_DefaultHandler;
/// <summary>
/// 初始化事件池的新实例。
/// </summary>
/// <param name="mode">事件池模式。</param>
public EventPool(EventPoolMode mode)
{
m_EventHandlers = new GameFrameworkMultiDictionary<int, EventHandler<T>>();
m_Events = new Queue<Event>();
m_CachedNodes = new Dictionary<object, LinkedListNode<EventHandler<T>>>();
m_TempNodes = new Dictionary<object, LinkedListNode<EventHandler<T>>>();
m_EventPoolMode = mode;
m_DefaultHandler = null;
}
/// <summary>
/// 获取事件处理函数的数量。
/// </summary>
public int EventHandlerCount
{
get
{
return m_EventHandlers.Count;
}
}
/// <summary>
/// 获取事件数量。
/// </summary>
public int EventCount
{
get
{
return m_Events.Count;
}
}
/// <summary>
/// 事件池轮询。
/// </summary>
/// <param name="elapseSeconds">逻辑流逝时间,以秒为单位。</param>
/// <param name="realElapseSeconds">真实流逝时间,以秒为单位。</param>
public void Update(float elapseSeconds, float realElapseSeconds)
{
while (m_Events.Count > 0)
{
Event eventNode = null;
lock (m_Events)
{
eventNode = m_Events.Dequeue();
HandleEvent(eventNode.Sender, eventNode.EventArgs);
}
ReferencePool.Release(eventNode);
}
}
/// <summary>
/// 关闭并清理事件池。
/// </summary>
public void Shutdown()
{
Clear();
m_EventHandlers.Clear();
m_CachedNodes.Clear();
m_TempNodes.Clear();
m_DefaultHandler = null;
}
/// <summary>
/// 清理事件。
/// </summary>
public void Clear()
{
lock (m_Events)
{
m_Events.Clear();
}
}
/// <summary>
/// 获取事件处理函数的数量。
/// </summary>
/// <param name="id">事件类型编号。</param>
/// <returns>事件处理函数的数量。</returns>
public int Count(int id)
{
GameFrameworkLinkedListRange<EventHandler<T>> range = default(GameFrameworkLinkedListRange<EventHandler<T>>);
if (m_EventHandlers.TryGetValue(id, out range))
{
return range.Count;
}
return 0;
}
/// <summary>
/// 检查是否存在事件处理函数。
/// </summary>
/// <param name="id">事件类型编号。</param>
/// <param name="handler">要检查的事件处理函数。</param>
/// <returns>是否存在事件处理函数。</returns>
public bool Check(int id, EventHandler<T> handler)
{
if (handler == null)
{
throw new GameFrameworkException("Event handler is invalid.");
}
return m_EventHandlers.Contains(id, handler);
}
/// <summary>
/// 订阅事件处理函数。
/// </summary>
/// <param name="id">事件类型编号。</param>
/// <param name="handler">要订阅的事件处理函数。</param>
public void Subscribe(int id, EventHandler<T> handler)
{
if (handler == null)
{
throw new GameFrameworkException("Event handler is invalid.");
}
if (!m_EventHandlers.Contains(id))
{
m_EventHandlers.Add(id, handler);
}
else if ((m_EventPoolMode & EventPoolMode.AllowMultiHandler) == 0)
{
throw new GameFrameworkException(Utility.Text.Format("Event '{0}' not allow multi handler.", id.ToString()));
}
else if ((m_EventPoolMode & EventPoolMode.AllowDuplicateHandler) == 0 && Check(id, handler))
{
throw new GameFrameworkException(Utility.Text.Format("Event '{0}' not allow duplicate handler.", id.ToString()));
}
else
{
m_EventHandlers.Add(id, handler);
}
}
/// <summary>
/// 取消订阅事件处理函数。
/// </summary>
/// <param name="id">事件类型编号。</param>
/// <param name="handler">要取消订阅的事件处理函数。</param>
public void Unsubscribe(int id, EventHandler<T> handler)
{
if (handler == null)
{
throw new GameFrameworkException("Event handler is invalid.");
}
if (m_CachedNodes.Count > 0)
{
foreach (KeyValuePair<object, LinkedListNode<EventHandler<T>>> cachedNode in m_CachedNodes)
{
if (cachedNode.Value != null && cachedNode.Value.Value == handler)
{
m_TempNodes.Add(cachedNode.Key, cachedNode.Value.Next);
}
}
if (m_TempNodes.Count > 0)
{
foreach (KeyValuePair<object, LinkedListNode<EventHandler<T>>> cachedNode in m_TempNodes)
{
m_CachedNodes[cachedNode.Key] = cachedNode.Value;
}
m_TempNodes.Clear();
}
}
if (!m_EventHandlers.Remove(id, handler))
{
throw new GameFrameworkException(Utility.Text.Format("Event '{0}' not exists specified handler.", id.ToString()));
}
}
/// <summary>
/// 设置默认事件处理函数。
/// </summary>
/// <param name="handler">要设置的默认事件处理函数。</param>
public void SetDefaultHandler(EventHandler<T> handler)
{
m_DefaultHandler = handler;
}
/// <summary>
/// 抛出事件,这个操作是线程安全的,即使不在主线程中抛出,也可保证在主线程中回调事件处理函数,但事件会在抛出后的下一帧分发。
/// </summary>
/// <param name="sender">事件源。</param>
/// <param name="e">事件参数。</param>
public void Fire(object sender, T e)
{
Event eventNode = Event.Create(sender, e);
lock (m_Events)
{
m_Events.Enqueue(eventNode);
}
}
/// <summary>
/// 抛出事件立即模式,这个操作不是线程安全的,事件会立刻分发。
/// </summary>
/// <param name="sender">事件源。</param>
/// <param name="e">事件参数。</param>
public void FireNow(object sender, T e)
{
HandleEvent(sender, e);
}
/// <summary>
/// 处理事件结点。
/// </summary>
/// <param name="sender">事件源。</param>
/// <param name="e">事件参数。</param>
private void HandleEvent(object sender, T e)
{
bool noHandlerException = false;
GameFrameworkLinkedListRange<EventHandler<T>> range = default(GameFrameworkLinkedListRange<EventHandler<T>>);
if (m_EventHandlers.TryGetValue(e.Id, out range))
{
LinkedListNode<EventHandler<T>> current = range.First;
while (current != null && current != range.Terminal)
{
m_CachedNodes[e] = current.Next != range.Terminal ? current.Next : null;
current.Value(sender, e);
current = m_CachedNodes[e];
}
m_CachedNodes.Remove(e);
}
else if (m_DefaultHandler != null)
{
m_DefaultHandler(sender, e);
}
else if ((m_EventPoolMode & EventPoolMode.AllowNoHandler) == 0)
{
noHandlerException = true;
}
ReferencePool.Release(e);
if (noHandlerException)
{
throw new GameFrameworkException(Utility.Text.Format("Event '{0}' not allow no handler.", e.Id.ToString()));
}
}
}
}
需要关注两个函数,一个是Fire(抛出事件)还有一个是Subscribe(订阅事件),它们到底有何区别呢?这么感觉傻傻分不清楚它们具体的区别是什么,正常情况不是订阅一下事件不就可以了?不要慌!在这里一起去分析到底它们的功能是干嘛的?
在Update函数里循环处理调用事件,处理一次就释放掉一个参数,它们会先把参数从队列里面取出来,然后调用HandleEvent函数,Fire函数和Subscribe函数保存的数据到底有什么区别呢?经过反复观摩,得到的结论是Fire将保存需要的参数值到队列里,Subscribe是仅仅把多播事件保存到字典中,然后它们通过一个Id关联在一起,Id是类的hashcode,通过函数GetHashCode获取到的,保证key值必须是唯一、匹配,具体代码如下:
public sealed class Args : GameEventArgs
{
/// <summary>
/// 显示实体成功事件编号。
/// </summary>
public static readonly int EventId = typeof(Args).GetHashCode();
}
然后通过key值获取到多播事件,之后把队列取出的参数传递给多播事件去使用,多播事件是保存了功能类似的函数列表,需要的形参也是一致的,比如多播事件里可以是资源加载失败、资源加载成功、资源更新事件、资源异步加载。所以需要触发事件前先进行监听(Subscribe),然后实际需要调用事件时,通过Fire函数将参数保存到队列中,等待循环将其依次取出,以下是图解:
是不是感觉突然懂了!!!而且更加优秀的地方是参数里有调用者类,可以在回调事件里持有调用者类进行任何操作,且参数也是自定义的,说明可以拿着事件模块去复用。Subscribe函数正常情况在流程进入时调用,UnSubscribe函数在流程销毁时调用,形成添加和释放的配对(如果不UnSubscribe也没有问题的,但是字典过大可能导致性能问题),当流程需要什么事件,就监听什么事件即可。实际调用就是通过Fire将参数压到队列里,参数循环取用时会找到对应主公把一切交给自己的主公。
3.重构消息监听代码
接下来我们用框架事件模块的思想去简单的重构一下小节一的代码,具体代码如下:
public class LoginUIForm : UIForm
{
void Awake()
{
Globe.LoginUiForm = this;
SocketManager.instance.AddListener(CsProtocol.cs_login_result,ReceiveLoginResult);
}
void OnDestroy()
{
Globe.LoginUiForm = null;
SocketManager.instance.RemoveListener(CsProtocol.cs_login_result);
}
public void ReceiveLoginResult(GmMessage gmMessage)
{
Console.WriteLine("登陆模块消息");
}
}
public class RegisterUIForm : UIForm
{
void Awake()
{
Globe.RegisterUiForm = this;
SocketManager.instance.AddListener(CsProtocol.cs_register_result,ReceiveRegisterResult);
}
void OnDestroy()
{
Globe.RegisterUiForm = null;
SocketManager.instance.RemoveListener(CsProtocol.cs_register_result);
}
public void ReceiveRegisterResult(GmMessage gmMessage)
{
Console.WriteLine("注册模块消息");
}
}
public class GameShopUIForm : UIForm
{
void Awake()
{
Globe.GameShopUiForm = this;
SocketManager.instance.AddListener(CsProtocol.cs_purchase_result,ReceivePurchaseResult);
}
void OnDestroy()
{
Globe.GameShopUiForm = null;
SocketManager.instance.RemoveListener(CsProtocol.cs_purchase_result);
}
public void ReceivePurchaseResult(GmMessage gmMessage)
{
Console.WriteLine("购买模块消息");
}
}
public class ChatRoomUIForm : UIForm
{
void Awake()
{
Globe.ChatRoomUiForm = this;
SocketManager.instance.AddListener(CsProtocol.cs_chat_result,ReceiveChatResut);
}
void OnDestroy()
{
Globe.ChatRoomUiForm = null;
SocketManager.instance.RemoveListener(CsProtocol.cs_chat_result);
}
public void ReceiveChatResut(GmMessage gmMessage)
{
Console.WriteLine("聊天模块消息");
}
}
public class ExchangeGoodsUIForm : UIForm
{
void Awake()
{
Globe.ExchangeGoodsUiForm = this;
SocketManager.instance.AddListener(CsProtocol.cs_exchange_result,ReceiveExchangeResult);
}
void OnDestroy()
{
Globe.ExchangeGoodsUiForm = null;
SocketManager.instance.RemoveListener(CsProtocol.cs_exchange_result);
}
public void ReceiveExchangeResult(GmMessage gmMessage)
{
Console.WriteLine("兑换模块消息");
}
}
public static class Globe
{
public static LoginUIForm LoginUiForm = null;
public static RegisterUIForm RegisterUiForm = null;
public static GameShopUIForm GameShopUiForm = null;
public static ChatRoomUIForm ChatRoomUiForm = null;
public static ExchangeGoodsUIForm ExchangeGoodsUiForm = null;
}
public class SocketManager
{
public static readonly SocketManager instance = new SocketManager();
public static Dictionary<CsProtocol,Action<GmMessage>> MessageProcessor
= new Dictionary<CsProtocol, Action<GmMessage>>();
private readonly Queue<GmMessage> m_Event = snew Queue<GmMessage>();
public void ReceiveMessage(GmMessage gmMessage)//消息监听主入口
{
m_Event.Enqueue(gmMessage);
}
private void OnUpdate()//循环
{
if(m_Events.Count = 0)
return;
try
{
GmMessage gmMessage = m_Events.Dequeue();
Action<GmMessage> netFun = null;
if(MessageProcessor.TryGetValue(gmMessage.Protocol, out netFun))
netFun?.Invoke(gmMessage);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
public void AddListener(CsProtocol csProtocol, Action<GmMessage> netFun)
{
if(!MessageProcessor.ContainsKey(csProtocol))
MessageProcessor.Add(csProtocol, netFun);
else
Console.WriteLine("已经注册相关协议,请勿重复");
}
public void RemoveListener(CsProtocol csProtocol)
{
if (!MessageProcessor.Remove(csProtocol))
Console.WriteLine("移除失败,没有存在此协议");
}
}
这样做到了功能和数据分离,虽然以上经过修改的代码没有看出什么优势,正常情况封装出EventPool和EventManager然后公用事件模块代码,这里只是介绍一下思想而已(我才没有水文章,我没有....)。经过这种设计将弥补之前说过的三个缺点,各位读到最后应该是甚解了,如果还没有明白可以打电话和我讨论的,放心不会把各位按在墙上暴打的,电话是XXXXXXXXXXX。