前言
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。