Unity3D游戲輕量級xlua熱修復框架


Unity3D游戲輕量級xlua熱修復框架

 

一  這是什么東西

  前陣子剛剛集成xlua到項目,目的只有一個:對線上游戲C#邏輯有Bug的地方執行修復,通過考察xlua和tolua,最終選擇了xlua,很大部分原因是因為項目已經到了后期,線上版本迭代了好幾次,所以引入Lua的目的不是為了開發新版本模塊。xlua在我們的這種情況下很是適用,如xlua作者所說,用C#開發,用lua熱更,xlua這套框架為我們提供了諸多便利,至少我可以說,在面臨同樣的情況下,你用tolua去做同樣的事情是很費心的。但是如果你是想用xlua做整套客戶端游戲邏輯的,這篇文對你可能就沒什么借鑒意義了。其實純lua寫邏輯,使用xlua還是tolua並不是那么重要,因為與c#交互會少很多,而且一般都是耗性能的地方才放c#,即使網上有各種lua框架性能的評測,其實我感覺意義都不太大,如果真要頻繁調用,那不管xlua還是tolua你都要考慮方案去優化的。

  當時在做完這個xlua熱更框架,本打算寫篇博文分享一下。后來,由於工作一直比較忙,這個事情就被擱淺了下來,另外,集成xlua時自己寫的代碼少得可伶,感覺也沒什么太多要分享的地方。畢竟熱修復,本質上來說就是一個輕量級的東西。除非你是新開的項目,一開始就遵循xlua熱更的各種規范。而如果你是后期引入的xlua,那么,xlua熱修復代碼的復雜度,很大程度上取決於你框架原先c#代碼的寫法,比如說委托的使用,在c#側經常作為回調去使用,xlua的demo里對委托的熱修復示例是這樣的:

 

復制代碼
 1 public Action<string> TestDelegate = (param) =>
 2 {
 3     Debug.Log("TestDelegate in c#:" + param);
 4 };
 5 
 6 public void TestFunction(Action<string> callback)
 7 {
 8     //do something
 9     callback("this is a test string");
10     //do something
11 }
12 
13 public void TestCall()
14 {
15     TestFunction(TestDelegate);
16 }
復制代碼

 

  這里相當於把委托定義為了成員變量,那么你在lua側,如果要熱修復TestCall函數,要將這個委托作為回調傳遞給TestFunction,只需要使用self.TestDelegate就能訪問,很簡單。而問題就在於,我們項目之前對委托的使用方式是這樣的:

 

復制代碼
 1 public void TestDelegate(String param)
 2 {
 3     Debug.Log("TestDelegate in c#:" + param);
 4 }
 5 
 6 public void TestFunction(Action<string> callback)
 7 {
 8     //do something
 9     callback("this is a test string");
10     //do something
11 }
12 
13 public void TestCall()
14 {
15     TestFunction(TestDelegate);
16 }
復制代碼

 

  那么問題就來了,這個TestDelegate是一個函數,在調用的時候才自動創建了一個臨時委托,那么Lua側,你就沒辦法簡單地去熱更了,怎么辦?這里我要說的就是類似這樣的一些問題,因為一開始沒有考慮過進行xlua熱更,所以導致沒有明確匹配xlua熱更規則的相關代碼規范,從而修復困難。

  這個例子可能舉得不是太好,你可以暴力修改項目中所有這樣寫法的地方(只要你樂意- -),另外,下面的這種寫法有GC問題,這個問題是項目歷史遺留下來的。

二  現行xlua分享的弊端

  當初在集成xlua到項目時,發現現行網絡上對xlua的大多分享,沒有直接命中我所面臨的問題,有實際借鑒意義的項目不多,對很多分享來說:

  1)體積太重:集成了各種資源熱更新、場景管理、音樂管理、定時器管理等等邊緣模塊,xlua內容反而顯得太輕。

  2)避重就輕:簡單集成xlua,然后自己用NGUI或者UGUI寫了個小demo,完事。

三  輕量級xlua熱修復框架

  其實說是xlua的一個擴展更加貼切,對xlua沒有提供的一些外圍功能進行了擴展。xlua的設計還是挺不錯的,相比tolua的代碼讀起來還是要清爽多了。

3.1  框架工程結構

  我假設你已經清楚了xlua做熱修復的基本流程,因為下面不會對xlua本身的熱更操作做太多說明。先一張本工程的截圖:

 

