1. 程式人生 > >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的也會了。