前陣子剛剛集成xlua到項目,目的只有一個:對線上遊戲C#邏輯有Bug的地方執行修復,經過考察xlua和tolua,最終選擇了xlua,很大部分緣由是由於項目已經到了後期,線上版本迭代了好幾回,因此引入Lua的目的不是爲了開發新版本模塊。xlua在咱們的這種狀況下非常適用,如xlua做者所說,用C#開發,用lua熱更,xlua這套框架爲咱們提供了諸多便利,至少我能夠說,在面臨一樣的狀況下,你用tolua去作一樣的事情是很費心的。可是若是你是想用xlua作整套客戶端遊戲邏輯的,這篇文對你可能就沒什麼借鑑意義了。其實純lua寫邏輯,使用xlua仍是tolua並非那麼重要,由於與c#交互會少不少,並且通常都是耗性能的地方纔放c#,即便網上有各類lua框架性能的評測,其實我感受意義都不太大,若是真要頻繁調用,那無論xlua仍是tolua你都要考慮方案去優化的。php
當時在作完這個xlua熱更框架,本打算寫篇博文分享一下。後來,因爲工做一直比較忙,這個事情就被擱淺了下來,另外,集成xlua時本身寫的代碼少得可伶,感受也沒什麼太多要分享的地方。畢竟熱修復,本質上來講就是一個輕量級的東西。除非你是新開的項目,一開始就遵循xlua熱更的各類規範。而若是你是後期引入的xlua,那麼,xlua熱修復代碼的複雜度,很大程度上取決於你框架原先c#代碼的寫法,好比說委託的使用,在c#側常常做爲回調去使用,xlua的demo裏對委託的熱修復示例是這樣的:html
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就能訪問,很簡單。而問題就在於,咱們項目以前對委託的使用方式是這樣的:git
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熱更規則的相關代碼規範,從而修復困難。github
這個例子可能舉得不是太好,你能夠暴力修改項目中全部這樣寫法的地方(只要你樂意- -),另外,下面的這種寫法有GC問題,這個問題是項目歷史遺留下來的。編程
當初在集成xlua到項目時,發現現行網絡上對xlua的大多分享,沒有直接命中我所面臨的問題,有實際借鑑意義的項目很少,對不少分享來講:c#
1)體積過重:集成了各類資源熱更新、場景管理、音樂管理、定時器管理等等邊緣模塊,xlua內容反而顯得過輕。緩存
2)拈輕怕重:簡單集成xlua,而後本身用NGUI或者UGUI寫了個小demo,完事。網絡
其實說是xlua的一個擴展更加貼切,對xlua沒有提供的一些外圍功能進行了擴展。xlua的設計仍是挺不錯的,相比tolua的代碼讀起來仍是要清爽多了。數據結構
我假設你已經清楚了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側傳遞委託形參
對象建立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 }
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使用迭代器的工做機制是有點雷同的,若是你清楚這個機制,那麼這裏的原理就不難理解。
先看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 -- 一、若是使用cs側函數,必須將最後一個參數的回調(cs側定義爲委託)導出到[CSharpCallLua] 28 -- 二、用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做者已經對協程作了比較好的支持,不須要咱們另外去操心太多。
這裏迴歸的是篇頭所闡述的問題,當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 -- 一、緩存委託 4 -- 二、Lua綁定(其實是建立LuaFunction再cast到delegate),須要在委託類型上打[CSharpCallLua]標籤--推薦 5 -- 三、使用反射再執行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 -- 一、使用反射建立委託---這裏無法直接使用,返回的是Callback<,>類型,無法隱式轉換到CS.GameObjectPool.GetGameObjectDelegate類型 22 -- 二、再執行Lua綁定--須要在委託類型上打[CSharpCallLua]標籤 23 -- 注意: 24 -- 一、使用反射建立的委託能夠直接在Lua中調用,但做爲參數時,必需要求參數類型一致,或者參數類型爲Delegate--參考Lua側消息系統實現 25 -- 二、正由於存在類型轉換問題,而CS側的委託類型在Lua中無法拿到,因此在Lua側執行類型轉換成爲了避免可能,上面才使用了Lua綁定 26 -- 三、對於Lua側無法執行類型轉換的問題,能夠在CS側去作,這就是[CSharpCallLua]標籤的做用,xlua底層已經爲咱們作好這一步 27 -- 四、因此,這裏至關於方式二多包裝了一層委託,從這裏能夠知道,委託作好所有打[CSharpCallLua]標籤,不然更新起來很受限 28 -- 五、對於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綁定:這種方式使用起來很受限,這裏再也不作說明,要了解的朋友本身參考源代碼。
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的熱更真的和被熱更的cs項目有很大的直接牽連,仍是拿篇頭那個委託熱更的例子作下說明:若是你cs項目代碼規範就就已經支持了xlua熱更,那本文中不少關於委託熱更的討論你根本就用不上。可是這裏給的代碼組織結構和解決問題的思路仍是頗有參考性的,實踐時你項目中遇到某些難以熱更的模塊,能夠參考這裏消息系統的設計思路去解決。
另外,以前看xlua討論羣裏還有人問怎麼構建xlua動態庫,或者怎麼集成第三方插件。這個問題能夠參考個人另外一篇博客:Unity3D跨平臺動態庫編譯---記kcp基於CMake的各平臺構建實踐。這裏有kcp的構建,其實這是我第一次嘗試去編譯Unity各平臺的動態庫經歷,整個構建都是參考的xlua構建工程,你看懂並實踐成功了kcp的構建,那麼xlua的也會了。