xlua熱修復框架工程結構

 

  1)Scripts/xlua/XLuaManager:xlua熱修復環境,包括luaState管理,自定義loader。

  2)Resources/xlua/Main.lua:xlua熱修復入口

  3)Resources/xlua/Common:提供給lua代碼使用的一些工具方法,提供lua邏輯代碼到C#調用的一層封裝

  4)Scripts/xlua/Util:為xlua的lua腳本提供的C#側代碼支持,被Resources/xlua/Common所使用

  5)Scripts/test/HotfixTest:需要熱修復的c#腳本

  6)Resources/xlua/HotFix:熱修復腳本

  需要說明的一點是,這里所有的熱修復示例我都沒有單獨去做demo演示了,其實如果你真的需要,自己去寫測試也沒多大問題,所有Lua熱更對應的C#邏輯都在,好進行對比。本文主要說的方向有這么幾點:

  1)消息系統:打通cs和lua側的消息系統,其中的關鍵問題是泛型委托

  2)對象創建:怎么樣在lua側創建cs對象,特別是泛型對象

  3)迭代器:cs側列表、字典之類的數據類型,怎樣在lua側泛型迭代

  4)協程:cs側協程怎么熱更,怎么在lua側創建協程

  5)委托作為回調:cs側函數用作委托回調,當作函數調用的形參時,怎樣在lua側傳遞委托形參

3.2  lua側cs泛型對象創建

  對象創建xlua給的例子很簡單,直接new CS.XXX就好,但是如果你要創建一個泛型List對象,比如List<string>,要怎么弄?你可以為List<sting>在c#側定義一個靜態輔助類,提供類似叫CreateListString的函數去創建,但是你不可能為所有的類型都定義這樣一層包裝吧。所以,問題的核心是,我們怎么樣在Lua側只知道類型信息,就能讓cs代勞給我們創建出對象:

 

復制代碼
 1 --common.helper.lua
 2 -- new泛型array
 3 local function new_array(item_type, item_count)
 4     return CS.XLuaHelper.CreateArrayInstance(item_type, item_count)
 5 end
 6 
 7 -- new泛型list
 8 local function new_list(item_type)
 9     return CS.XLuaHelper.CreateListInstance(item_type)
10 end
11 
12 -- new泛型字典
13 local function new_dictionary(key_type, value_type)
14     return CS.XLuaHelper.CreateDictionaryInstance(key_type, value_type)
15 end
復制代碼

 

  這是Resources/xlua/Common下的helper腳本其中的一部分,接下來的腳本我都會在開頭寫上模塊名,不再做說明。這個目錄下的代碼為lua邏輯層代碼提過對cs代碼訪問的橋接,這樣做有兩個好處:第一個是隱藏實現細節,第二個是容易更改實現。這里的三個接口都使用到了Scripts/xlua/Util下的XLuaHelper來做真實的事情。這兩個目錄下的腳本大概的職責都是這樣的,Resources/xlua/Common封裝lua調用,如果能用lua腳本實現,那就實現,不能實現,那在Resources/xlua/Common寫cs腳本提供支持。下面是cs側相關代碼:

 

復制代碼
 1 // CS.XLuaHelper
 2 // 說明:擴展CreateInstance方法
 3 public static Array CreateArrayInstance(Type itemType, int itemCount)
 4 {
 5     return Array.CreateInstance(itemType, itemCount);
 6 }
 7 
 8 public static IList CreateListInstance(Type itemType)
 9 {
10     return (IList)Activator.CreateInstance(MakeGenericListType(itemType));
11 }
12 
13 public static IDictionary CreateDictionaryInstance(Type keyType, Type valueType)
14 {
15     return (IDictionary)Activator.CreateInstance(MakeGenericDictionaryType(keyType, valueType));
16 }
復制代碼

3.3  lua側cs迭代器訪問

  xlua作者在demo中給出了示例,只是個人覺得用起來麻煩,所以包裝了一層語法糖,lua代碼如下:

 

復制代碼
 1 -- common.helper.lua
 2 -- cs列表迭代器:含包括Array、ArrayList、泛型List在內的所有列表
 3 local function list_iter(cs_ilist, index)
 4     index = index + 1
 5     if index < cs_ilist.Count then
 6         return index, cs_ilist[index]
 7     end
 8 end
 9 
10 local function list_ipairs(cs_ilist)
11     return list_iter, cs_ilist, -1
12 end
13 
14 -- cs字典迭代器
15 local function dictionary_iter(cs_enumerator)
16     if cs_enumerator:MoveNext() then
17         local current = cs_enumerator.Current
18         return current.Key, current.Value
19     end
20 end
21 
22 local function dictionary_ipairs(cs_idictionary)
23     local cs_enumerator = cs_idictionary:GetEnumerator()
24     return dictionary_iter, cs_enumerator
25 end
復制代碼

 

  這部分代碼不需要額外的cs腳本提供支持,只是實現了lua的泛型迭代,能夠用在lua的for循環中,使用代碼如下(只給出列表示例,對字典是類似的):

 

復制代碼
 1 -- common.helper.lua
 2 -- Lua創建和遍歷泛型列表示例
 3 local helper = require 'common.helper'
 4 local testList = helper.new_list(typeof(CS.System.String))
 5 testList:Add('111')
 6 testList:Add('222')
 7 testList:Add('333')
 8 print('testList', testList, testList.Count, testList[testList.Count - 1])
 9 
