zoukankan      html  css  js  c++  java
  • 【Unity游戏开发】用C#和Lua实现Unity中的事件分发机制EventDispatcher

    一、简介

      最近马三换了一家大公司工作,公司制度规范了一些,因此平时的业余时间多了不少。但是人却懒了下来,最近这一个月都没怎么研究新技术,博客写得也是拖拖拉拉,周六周天就躺尸在家看帖子、看小说,要么就是吃鸡,唉!真是罪过罪过。希望能从这篇博客开始有些改善吧,尽量少玩耍,还是多学习吧~

      好了扯得有点远了,来说说我们今天博客的主题——“用C#和Lua实现Unity中的事件分发机制”,事件分发机制或者叫事件监听派发系统,在每个游戏框架中都是不可或缺的一个模块。我们可以用它来解耦,监听网络消息,或者做一些异步的操作,好处多多(其实是别人的框架都有这个,所以我们的框架也必须有这玩意~)。今天马三就和大家一起,分别使用C#和Lua实现两种可以用在Unity游戏开发中的事件分发处理机制,希望能对大家有些帮助吧~

    二、C#版的事件分发机制

      首先我们来实现C#版本的事件分发机制,目前这套流程已经集成到了马三自己的 ColaFrameWork框架 中了。这套框架还在架构阶段,里面很多东西都不完善,马三也是会随时把自己的一些想法放到里面,大家感兴趣的话也可以帮忙维护一下哈!

      一般来说事件订阅、派发这种机制都是使用观察者模式来实现的,本篇博客也不例外,正是利用了这种思想。为了解耦和面向接口编程,我们制定了一个接口IEventHandler,凡是观察者都需要实现这个接口,而GameEventMgr事件中心维护了一个IEventHandler列表,保存着一系列的观察者,并在需要的时候进行一系列的动作。这样操作正是遵循了依赖倒置的设计原则:“高层模块不应该依赖于低层模块,两者都应该依赖于抽象概念;”、“抽象接口不应该依赖于实现,而实现应该依赖于抽象接口”。下面的代码定义了IEventHandler接口和一些委托还有事件传递时需要携带的参数。

     1 using System.Collections;
     2 using System.Collections.Generic;
     3 using EventType = ColaFrame.EventType;
     4 
     5 /// <summary>
     6 /// 接收消息后触发的回调
     7 /// </summary>
     8 /// <param name="data"></param>
     9 public delegate void MsgHandler(EventData data);
    10 
    11 /// <summary>
    12 /// 事件处理器的接口
    13 /// </summary>
    14 public interface IEventHandler
    15 {
    16     bool HandleMessage(GameEvent evt);
    17 
    18     bool IsHasHandler(GameEvent evt);
    19 }
    20 
    21 /// <summary>
    22 /// 事件消息传递的数据
    23 /// </summary>
    24 public class EventData
    25 {
    26     public string Cmd;
    27     public List<object> ParaList;
    28 }
    29 
    30 /// <summary>
    31 /// 游戏中的事件
    32 /// </summary>
    33 public class GameEvent
    34 {
    35     /// <summary>
    36     /// 事件类型
    37     /// </summary>
    38     public EventType EventType { get; set; }
    39     /// <summary>
    40     /// 携带参数
    41     /// </summary>
    42     public object Para { get; set; }
    43 }
    44 
    45 
    46 namespace ColaFrame
    47 {
    48     /// <summary>
    49     /// 事件的类型
    50     /// </summary>
    51     public enum EventType : byte
    52     {
    53         /// <summary>
    54         /// 系统的消息
    55         /// </summary>
    56         SystemMsg = 0,
    57         /// <summary>
    58         /// 来自服务器推送的消息
    59         /// </summary>
    60         ServerMsg = 1,
    61         /// <summary>
    62         /// UI界面消息
    63         /// </summary>
    64         UIMsg = 2,
    65     }
    66 }
    View Code

      然后我们再来看一下最核心的事件中心处理器GameEventMgr是如何实现的,还是先上一下全部的代码 GameEventMgr.cs:

      1 using System;
      2 using System.Collections;
      3 using System.Collections.Generic;
      4 using UnityEngine;
      5 using EventType = ColaFrame.EventType;
      6 
      7 /// <summary>
      8 /// 事件消息管理中心
      9 /// </summary>
     10 public class GameEventMgr
     11 {
     12     /// <summary>
     13     /// 存储Hander的字典
     14     /// </summary>
     15     private Dictionary<int, List<IEventHandler>> handlerDic;
     16 
     17     private static GameEventMgr instance = null;
     18 
     19     private GameEventMgr()
     20     {
     21         handlerDic = new Dictionary<int, List<IEventHandler>>();
     22     }
     23 
     24     public static GameEventMgr GetInstance()
     25     {
     26         if (null == instance)
     27         {
     28             instance = new GameEventMgr();
     29         }
     30         return instance;
     31     }
     32 
     33     /// <summary>
     34     /// 对外提供的注册监听的接口
     35     /// </summary>
     36     /// <param name="handler"></param>监听者(处理回调)
     37     /// <param name="eventTypes"></param>想要监听的事件类型
     38     public void RegisterHandler(IEventHandler handler, params EventType[] eventTypes)
     39     {
     40         for (int i = 0; i < eventTypes.Length; i++)
     41         {
     42             RegisterHandler(eventTypes[i], handler);
     43         }
     44     }
     45 
     46     /// <summary>
     47     /// 内部实际调用的注册监听的方法
     48     /// </summary>
     49     /// <param name="type"></param>要监听的事件类型
     50     /// <param name="handler"></param>监听者(处理回调)
     51     private void RegisterHandler(EventType type, IEventHandler handler)
     52     {
     53         if (null != handler)
     54         {
     55             if (!handlerDic.ContainsKey((int)type))
     56             {
     57                 handlerDic.Add((int)type, new List<IEventHandler>());
     58             }
     59             if (!handlerDic[(int)type].Contains(handler))
     60             {
     61                 handlerDic[(int)type].Add(handler);
     62             }
     63         }
     64     }
     65 
     66     /// <summary>
     67     /// 反注册事件监听的接口,对所有类型的事件移除指定的监听
     68     /// </summary>
     69     /// <param name="handler"></param>
     70     public void UnRegisterHandler(IEventHandler handler)
     71     {
     72         using (var enumerator = handlerDic.GetEnumerator())
     73         {
     74             List<IEventHandler> list;
     75             while (enumerator.MoveNext())
     76             {
     77                 list = enumerator.Current.Value;
     78                 list.Remove(handler);
     79             }
     80         }
     81     }
     82 
     83     /// <summary>
     84     /// 反注册事件监听的接口,移除指定类型事件的监听
     85     /// </summary>
     86     /// <param name="handler"></param>
     87     /// <param name="types"></param>
     88     public void UnRegisterHandler(IEventHandler handler, params EventType[] types)
     89     {
     90         EventType type;
     91         for (int i = 0; i < types.Length; i++)
     92         {
     93             type = types[i];
     94             if (handlerDic.ContainsKey((int)type) && handlerDic[(int)type].Contains(handler))
     95             {
     96                 handlerDic[(int)type].Remove(handler);
     97             }
     98         }
     99     }
    100 
    101     /// <summary>
    102     /// 分发事件
    103     /// </summary>
    104     /// <param name="gameEvent"></param>想要分发的事件
    105     public void DispatchEvent(GameEvent gameEvent)
    106     {
    107         bool eventHandle = false;
    108 
    109         List<IEventHandler> handlers;
    110         if (null != gameEvent && handlerDic.TryGetValue((int)gameEvent.EventType, out handlers))
    111         {
    112             for (int i = 0; i < handlers.Count; i++)
    113             {
    114                 try
    115                 {
    116                     eventHandle = handlers[i].HandleMessage(gameEvent) || eventHandle;
    117                 }
    118                 catch (Exception e)
    119                 {
    120                     Debug.LogError(e);
    121                 }
    122             }
    123 
    124             if (!eventHandle)
    125             {
    126                 if (null != gameEvent)
    127                 {
    128                     switch (gameEvent.EventType)
    129                     {
    130                         case EventType.ServerMsg:
    131                             break;
    132                         default:
    133                             Debug.LogError("消息未处理,类型:" + gameEvent.EventType);
    134                             break;
    135                     }
    136                 }
    137             }
    138         }
    139     }
    140 
    141     /// <summary>
    142     /// 分发事件
    143     /// </summary>
    144     /// <param name="evt"></param>分发的消息名称
    145     /// <param name="eventType"></param>消息事件类型
    146     /// <param name="para"></param>参数
    147     public void DispatchEvent(string evt, EventType eventType = EventType.UIMsg, params object[] para)
    148     {
    149         GameEvent gameEvent = new GameEvent();
    150         gameEvent.EventType = eventType;
    151         EventData eventData = new EventData();
    152         eventData.Cmd = evt;
    153         if (null != para)
    154         {
    155             eventData.ParaList = new List<object>();
    156             for (int i = 0; i < para.Length; i++)
    157             {
    158                 eventData.ParaList.Add(para[i]);
    159             }
    160         }
    161         gameEvent.Para = eventData;
    162 
    163         this.DispatchEvent(gameEvent);
    164     }
    165 
    166     /// <summary>
    167     /// 分发事件
    168     /// </summary>
    169     /// <param name="evt"></param>分发的消息名称
    170     /// <param name="eventType"></param>消息事件类型
    171     public void DispatchEvent(string evt, EventType eventType = EventType.UIMsg)
    172     {
    173         GameEvent gameEvent = new GameEvent();
    174         gameEvent.EventType = eventType;
    175         EventData eventData = new EventData();
    176         eventData.Cmd = evt;
    177         eventData.ParaList = null;
    178         gameEvent.Para = eventData;
    179 
    180         this.DispatchEvent(gameEvent);
    181     }
    182 }
    View Code

    我们在其内部维护了一个handlerDic字典,它的key是int类型的,对应的其实就是我们在上面定义的EventType 这个枚举,它的value是一个元素为IEventHandler类型的列表,也就是说我们按照不同的事件类型,将监听者分为了几类进行处理。监听者是可以监听多个消息类型的,也就是说一个监听者实例可以存在于多个列表中,这样并不会产生冲突。我们就从RegisterHandler(IEventHandler handler, params EventType[] eventType)这个对外提供的注册监听的接口入手,逐步的分析一下它的工作流程:

    1. 调用RegisterHandler方法,传入监听者和需要监听的事件类型(可以是数组,支持多个事件类型),然后遍历事件类型,依次调用RegisterHandler(EventType type, IEventHandler handler)接口,将监听者逐个的注册到每个事件类型对应的监听者列表中;
    2. 当需要分发事件的时候,调用DispatchEvent方法,传入一个GameEvent类型的参数gameEvent,它包含了需要派发的事件属于什么类型,和对应的事件消息需要传递的参数,其中这个参数又包含了字符串具体的事件名称和一个参数列表;
    3. 在DispatchEvent中,会根据事件类型来判断内部字段中是否有注册了该事件的监听者,如果有就取到存有这个监听者的列表;
    4. 然后依次遍历每个监听者,调用其HandleMessage方法,进行具体消息的处理,该函数还会返回一个bool值,表示是否处理了该消息。如果遍历了所有的监听者以后,发现没有处理该消息的监听者,则会打印一个错误消息进行提示;
    5. DispatchEvent(string evt, EventType eventType = EventType.UIMsg, params object[] para)和DispatchEvent(string evt, EventType eventType = EventType.UIMsg)这两个接口是对DispatchEvent接口的进一步封装,方便用户进行无参消息派发和含参数消息派发;

    最后我们再来看一下具体的监听者应该如何实现IEventHandler接口,以 ColaFrameWork框架 中的UI基类——UIBase举例,在UIBase内部维护了一个Dictionary<string, MsgHandler> msgHanderDic 结构,用它来保存具体的事件名称对应的回调函数,然后再去具体地实现HandleMessage和IsHasHandler接口中的抽象方法,代码如下:

     1 /// <summary>
     2     /// 处理消息的函数的实现
     3     /// </summary>
     4     /// <param name="gameEvent"></param>事件
     5     /// <returns></returns>是否处理成功
     6     public bool HandleMessage(GameEvent evt)
     7     {
     8         bool handled = false;
     9         if (EventType.UIMsg == evt.EventType)
    10         {
    11             if (null != msgHanderDic)
    12             {
    13                 EventData eventData = evt.Para as EventData;
    14                 if (null != eventData && msgHanderDic.ContainsKey(eventData.Cmd))
    15                 {
    16                     msgHanderDic[eventData.Cmd](eventData);
    17                     handled = true;
    18                 }
    19             }
    20         }
    21         return handled;
    22     }
    23 
    24     /// <summary>
    25     /// 是否处理了该消息的函数的实现
    26     /// </summary>
    27     /// <returns></returns>是否处理
    28     public bool IsHasHandler(GameEvent evt)
    29     {
    30         bool handled = false;
    31         if (EventType.UIMsg == evt.EventType)
    32         {
    33             if (null != msgHanderDic)
    34             {
    35                 EventData eventData = evt.Para as EventData;
    36                 if (null != eventData && msgHanderDic.ContainsKey(eventData.Cmd))
    37                 {
    38                     handled = true;
    39                 }
    40             }
    41         }
    42         return handled;
    43     }

    为了使用更加简洁方便,我们还可以再封装一些函数出来,以便随时注册一个消息和取消注册一个消息,主要是RegisterEvent和UnRegisterEvent接口,代码如下:

     1 /// <summary>
     2     /// 初始化注册消息监听
     3     /// </summary>
     4     protected void InitRegisterHandler()
     5     {
     6         msgHanderDic = null;
     7         GameEventMgr.GetInstance().RegisterHandler(this, EventType.UIMsg);
     8         msgHanderDic = new Dictionary<string, MsgHandler>()
     9         {
    10         };
    11     }
    12 
    13     /// <summary>
    14     /// 取消注册该UI监听的所有消息
    15     /// </summary>
    16     protected void UnRegisterHandler()
    17     {
    18         GameEventMgr.GetInstance().UnRegisterHandler(this);
    19 
    20         if (null != msgHanderDic)
    21         {
    22             msgHanderDic.Clear();
    23             msgHanderDic = null;
    24         }
    25     }
    26 
    27     /// <summary>
    28     /// 注册一个UI界面上的消息
    29     /// </summary>
    30     /// <param name="evt"></param>
    31     /// <param name="msgHandler"></param>
    32     public void RegisterEvent(string evt, MsgHandler msgHandler)
    33     {
    34         if (null != msgHandler && null != msgHanderDic)
    35         {
    36             if (!msgHanderDic.ContainsKey(evt))
    37             {
    38                 msgHanderDic.Add(Name + evt, msgHandler);
    39             }
    40             else
    41             {
    42                 Debug.LogWarning(string.Format("消息{0}重复注册!", evt));
    43             }
    44         }
    45     }
    46 
    47     /// <summary>
    48     /// 取消注册一个UI界面上的消息
    49     /// </summary>
    50     /// <param name="evt"></param>
    51     public void UnRegisterEvent(string evt)
    52     {
    53         if (null != msgHanderDic)
    54         {
    55             msgHanderDic.Remove(Name + evt);
    56         }
    57     }

     关于C#版的事件分发机制大概就介绍到这里了,马三在这里只是大概地讲了下思路,更细致的原理和使用方法大家可以去马三的 ColaFrameWork框架 中找一下相关代码。

    三、Lua版的事件分发机制

      Lua版本的事件分发机制相对C#版的来说就简单了很多,Lua中没有接口的概念,因此实现方式和C#版的也大有不同,不过总的来说还是对外暴露出以下几个接口:

    • Instance():获取单例
    • RegisterEvent():注册一个事件
    • UnRegisterEvent():反注册一个事件
    • DispatchEvent():派发事件
    • AddEventListener():增加监听者
    • RemoveEventListener():移除监听者

      照例还是先上一下核心代码EventMgr.lua,然后再逐步解释:

     1 require("Class")
     2 local bit = require "bit"
     3 
     4 EventMgr = {
     5     --实例对象
     6     _instance = nil,
     7     --观察者列表
     8     _listeners = nil
     9 }
    10 EventMgr.__index = EventMgr
    11 setmetatable(EventMgr, Class)
    12 
    13 -- 构造器
    14 function EventMgr:new()
    15     local t = {}
    16     t = Class:new()
    17     setmetatable(t, EventMgr)
    18     return t
    19 end
    20 
    21 -- 获取单例接口
    22 function EventMgr:Instance()
    23     if EventMgr._instance == nil then
    24         EventMgr._instance = EventMgr:new()
    25         EventMgr._listeners = {}
    26     end
    27     return EventMgr._instance
    28 end
    29 
    30 function EventMgr:RegisterEvent(moduleId, eventId, func)
    31     local key = bit.lshift(moduleId, 16) + eventId
    32     self:AddEventListener(key, func, nil)
    33 end
    34 
    35 function EventMgr:UnRegisterEvent(moduleId, eventId, func)
    36     local key = bit.lshift(moduleId, 16) + eventId
    37     self:RemoveEventListener(key, func)
    38 end
    39 
    40 function EventMgr:DispatchEvent(moduleId, eventId, param)
    41     local key = bit.lshift(moduleId, 16) + eventId
    42     local listeners = self._listeners[key]
    43     if nil == listeners then
    44         return
    45     end
    46     for _, v in ipairs(listeners) do
    47         if v.p then
    48             v.f(v.p, param)
    49         else
    50             v.f(param)
    51         end
    52     end
    53 end
    54 
    55 function EventMgr:AddEventListener(eventId, func, param)
    56     local listeners = self._listeners[eventId]
    57     -- 获取key对应的监听者列表,结构为{func,para},如果没有就新建
    58     if listeners == nil then
    59         listeners = {}
    60         self._listeners[eventId] = listeners -- 保存监听者
    61     end
    62     --过滤掉已经注册过的消息,防止重复注册
    63     for _, v in pairs(listeners) do
    64         if (v and v.f == func) then
    65             return
    66         end
    67     end
    68     --if func == nil then
    69     --    print("func is nil!")
    70     --end
    71     --加入监听者的回调和参数
    72     table.insert(listeners, { f = func, p = param })
    73 end
    74 
    75 function EventMgr:RemoveEventListener(eventId, func)
    76     local listeners = self._listeners[eventId]
    77     if nil == listeners then
    78         return
    79     end
    80     for k, v in pairs(listeners) do
    81         if (v and v.f == func) then
    82             table.remove(listeners, k)
    83             return
    84         end
    85     end
    86 end

      在实际使用的时候主要是调用 RegisterEvent、UnRegisterEvent 和 DispatchEvent这三个接口。RegisterEvent用来注册一个事件,UnRegisterEvent 用来反注册一个事件,DispatchEvent用来派发事件。先从RegisterEvent接口说起,它需要传入3个参数,分别是ModuleId,EventId和回调函数func。ModuleId就是我们不同模块的id,他是一个模块的唯一标识,在实际应用中我们可以定义一个全局的枚举来标识这些模块ID。EventId是不同的消息的标识,它也是数字类型的枚举值,并且因为有了模块ID的存在,不同模块可以使用相同的EventId,这并不会导致消息的冲突。在RegisterEvent内部操作中,我们首先对ModuleId进行了左移16位的操作,然后再加上EventID组成我们的消息key,左移16位可以避免ModuleID直接与EventId组合后会产生Key冲突的问题,一般来说左移16位已经可以满足定义很多模块和事件id的需求了。然后调用 self:AddEventListener(key, func, nil) 方法,将计算出来的key和回调函数进行注册。在EventMgr的内部其实还是维护了一个监听者列表,注册消息的时候,就是把回调和参数添加到监听者列表中。反注册消息就是把对应key的回调从监听者列表中移除。派发事件的时候就是遍历key所对应的监听者列表,然后依次执行里面的回调函数。好了,接着说AddEventListener这个函数的操作,它首先会去获取key对应的监听者列表,结构为{func,para},如果没有就新建一个table,并把它保存为key所对应的监听者列表。得到这个监听者列表以后,我们首先会对其进行遍历,如果里面已经包含func回调函数的话,就直接return掉,过滤掉已经注册过的消息,防止重复注册。如果通过了上一步检查的话,就执行 table.insert(listeners, { f = func, p = param })操作,加入监听者的回调和参数。对于UnRegisterEvent方法,我们依然会计算出key,然后调用 RemoveEventListener 操作,把监听者从监听者列表中移除。在使用DispatchEvent接口进行事件派发的时候,我们依然会先计算出Key,然后取出key对应的监听者列表。接着依次遍历这些监听者,然后执行其中保存着的回调函数,并且把需要传递的事件参数传递进去。具体的使用方法,可以参考下面的Main.lua:

     1 require("EventMgr")
     2 
     3 local function TestCallback_1()
     4     print("Callback_1")
     5 end
     6 
     7 local function TestCallback_2(param)
     8     print("Callback_2")
     9     print(param.id)
    10     print(param.pwd)
    11 end
    12 
    13 local EventMgr = EventMgr:Instance()
    14 EventMgr:RegisterEvent(1, 1, TestCallback_1)
    15 EventMgr:RegisterEvent(2, 1, TestCallback_2)
    16 EventMgr:DispatchEvent(1, 1)
    17 EventMgr:DispatchEvent(2, 1, { id = "abc", pwd = "123" })

    支持含参数事件分发和无参数事件分发,上面代码的执行结果如下,可以发现成功地监听了注册的消息,并且也获取到了传递过来的参数:

     

     图1:代码执行结果

    四、总结

    通过本篇博客,马三和大家一起学习了如何在Unity中使用C#和Lua分别实现事件分发机制,希望本篇博客能为大家的工作过程中带来一些帮助与启发。

    本篇博客中的样例工程已经同步至Github:https://github.com/XINCGer/Unity3DTraining/tree/master/lua/LuaEventMgr,欢迎大家Fork! 

    马三的开源Unity客户端框架 ColaFramework框架:https://github.com/XINCGer/ColaFrameWork

    如果觉得本篇博客对您有帮助,可以扫码小小地鼓励下马三,马三会写出更多的好文章,支持微信和支付宝哟!

           

    作者:马三小伙儿
    出处:https://www.cnblogs.com/msxh/p/9539231.html 
    请尊重别人的劳动成果,让分享成为一种美德,欢迎转载。另外,文章在表述和代码方面如有不妥之处,欢迎批评指正。留下你的脚印,欢迎评论!

    我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=3s3rkmf0oback

  • 相关阅读:
    《软件架构师的12项修炼》阅读笔记01
    《架构即未来》阅读笔记03
    《一线架构师时间指南》-Refined Architecture阶段
    《架构即未来》阅读笔记02
    《架构即未来》阅读笔记01
    大三下第四周总结
    RPA自动化
    在shell中使用Flask
    用蓝图实现模块化应用
    请求上下文
  • 原文地址:https://www.cnblogs.com/msxh/p/9539231.html
Copyright © 2011-2022 走看看