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的也會了。