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/
感謝您的閱讀,如果您覺得本文對您有所幫助,請點一波推薦。
歡迎各位點評轉載,但是轉載文章之后務必在文章頁面中給出作者和原文鏈接,謝謝。