10 -- 注意:循環區間為閉區間[0,testList.Count - 1]
11 -- 適用於列表子集(子區間)遍歷
12 for i = 0, testList.Count - 1 do
13     print('testList', i, testList[i])
14 end
15 
16 -- 說明:工作方式與上述遍歷一樣,使用方式上雷同lua庫的ipairs,類比於cs的foreach
17 -- 適用於列表全集(整區間)遍歷,推薦,很方便
18 -- 注意:同cs的foreach,遍歷函數體不能修改i,v,否則結果不可預料
19 for i, v in helper.list_ipairs(testList) do
20     print('testList', i, v)
21 end
復制代碼

 

  要看懂這部分的代碼,需要知道lua中的泛型for循環是怎么樣工作的:

 

1 for var_1, ..., var_n in explist do 
2     block 
3 end

 

  對於如上泛型for循環通用結構,其代碼等價於:

 

復制代碼
1 do
2     local _f, _s, _var = explist
3     while true do
4         local var_1, ... , var_n = _f(_s, _var)
5         _var = var_1
6         if _var == nil then break end
7         block
8     end
9 end
復制代碼

 

  泛型for循環的執行過程如下:
  首先,初始化,計算 in 后面表達式的值,表達式應該返回范性 for 需要的三個值:迭代函數_f,狀態常量_s和控制變量_var;與多值賦值一樣,如果表達式返回的結果個數不足三個會自動用 nil 補足,多出部分會被忽略。
  第二,將狀態常量_s和控制變量_var作為參數調用迭代函數_f(注意:對於 for 結構來說,狀態常量_s沒有用處,僅僅在初始化時獲取他的值並傳遞給迭代函數_f)。
  第三,將迭代函數_f返回的值賦給變量列表。
  第四,如果返回的第一個值為 nil 循環結束,否則執行循環體。
  第五,回到第二步再次調用迭代函數。

  如果控制變量的初始值是 a0,那么控制變量將循環:a1=_f(_s,a0)、a2=_f(_s,a1)、……,直到 ai=nil。對於如上列表類型的迭代,其中explist = list_ipairs(cs_ilist),根據第一點,可以得到_f = list_iter,_s = cs_ilist, _var = -1,然后進入while死循環,此處每次循環拿_s = cs_ilist, _var = -1作為參數調用_f = list_iter,_f = list_iter內部對_var執行自增,所以這里的_var就是一個計數變量,也是list的index下標,返回值index、cs_ilist[index]賦值給for循環中的i、v,當遍歷到列表末尾時,兩個值都被賦值為nil,循環結束。這個機制和cs側的foreach使用迭代器的工作機制是有點雷同的,如果你清楚這個機制,那么這里的原理就不難理解。

3.4  lua側cs協程熱更

  先看cs側協程的用法:

 

復制代碼
 1 // cs.UIRankMain
 2 public override void Open(object param, UIPathData pathData)
 3 {
 4     // 其它代碼省略
 5     StartCoroutine(TestCorotine(3));
 6 }
 7 
 8 IEnumerator TestCorotine(int sec)
 9 {
10     yield return new WaitForSeconds(sec);
11     Logger.Log(string.Format("This message appears after {0} seconds in cs!", sec));
12     yield break;
13 }
復制代碼

 

  很普通的一種協程寫法,下面對這個協程的調用函數Open,協程函數體TestCorotine執行熱修復:

 

復制代碼
 1 -- HotFix.UIRankMainTest.lua
 2 -- 模擬Lua側的異步回調
 3 local function lua_async_test(seconds, coroutine_break)
 4     print('lua_async_test '..seconds..' seconds!')
 5     -- TODO:這里還是用Unity的協程相關API模擬異步,有需要的話再考慮在Lua側實現一個獨立的協程系統
 6     yield_return(CS.UnityEngine.WaitForSeconds(seconds))
 7     coroutine_break(true, seconds)
 8 end
 9 
