一、简介
最近马三换了一家大公司工作,公司制度规范了一些,因此平时的业余时间多了不少。但是人却懒了下来,最近这一个月都没怎么研究新技术,博客写得也是拖拖拉拉,周六周天就躺尸在家看帖子、看小说,要么就是吃鸡,唉!真是罪过罪过。希望能从这篇博客开始有些改善吧,尽量少玩耍,还是多学习吧~
好了扯得有点远了,来说说我们今天博客的主题——“用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 }
然后我们再来看一下最核心的事件中心处理器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 }
我们在其内部维护了一个handlerDic字典,它的key是int类型的,对应的其实就是我们在上面定义的EventType 这个枚举,它的value是一个元素为IEventHandler类型的列表,也就是说我们按照不同的事件类型,将监听者分为了几类进行处理。监听者是可以监听多个消息类型的,也就是说一个监听者实例可以存在于多个列表中,这样并不会产生冲突。我们就从RegisterHandler(IEventHandler handler, params EventType[] eventType)这个对外提供的注册监听的接口入手,逐步的分析一下它的工作流程:
- 调用RegisterHandler方法,传入监听者和需要监听的事件类型(可以是数组,支持多个事件类型),然后遍历事件类型,依次调用RegisterHandler(EventType type, IEventHandler handler)接口,将监听者逐个的注册到每个事件类型对应的监听者列表中;
- 当需要分发事件的时候,调用DispatchEvent方法,传入一个GameEvent类型的参数gameEvent,它包含了需要派发的事件属于什么类型,和对应的事件消息需要传递的参数,其中这个参数又包含了字符串具体的事件名称和一个参数列表;
- 在DispatchEvent中,会根据事件类型来判断内部字段中是否有注册了该事件的监听者,如果有就取到存有这个监听者的列表;
- 然后依次遍历每个监听者,调用其HandleMessage方法,进行具体消息的处理,该函数还会返回一个bool值,表示是否处理了该消息。如果遍历了所有的监听者以后,发现没有处理该消息的监听者,则会打印一个错误消息进行提示;
- 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