Gameframework(Event初探篇)

前言

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。

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值