10 -- lua側新建協程:本質上是在Lua側建立協程,然后用異步回調驅動,
11 local corotineTest = function(self, seconds)
12     print('NewCoroutine: lua corotineTest', self)
13     
14     local s = os.time()
15     print('coroutine start1 : ', s)
16     -- 使用Unity的協程相關API:實際上也是CS側協程結束時調用回調,驅動Lua側協程繼續往下跑
17     -- 注意:這里會在CS.CorotineRunner新建一個協程用來等待3秒,這個協程是和self沒有任何關系的
18     yield_return(CS.UnityEngine.WaitForSeconds(seconds))
19     print('coroutine end1 : ', os.time())
20     print('This message1 appears after '..os.time() - s..' seconds in lua!')
21     
22     local s = os.time()
23     print('coroutine start2 : ', s)
24     -- 使用異步回調轉同步調用模擬yield return
25     -- 這里使用cs側的函數也是可以的,規則一致:最后一個參數必須是一個回調,回調被調用時表示異步操作結束
26     -- 注意:
27     --    1、如果使用cs側函數,必須將最后一個參數的回調(cs側定義為委托)導出到[CSharpCallLua]
28     --    2、用cs側函數時,返回值也同樣通過回調(cs側定義為委托)參數傳回
29     local boolRetValue, secondsRetValue = util.async_to_sync(lua_async_test)(seconds)
30     print('coroutine end2 : ', os.time())
31     print('This message2 appears after '..os.time() - s..' seconds in lua!')
32     -- 返回值測試
33     print('boolRetValue:', boolRetValue, 'secondsRetValue:', secondsRetValue)
34 end
35 
36 -- 協程熱更示例
37 xlua.hotfix(CS.UIRankMain, 'Open', function(self, param, pathData)
38     print('HOTFIX:Open ', self)
39     -- 省略其它代碼
40     -- 方式一:新建Lua協程,優點:可新增協程;缺點:使用起來麻煩
41     print('----------async call----------')
42     util.coroutine_call(corotineTest)(self, 4)--相當於CS的StartCorotine,啟動一個協程並立即返回
43     print('----------async call end----------')
44     
45     -- 方式二:沿用CS協程,優點:使用方便,可直接熱更協程代碼邏輯,缺點:不可以新增協程
46     self:StartCoroutine(self:TestCorotine(3))
47 end)
48 
49 -- cs側協程熱更
50 xlua.hotfix(CS.UIRankMain, 'TestCorotine', function(self, seconds)
51     print('HOTFIX:TestCorotine ', self, seconds)
52     --注意:這里定義的匿名函數是無參的,全部參數以閉包方式傳入
53     return util.cs_generator(function()
54         local s = os.time()
55         print('coroutine start3 : ', s)
56         --注意:這里直接使用coroutine.yield,跑在self這個MonoBehaviour腳本中
57         coroutine.yield(CS.UnityEngine.WaitForSeconds(seconds))
58         print('coroutine end3 : ', os.time())
59         print('This message3 appears after '..os.time() - s..' seconds in lua!')
60     end)
61 end)
復制代碼

 

  代碼看起來有點復雜,但是實際上要說的點都在代碼注釋中了。xlua作者已經對協程做了比較好的支持,不需要我們另外去操心太多。

3.5  lua側創建cs委托回調

  這里回歸的是篇頭所闡述的問題,當cs側某個函數的參數是一個委托,而調用方在cs側直接給了個函數,在lua側怎么去熱更的問題,先給cs代碼:

 

復制代碼
 1 // cs.UIArena
 2 private void UpdateDailyAwardItem(List<BagItemData> itemList)
 3 {
 4     if (itemList == null)
 5     {
 6         return;
 7     }
 8 
 9     for (int i = 0; i < itemList.Count; i++)
10     {
11         UIGameObjectPool.instance.GetGameObject(ResourceMgr.RESTYPE.UI, TheGameIds.UI_BAG_ITEM_ICON, new GameObjectPool.CallbackInfo(onBagItemLoad, itemList[i], Vector3.zero, Vector3.one * 0.65f, m_awardGrid.gameObject));
12     }
13     m_awardGrid.Reposition();
14 }
復制代碼

 

  這是UI上面普通的一段異步加載背包Item的Icon資源問題,資源層異步加載完畢以后回調到當前腳本的onBagItemLoa函數對UI資源執行展示。現在就這段代碼執行一下熱修復:

 

復制代碼
 1 -- HotFix.UIArenaTese.lua
 2 -- 回調熱更示例(消息系統的回調除外)
 3 --    1、緩存委托
 4 --    2、Lua綁定(實際上是創建LuaFunction再cast到delegate),需要在委托類型上打[CSharpCallLua]標簽--推薦
 5 --    3、使用反射再執行Lua綁定
 6 xlua.hotfix(CS.UIArena, 'UpdateDailyAwardItem', function(self, itemList)
 7     print('HOTFIX:UpdateDailyAwardItem ', self, itemList)
 8     
 9     if itemList == nil then
10         do return end
11     end
12     
13     for i, item in helper.list_ipairs(itemList) do
14         -- 方式一:使用CS側緩存委托
15         local callback1 = self.onBagItemLoad
16         -- 方式二:Lua綁定
17         local callback2 = util.bind(function(self, gameObject, object)
18             self:OnBagItemLoad(gameObject, object)
19         end, self)
20         -- 方式三:
21         --    1、使用反射創建委托---這里沒法直接使用,返回的是Callback<,>類型,沒法隱式轉換到CS.GameObjectPool.GetGameObjectDelegate類型
22         --    2、再執行Lua綁定--需要在委托類型上打[CSharpCallLua]標簽
23         -- 注意:
24         --    1、使用反射創建的委托可以直接在Lua中調用,但作為參數時,必須要求參數類型一致,或者參數類型為Delegate--參考Lua側消息系統實現
25         --    2、正因為存在類型轉換問題,而CS側的委托類型在Lua中沒法拿到,所以在Lua側執行類型轉換成為了不可能,上面才使用了Lua綁定
26         --    3、對於Lua側沒法執行類型轉換的問題,可以在CS側去做,這就是[CSharpCallLua]標簽的作用,xlua底層已經為我們做好這一步
27         --    4、所以,這里相當於方式二多包裝了一層委托,從這里可以知道,委托做好全部打[CSharpCallLua]標簽,否則更新起來很受限
28         --    5、對於Callback和Action類型的委托(包括泛型)都在CS.XLuaHelper實現了反射類型創建,所以不需要依賴Lua綁定,可以任意使用
29         -- 靜態函數測試
30         local delegate = helper.new_callback(typeof(CS.UIArena), 'OnBagItemLoad2', typeof(CS.UnityEngine.GameObject), typeof(CS.System.Object))
31         delegate(self.gameObject, nil)
32         -- 成員函數測試
33         local delegate = helper.new_callback(self, 'OnBagItemLoad', typeof(CS.UnityEngine.GameObject), typeof(CS.System.Object))
34         local callback3 = util.bind(function(self, gameObject, object)
35             delegate(gameObject, object)
36         end, self)
37         
38         -- 其它測試:使用Lua綁定添加委托:必須[CSharpCallLua]導出委托類型,否則不可用
39         callback5 = callback1 + util.bind(function(self, gameObject, object)
40             print('callback4 in lua', self, gameObject, object)
41         end, self)
42         
43         local callbackInfo = CS.GameObjectPool.CallbackInfo(callback3, item, Vector3.zero, Vector3.one * 0.65, self.m_awardGrid.gameObject)
44         CS.UIGameObjectPool.instance:GetGameObject(CS.ResourceMgr.RESTYPE.UI, CS.TheGameIds.UI_BAG_ITEM_ICON, callbackInfo)
45     end
46     self.m_awardGrid:Reposition()
47 end)
復制代碼

 

  有三種可行的熱修復方式:

  1)緩存委托:就是在cs側不要直接用函數名來作為委托參數傳遞(會臨時創建一個委托),而是在cs側用一個成員變量緩存委托,並使用函數初始化它,使用時直接self.xxx訪問。

  2)Lua綁定:創建一個閉包,需要在cs側的委托類型上打上[CSharpCallLua]標簽,實際上xlua作者建議將工程中所有的委托類型打上這個標簽。

  3)使用反射再執行lua綁定:這種方式使用起來很受限,這里不再做說明,要了解的朋友自己參考源代碼。

 

