【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)這個對外提供的註冊監聽的介面入手,逐步的分析一下它的工作流程:
- 呼叫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分別實現事件分發機制,希望本篇部落格能為大家的工作過程中帶來一些幫助與啟發。
如果覺得本篇部落格對您有幫助,可以掃碼小小地鼓勵下馬三,馬三會寫出更多的好文章,支援微信和支付寶喲!
作者:馬三小夥兒 出處:https://www.cnblogs.com/msxh/p/9539231.html 請尊重別人的勞動成果,讓分享成為一種美德,歡迎轉載。另外,文章在表述和程式碼方面如有不妥之處,歡迎批評指正。留下你的腳印,歡迎評論!
我的部落格即將搬運同步至騰訊雲+社群,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=3s3rkmf0oback