1. 程式人生 > >【Unity遊戲開發】用C#和Lua實現Unity中的事件分發機制EventDispatcher

【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分別實現事件分發機制,希望本篇部落格能為大家的工作過程中帶來一些幫助與啟發。

如果覺得本篇部落格對您有幫助,可以掃碼小小地鼓勵下馬三,馬三會寫出更多的好文章,支援微信和支付寶喲!

 作者:馬三小夥兒 出處:https://www.cnblogs.com/msxh/p/9539231.html  請尊重別人的勞動成果,讓分享成為一種美德,歡迎轉載。另外,文章在表述和程式碼方面如有不妥之處,歡迎批評指正。留下你的腳印,歡迎評論!

我的部落格即將搬運同步至騰訊雲+社群,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=3s3rkmf0oback