3.6  打通lua和cs的消息系統

  cs側消息系統使用的是這個:http://wiki.unity3d.com/index.php/Advanced_CSharp_Messenger。里面使用了泛型編程的思想,xlua作者在demo中針對泛型接口的熱修復給出的建議是實現擴展函數,但是擴展函數需要對一個類型去做一個接口,這里的消息系統類型完全是可以任意的,顯然這種方案顯得捉襟見肘。核心的問題只有一個,怎么根據參數類型信息去動態創建委托類型。

  委托類型其實是一個數據結構,它引用靜態方法或引用類實例及該類的實例方法。在我們定義一個委托類型時,C#會創建一個類,有點類似C++函數對象的概念,但是它們還是相差很遠,由於時間和篇幅關系,這里不再做太多說明。總之這個數據結構在lua側是無法用類似CS.XXX去訪問到的,正因為如此,所以才為什么所有的委托類型都需要打上[CSharpCallLua]標簽去做一個映射表。lua不能訪問到cs委托類型,沒關系,我們可以在cs側創建出來就行了。而Delegate 類是委托類型的基類,所有的泛型委托類型都可通過它進行函數調用的參數傳遞,解決泛型委托的傳參問題。先看下lua怎么用這個消息系統:

 

復制代碼
 1 -- HotFix.UIArenaTest.lua
 2 -- Lua消息響應
 3 local TestLuaCallback = function(self, param)
 4     print('LuaDelegateTest: ', self, param, param and param.rank)
 5 end
 6 
 7 local TestLuaCallback2 = function(self, param)
 8     print('LuaDelegateTest: ', self, param, param and param.Count)
 9 end
10 
11 -- 添加消息示例
12 xlua.hotfix(CS.UIArena, 'AddListener', function(self)
13     ---------------------------------消息系統熱更測試---------------------------------
14     -- 用法一:使用cs側函數作為回調,必須在XLuaMessenger導出,無法新增消息監聽,不支持重載函數
15     messenger.add_listener(CS.MessageName.MN_ARENA_PERSONAL_PANEL, self, self.UpdatePanelInfo)
16     
17     -- 用法二:使用lua函數作為回調,必須在XLuaMessenger導出,可以新增任意已導出的消息監聽
18     messenger.add_listener(CS.MessageName.MN_ARENA_PERSONAL_PANEL, self, TestLuaCallback)
19     
20     -- 用法三:使用CS側成員委托,無須在XLuaMessenger導出,可以新增同類型的消息監聽,CS側必須緩存委托
21     messenger.add_listener(CS.MessageName.MN_ARENA_UPDATE, self.updateLeftTimes)
22     
23     -- 用法四:使用反射創建委托,無須在XLuaMessenger導出,CS側無須緩存委托,靈活度高,效率低,支持重載函數
24     -- 注意:如果該消息在CS代碼中沒有使用過,則最好打[ReflectionUse]標簽,防止IOS代碼裁剪
25     messenger.add_listener(CS.MessageName.MN_ARENA_BOX, self, 'SetBoxState', typeof(CS.System.Int32))
26 end)
27 
28 -- 移除消息示例
29 xlua.hotfix(CS.UIArena, 'RemoveListener', function(self)
30     -- 用法一
31     messenger.remove_listener(CS.MessageName.MN_ARENA_PERSONAL_PANEL, self, self.UpdatePanelInfo)
32     
33     -- 用法二
34     messenger.remove_listener(CS.MessageName.MN_ARENA_PERSONAL_PANEL, self, TestLuaCallback)
35     
36     -- 用法三
37     messenger.remove_listener(CS.MessageName.MN_ARENA_UPDATE, self.updateLeftTimes)
38     
39     -- 用法四
40     messenger.remove_listener(CS.MessageName.MN_ARENA_BOX, self, 'SetBoxState', typeof(CS.System.Int32))
41 end)
42 
43 -- 發送消息示例
44 util.hotfix_ex(CS.UIArena, 'OnGUI', function(self)
45     if Button(Rect(100, 300, 150, 80), 'lua BroadcastMsg1') then
46         local testData = CS.ArenaPanelData()--正確
47         --local testData = helper.new_object(typeof(CS.ArenaPanelData))--正確
48         testData.rank = 7777;
49         messenger.broadcast(CS.MessageName.MN_ARENA_PERSONAL_PANEL, testData)
50     end
51     
52     if Button(Rect(100, 400, 150, 80), 'lua BroadcastMsg3') then
53         local testData = CS.ArenaPanelData()
54         testData.rank = 7777;
55         messenger.broadcast(CS.MessageName.MN_ARENA_UPDATE, testData)
56     end
57 
58     if Button(Rect(100, 500, 150, 80), 'lua BroadcastMsg4') then
59         messenger.broadcast(CS.MessageName.MN_ARENA_BOX, 3)
60     end
61     self:OnGUI()
62 end)
復制代碼

 

  從lua側邏輯層來說,有4種使用方式:

  1)使用cs側函數作為回調:直接使用cs側的函數作為回調,傳遞self.xxx函數接口,必須在XLuaMessenger導出,無法新增消息監聽,不支持重載函數,XLuaMessenger稍后再做說明

  2)使用lua函數作為回調:在lua側定義函數作為消息回調,必須在XLuaMessenger導出,可以新增任意已導出的消息監聽

  3)使用CS側成員委托:無須在XLuaMessenger導出,可以新增同類型的消息監聽,CS側必須緩存委托,這個之前也說了,委托作為類成員變量緩存,很方便在lua中使用

  4)使用反射創建委托:就是根據參數類型動態生成委托類型,無須在XLuaMessenger導出,CS側無須緩存委托,靈活度高,效率低,支持重載函數。需要注意的是該委托類型一定要沒有被裁剪

  從以上4種使用方式來看,lua層邏輯代碼使用消息系統十分簡單,且靈活性很大。lua側的整套消息系統用common.messenger.lua輔助實現,看下代碼:

 

復制代碼
  1 -- common.messenger.lua
  2 -- added by wsh @ 2017-09-07 for Messenger-System-Proxy
  3 -- lua側消息系統,基於CS.XLuaMessenger導出類,可以看做是對CS.Messenger的擴展,使其支持Lua
  4 
  5 local unpack = unpack or table.unpack
  6 local util = require 'common.util'
  7 local helper = require 'common.helper'
  8 local cache = {}
  9 
 10 local GetKey = function(...)
 11     local params = {...}
 12     local key = ''
 13     for _,v in ipairs(params) do
 14         key = key..'\t'..tostring(v)
 15     end
 16     return key
 17 end
 18 
 19 local GetCache = function(key)
 20     return cache[key]
 21 end
 22 
 23 local SetCache = function(key, value)
 24     assert(GetCache(key) == nil, 'already contains key '..key)
 25     cache[key] = value
 26 end
 27 
 28 local ClearCache = function(key)
 29     cache[key] = nil
 30 end
 31 
 32 local add_listener_with_delegate = function(messengerName, cs_del_obj)
 33     CS.XLuaMessenger.AddListener(messengerName, cs_del_obj)
 34 end
 35 
 36 local add_listener_with_func = function(messengerName, cs_obj, func)
 37     local key = GetKey(cs_obj, func)
 38     local obj_bind_callback = GetCache(key)
 39     if obj_bind_callback == nil then
 40         obj_bind_callback = util.bind(func, cs_obj)
 41         SetCache(key, obj_bind_callback)
 42         
 43         local lua_callback = CS.XLuaMessenger.CreateDelegate(messengerName, obj_bind_callback)
 44         CS.XLuaMessenger.AddListener(messengerName, lua_callback)
 45     end
 46 end
 47 
 48 local add_listener_with_reflection = function(messengerName, cs_obj, method_name, ...)
 49     local cs_del_obj = helper.new_callback(cs_obj, method_name, ...)
 50     CS.XLuaMessenger.AddListener(messengerName, cs_del_obj)
 51 end
 52 
 53 local add_listener = function(messengerName, ...)
 54     local params = {...}
 55     assert(#params >= 1, 'error params count!')
 56     if #params == 1 then
 57         add_listener_with_delegate(messengerName, unpack(params))
 58     elseif #params == 2 and type(params[2]) == 'function' then
 59         add_listener_with_func(messengerName, unpack(params))
 60     else
 61         add_listener_with_reflection(messengerName, unpack(params))
 62     end
 63 end
 64 
 65 local broadcast = function(messengerName, ...)
 66     CS.XLuaMessenger.Broadcast(messengerName, ...)
 67 end
 68 
 69 local remove_listener_with_delegate = function(messengerName, cs_del_obj)
 70     CS.XLuaMessenger.RemoveListener(messengerName, cs_del_obj)
 71 end
 72 
 73 local remove_listener_with_func = function(messengerName, cs_obj, func)
 74     local key = GetKey(cs_obj, func)
 75     local obj_bind_callback = GetCache(key)
 76     if obj_bind_callback ~= nil then
 77         ClearCache(key)
 78         
 79         local lua_callback = CS.XLuaMessenger.CreateDelegate(messengerName, obj_bind_callback)
 80         CS.XLuaMessenger.RemoveListener(messengerName, lua_callback)
 81     end
 82 end
 83 
 84 local remove_listener_with_reflection = function(messengerName, cs_obj, method_name, ...)
 85     local cs_del_obj = helper.new_callback(cs_obj, method_name, ...)
 86     CS.XLuaMessenger.RemoveListener(messengerName, cs_del_obj)
 87 end
 88 
 89 local remove_listener = function(messengerName, ...)
 90     local params = {...}
 91     assert(#params >= 1, 'error params count!')
 92     if #params == 1 then
 93         remove_listener_with_delegate(messengerName, unpack(params))
 94     elseif #params == 2 and type(params[2]) == 'function' then
 95         remove_listener_with_func(messengerName, unpack(params))
 96     else
 97         remove_listener_with_reflection(messengerName, unpack(params))
 98     end
 99 end
100 
101 return {
102     add_listener = add_listener,
103     broadcast = broadcast,
104     remove_listener = remove_listener,
105 }
復制代碼

 

  有以下幾點需要說明:

  1)各個接口內部實現通過參數個數和參數類型實現重載,以下只對add_listener系列接口給出說明

  2)add_listener_with_delegate接受的參數直接是一個cs側的委托對象,在lua側不做任何特殊處理。對應上述的使用方式三

  3)add_listener_with_func接受參數是一個cs側的對象,和一個函數,內部使用這兩個信息創建閉包,傳遞給cs側的是一個LuaFunction作為回調。對應上述的使用方式一和使用方式二

  4)add_listener_with_reflection接受的是一個cs側的對象,外加一個cs側的函數,或者是函數的名字和參數列表。對應的是使用方式四

  add_listener_with_delegate最簡單;add_listener_with_func通過創建閉包,再將閉包函數映射到cs側委托類型來創建委托;add_listener_with_reflection通過反射動態創建委托。所有接口的共通點就是想辦法去創建委托,只是來源不一樣。下面着重看下后兩種方式是怎么實現的。

  對於反射創建委托,相對來說要簡單一點,helper.new_callback最終會調用到XLuaHelper.cs中去,相關代碼如下:

 

復制代碼
 1 // cs.XLuaHelper
 2 // 說明:創建委托
 3 // 注意:重載函數的定義順序很重要:從更具體類型(Type)到不具體類型(object),xlua生成導出代碼和lua側函數調用匹配時都是從上到下的,如果不具體類型(object)寫在上面,則永遠也匹配不到更具體類型(Type)的重載函數,很坑爹
 4 public static Delegate CreateActionDelegate(Type type, string methodName, params Type[] paramTypes)
 5 {
 6     return InnerCreateDelegate(MakeGenericActionType, null, type, methodName, paramTypes);
 7 }
 8 
 9 public static Delegate CreateActionDelegate(object target, string methodName, params Type[] paramTypes)
10 {
11     return InnerCreateDelegate(MakeGenericActionType, target, null, methodName, paramTypes);
12 }
13 
14 public static Delegate CreateCallbackDelegate(Type type, string methodName, params Type[] paramTypes)
15 {
16     return InnerCreateDelegate(MakeGenericCallbackType, null, type, methodName, paramTypes);
17 }
18 
19 public static Delegate CreateCallbackDelegate(object target, string methodName, params Type[] paramTypes)
20 {
21     return InnerCreateDelegate(MakeGenericCallbackType, target, null, methodName, paramTypes);
22 }
23 
24 delegate Type MakeGenericDelegateType(params Type[] paramTypes);
25 static Delegate InnerCreateDelegate(MakeGenericDelegateType del, object target, Type type, string methodName, params Type[] paramTypes)
26 {
27     if (target != null)
28     {
29         type = target.GetType();
30     }
31 
32     BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
33     MethodInfo methodInfo = (paramTypes == null || paramTypes.Length == 0) ? type.GetMethod(methodName, bindingFlags) : type.GetMethod(methodName, bindingFlags, null, paramTypes, null);
34     Type delegateType = del(paramTypes);
35     return Delegate.CreateDelegate(delegateType, target, methodInfo);
36 }
復制代碼

 

  這部分代碼就是利用反射創建委托類型,xlua作者在lua代碼中也有實現。接下來的是怎么利用LuaFunction去創建委托,看下XLuaMesseneger.cs中創建委托的代碼:

 

復制代碼
 1 public static Dictionary<string, Type> MessageNameTypeMap = new Dictionary<string, Type>() {
 2     // UIArena測試模塊
 3     { MessageName.MN_ARENA_PERSONAL_PANEL, typeof(Callback<ArenaPanelData>) },//導出測試
 4     { MessageName.MN_ARENA_UPDATE, typeof(Callback<ArenaPanelData>) },//緩存委托測試
 5     { MessageName.MN_ARENA_BOX, typeof(Callback<int>) },//反射測試
 6 };
 7 
 8 
 9 [LuaCallCSharp]
10 public static List<Type> LuaCallCSharp = new List<Type>() {
11     // XLuaMessenger
12     typeof(XLuaMessenger),
13     typeof(MessageName),
14 };
15 
16 [CSharpCallLua]
17 public static List<Type> CSharpCallLua1 = new List<Type>() {
18 };
19 
20 // 由映射表自動導出
21 [CSharpCallLua]
22 public static List<Type> CSharpCallLua2 = Enumerable.Where(MessageNameTypeMap.Values, type => typeof(Delegate).IsAssignableFrom(type)).ToList();
23 
24 public static Delegate CreateDelegate(string eventType, LuaFunction func)
25 {
26     if (!MessageNameTypeMap.ContainsKey(eventType))
27     {
28         Debug.LogError(string.Format("You should register eventType : {0} first!", eventType));
29         return null;
30     }
31     return func.Cast(MessageNameTypeMap[eventType]);
32 }
復制代碼

 

  我這里用消息類型(String)和消息對應的委托類型做了一次表映射,lua側傳遞LuaFunction過來時,通過消息類型就可以知道要Cast到什么類型的委托上面。而xlua中的原理是導出的委托類型存為列表,當LuaFunction要映射到委托類型時,遍歷這張表找一個參數類型匹配的委托進行映射。

  其它的應該都比較簡單了,XLuaMessenger.cs是對Messenger.cs做了擴展,使其支持object類型參數,主要是提供對Lua側發送消息的支持,截取其中一個函數來做下展示:

 

復制代碼
 1 public static void Broadcast(string eventType, object arg1, object arg2)
 2 {
 3     Messenger.OnBroadcasting(eventType);
 4 
 5     Delegate d;
 6     if (Messenger.eventTable.TryGetValue(eventType, out d))
 7     {
 8         try
 9         {
10             Type[] paramArr = d.GetType().GetGenericArguments();
11             object param1 = arg1;
12             object param2 = arg2;
13             if (paramArr.Length >= 2)
14             {
15                 param1 = CastType(paramArr[0], arg1) ?? arg1;
16                 param2 = CastType(paramArr[1], arg2) ?? arg2;
17             }
18             d.DynamicInvoke(param1, param2);
19         }
20         catch (System.Exception ex)
21         {
22             Debug.LogError(string.Format("{0}:{1}", ex.Message, string.Format("arg1 = {0}, typeof(arg1) = {1}, arg2 = {2}, typeof(arg2) = {3}", arg1, arg1.GetType(), arg2, arg2.GetType())));
23             throw Messenger.CreateBroadcastSignatureException(eventType);
24         }
25     }
26 }
復制代碼

 

四  xlua動態庫構建

  要說的重點就這些,需要說明的一點是,這里並沒有把項目中所有的東西放上來,因為xlua的熱更真的和被熱更的cs項目有很大的直接牽連,還是拿篇頭那個委托熱更的例子做下說明:如果你cs項目代碼規范就就已經支持了xlua熱更,那本文中很多關於委托熱更的討論你根本就用不上。但是這里給的代碼組織結構和解決問題的思路還是很有參考性的,實踐時你項目中遇到某些難以熱更的模塊,可以參考這里消息系統的設計思路去解決。

  另外,之前看xlua討論群里還有人問怎么構建xlua動態庫,或者怎么集成第三方插件。這個問題可以參考我的另一篇博客:Unity3D跨平台動態庫編譯---記kcp基於CMake的各平台構建實踐。這里有kcp的構建,其實這是我第一次嘗試去編譯Unity各平台的動態庫經歷,整個構建都是參考的xlua構建工程,你看懂並實踐成功了kcp的構建,那么xlua的也會了。

 

五  工程項目地址

  github地址在:https://github.com/smilehao/xlua-framework

 

博客:http://www.cnblogs.com/SChivas/

倉庫:https://github.com/smilehao/

郵箱:703016035@qq.com

感謝您的閱讀,如果您覺得本文對您有所幫助,請點一波推薦。

歡迎各位點評轉載,但是轉載文章之后務必在文章頁面中給出作者和原文鏈接,謝謝。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM