熱更新解決方案–tolua學習筆記
一.tolua使用準備工作:從GitHub上下載tolua(說明:這篇筆記使用的Unity版本是2019.4.18f1c1,使用的tolua是2021年4月9日從GitHub上Clone的tolua工程文件,沒有下載release版本,使用的ide為vscode)
1.在GitHub上搜索tolua,點擊下圖搜索結果中選中的項目進入:
2.進入後Clone項目或者下載右邊的release發佈出來的tolua,這裡我選擇的是Clone代碼:
3.導入tolua,將下載的tolua工程中的相關文件複製到自己的工程中。
將Luajit和Luajit64這兩個目錄複製。注意:這兩個目錄複製前和Assets目錄同級,複製後也應該和Assets目錄同級。
將Assets目錄下的所有文件夾複製到自己的工程文件中Assets目錄下。
打開Unity,會出現一個選擇框,選擇左邊按鈕。
接下來出現自動生成的選擇框,點擊確定。
成功導入後上方菜單欄出現Lua菜單,如果剛才不小心選擇了取消,這裡也可以選擇Generate All重新生成。
4.其他功能腳本導入,主要是AB包工具(在Unity中通過PackageManager導入AB包工具,導入方法詳見:熱更新基礎–AssetBundle學習筆記)和AB包管理器(為了方便加載AB包,我們可以製作一個AB包的管理器腳本,腳本詳見:熱更新基礎–AssetBundle學習筆記)
二.C#調用lua
1.在C#中使用lua代碼,和xlua類似,tolua也有一個解析器類用於執行lua代碼
void Start() { //初始化tolua解析器 LuaState state = new LuaState(); //啟動解析器 state.Start(); //執行lua代碼 state.DoString("print('歡迎來到tolua')"); //執行代碼時可以自定義代碼出處,方便查看代碼問題 state.DoString("print('hello tolua')","CSharpCallLua.cs"); //執行lua文件,執行文件時後綴可加可不加 state.DoFile("Main"); //執行lua文件,不添加文件後綴 state.Require("Main"); //檢查解析器棧頂是否為空 state.CheckTop(); //銷毀解析器 state.Dispose(); //置空 state = null; }
和xlua對比,解析器的使用方法類似。tolua解析器對象需要Start方法啟動後使用,xlua直接使用解析器對象;tolua提供了DoString方法執行單句lua代碼、DoFile方法執行lua文件(後綴可選)、Require方法執行lua文件(不加後綴),而xlua只提供了DoString方法執行單句lua代碼,要執行lua文件需要使用DoString方法執行lua中的require語句;tolua銷毀解析器時必須先執行CheckTop方法再Dispose方法銷毀,xlua直接就可以銷毀;其次xlua還提供了Tick方法進行垃圾回收。總體來看,xlua和tolua解析器使用是基本相同,只是名稱不同而已。
2.tolua解析器自定義路徑
void Start() { LuaState state = new LuaState(); state.Start(); //默認執行lua文件路徑是Lua文件夾下的文件,如果是lua文件夾下的子文件,通過Lua文件夾下相對目錄執行 //state.Require("CSharpCallLua/LoaderTest"); //如果是其他路徑,可以通過AddSearchPath方法添加搜索路徑,添加的是絕對路徑(這裡Lua文件夾下的目錄也可以) state.AddSearchPath(Application.dataPath + "/Lua/CSharpCallLua"); state.DoFile("LoaderTest.lua"); //有添加路徑的方法也有移除路徑的方法,但是實際應用中基本沒有移除路徑的需求 state.RemoveSeachPath(Application.dataPath + "/Lua/CSharpCallLua"); }
和xlua對比,xlua中並沒有提供自定義添加解析路徑的方法,tolua提供了這個方法。這種添加解析路徑的方法在實際使用中有局限性,主要是無法從AB包中解析,但是實際應用時自己寫的lua代碼都需要打到AB包中。xlua中直接添加解析lua文件的方法,我們稱為重定向,這個重定向的方法根據從外部傳入的文件名(注意這個參數是ref的,最好不要修改以方便後續的委託方法執行)讀取文件最後返回byte數組,一旦得到一個不為空的返回值即終止後續重定向方法的執行;而tolua中也可以實現自定義解析方法(就是xlua中的重定向),這樣就可以解決如何從AB包中讀取lua文件的問題。
3.自定義解析方式(重定向)
首先我們看到tolua中的LuaFileUtils類,這個類是單例模式的,在其中有一個可以被子類重寫的方法ReadFile,這就是根據lua文件名解析lua文件的核心方法(系統自定義的lua解析路徑和我們添加的lua解析路徑都會存儲到一個list集合中,ReadFile方法會調用FindFile方法去解析路徑,FindFile方法核心代碼塊是遍歷lua解析路徑的list,然後看list中是否有相應的文件),所以可以通過重寫這個方法實現自定義解析方式
1)創建一個類繼承LuaFileUtils,重寫其中的ReadFile方法,在這個方法中自定義自己的解析方式。
/// <summary> /// 繼承LuaFileUtils,tolua會調用這個類中的ReadFile方法讀取lua文件,而且這個方法是virtual的,支持重寫 /// </summary> public class CustomToluaLoader : LuaFileUtils { /// <summary> /// 重寫ReadFile方法自定義解析lua文件的方式 /// </summary> /// <param name="fileName"></param> /// <returns></returns> public override byte[] ReadFile(string fileName) { //可以先調用父類解析器加載 //byte[] bytes = base.ReadFile(fileName); //校驗父類解析器是否解析到了,如果父類方法沒有找到,再使用自定義的解析 //if(bytes.Length != 0) // return bytes; //保證後綴必須是.lua if(!fileName.EndsWith(".lua")) fileName += ".lua"; //從AB包中加載 //拆分路徑,得到文件名 string[] strs = fileName.Split('/'); //加載AB包文件 TextAsset luaFile = AssetBundleManager.Instance.LoadRes<TextAsset>("lua",strs[strs.Length-1]); //校驗是否加載到文件 if(luaFile != null) return luaFile.bytes; //從Resources文件下加載,tolua可以一鍵將文件拷貝到Resources目錄中的Lua文件夾下 string path = "Lua/" + fileName; TextAsset text = Resources.Load<TextAsset>(path); //加載資源 Resources.UnloadAsset(text); //校驗是否加載到文件 if(text != null) return text.bytes; return null; } }
2)為了使子類重寫的方法得到調用,由於父類是單例模式實現的,而且上圖中還可以看到父類單例的set方法是protexted的,所以在執行lua文件前我們需要new一下子類,目的是讓父類的instance先存儲一個子類對象,這樣調用時實際就是調用的子類重寫的方法。
void Start() { LuaState state = new LuaState(); state.Start(); //加載lua文件前new以下自定義的CustomToluaLoader類,使其父類LuaFileUtils的instance單例保存的是子類對象,這樣才能執行子類自定義的方法 new CustomToluaLoader(); //如果從Resources目錄下加載,在執行文件前記得要將文件複製到Resources目錄下,所有文件會保存在Resources目錄下的Lua文件夾中,填寫這個文件在Lua文件夾中的相對路徑 state.Require("CSharpCallLua/LoaderTest"); }
3)執行結果:沒有報錯
總結:和xlua對比,明顯tolua的重定向方式更加麻煩,xlua的加載方式可以分開,可以添加多個自定義的加載方法,每種方式一個方法,但是tolua的所有加載方式都必須在同一個方法(重寫的ReadFile方法)中實現。我們這裡自定義了兩種加載方式,一種是從Resources目錄下加載(一般用於加載tolua的框架中的lua代碼,系統自動加載的),一種是從AB包中加載(用於加載自己寫的lua代碼)。在使用這兩種方式加載lua文件時,有一些細節問題還需要注意:
1)從Resources目錄下加載時,tolua定義了編輯器可以實現一鍵將所有lua文件遷移到Resources目錄下,遷移後的lua文件都保存在Resources下的Lua目錄下,如下圖:
2)從AB包中加載lua文件時,AB包管理器打包會和tolua的生成代碼有衝突,因此需要先清除tolua生成代碼再打AB包,打包後重新生成代碼,清除AB包時會彈出自動生成的選擇框,記得選擇取消(不然又自動生成了代碼,清楚了個寂寞),下面分別是清除代碼、彈出自動生成框和重新生成代碼的選擇:
4.tolua解析器管理器:和xlua類似,我們可以提供一個tolua的解析器管理器,封裝一下xlua解析器,實現更方便調用xlua。
/// <summary> /// 管理唯一的tolua解析器 /// </summary> public class LuaManager : MonoBehaviour { //持有的全局唯一的解析器 private LuaState luaState; //提供給外部訪問的解析器 public LuaState LuaState{ get{ return luaState; } } //單例模塊,需要繼承MonoBehaviour的單例,自動創建空物體並掛載自身 private static LuaManager instance; public static LuaManager Instance { get { //如果沒有單例,自動創建一個空物體並掛載腳本,設置過場景不移除 if (instance == null) { GameObject obj = new GameObject("LuaManager"); DontDestroyOnLoad(obj); instance = obj.AddComponent<LuaManager>(); } return instance; } } //在Awake中初始化解析器 private void Awake() { Init(); } /// <summary> /// 初始化解析器方法,為解析器賦值 /// </summary> private void Init(){ //自定義解析路徑,建議開發時注釋掉這段代碼,打包時取消注釋 //new CustomToluaLoader(); luaState = new LuaState(); luaState.Start(); } /// <summary> /// 提供給外部執行單句lua代碼 /// </summary> /// <param name="luaCode">lua代碼</param> /// <param name="chunkName">lua代碼出處</param> public void DoString(string luaCode,string chunkName = "LuaManager.cs"){ //判空 if(luaState == null) Init(); luaState.DoString(luaCode,chunkName); } /// <summary> /// 提供給外部執行lua文件的方法 /// 只封裝require,不提供dofile加載(require加載不會重複執行lua代碼) /// </summary> /// <param name="fileName"></param> public void Require(string fileName){ //判空 if(luaState == null) Init(); luaState.Require(fileName); } public void Dispose(){ //校驗是否為空,解析器為空就不用再執行了 if(luaState == null) return; luaState.CheckTop(); luaState.Dispose(); //需要置空,不置空還會在棧內存儲引用 luaState = null; } }
和xlua解析器管理器相比,tolua的管理器更加複雜一些。tolua解析器是繼承mono的單例模式,xlua解析器是普通單例模式。這個解析器並不完善,後續學習過程中還需要繼續完善。
5.使用lua解析器管理器調用lua代碼。這是通過lua解析器調用lua地路徑,和xlua地方式相同,之後測試都使用這種方法調用就不再贅述。之後粘貼的lua代碼都是Test.lua中的代碼。
1)在Unity中掛載腳本,在腳本中執行Main.lua腳本。
void Start() { LuaManager.Instance.Require("Main"); }
2)將Main.lua作為所有lua腳本地主入口,在這個腳本中在調用各種其他lua腳本執行lua代碼。
print("do Main.lua succeed") --啟動測試腳本 require("CSharpCallLua/Test")
3)被Main.lua啟動地測試腳本。
print("do Test.lua succeed")
4)執行結果,可以看到兩個腳本都成功執行
6.C#中獲取lua的變量
print("do Test.lua succeed") --全局變量 testNumber = 1 testBool = true testFloat = 4.5 testString = "movin" --局部變量 local testLocal = 57
void Start() { LuaManager.Instance.Require("Main"); //獲取全局變量 //lua解析器提供了索引器直接訪問全局變量,這裡的LuaState不是類名,是LuaManager中的luaState解析器對象對應的屬性 Debug.Log(LuaManager.Instance.LuaState["testNumber"]); Debug.Log(LuaManager.Instance.LuaState["testFloat"]); Debug.Log(LuaManager.Instance.LuaState["testBool"]); Debug.Log(LuaManager.Instance.LuaState["testString"]); //得到的全局變量存儲為object類型,使用Convert類中的靜態方法轉換類型 //值拷貝,無法通過修改轉存的變量值修改lua中變量值,但是索引器提供了set方法修改 int value = Convert.ToInt32(LuaManager.Instance.LuaState["testNumber"]); value = 100; Debug.Log(LuaManager.Instance.LuaState["testNumber"]); LuaManager.Instance.LuaState["testNumber"] = 101; Debug.Log(LuaManager.Instance.LuaState["testNumber"]); //還可以使用索引器為lua新加全局變量 LuaManager.Instance.LuaState["newNumber"] = 56; Debug.Log(LuaManager.Instance.LuaState["newNumber"]); //本地變量無法獲取 Debug.Log(LuaManager.Instance.LuaState["testLocal"]); }
與xlua對比,在xlua中通過獲取_G表對象然後通過get和set方法讀取lua中的變量,而tolua直接通過索引值訪問,其實相當於將_G表進行了封裝,顯然tolua使用更為方便,但是tolua也存在缺陷,xlua可以通過泛型指定類型,tolua獲取的值是object類型,還需要轉換類型。
7.C#使用lua中的函數(無參無返回)
print("do Test.lua succeed") --定義函數 --無參無返回 function testFun() print("無參無返回") end
void Start() { LuaManager.Instance.Require("Main"); //方法一:通過GetFunction方法獲取 LuaFunction function = LuaManager.Instance.LuaState.GetFunction("testFun"); //使用Call方法執行 function.Call(); //使用完成後需要銷毀 function.Dispose(); //方法二:通過索引器獲取函數,需要轉型 function = LuaManager.Instance.LuaState["testFun"] as LuaFunction; //執行方法相同 function.Call(); function.Dispose(); //使用改進:轉化為委託使用,其實就是將得到的LuaFunction對象轉換為委託 function = LuaManager.Instance.LuaState["testFun"] as LuaFunction; //使用ToDelegate方法將LuaFunction對象轉換為委託 UnityAction action = function.ToDelegate<UnityAction>(); action(); function.Dispose(); action = null; }
注意:要想在C#中使用委託轉存LuaFunction對象,需要初始化tolua中的委託工廠,否則委託無法使用。在Lua解析器管理器類LuaManager中的Init方法中添加初始化委託工廠的代碼:
private void Init(){ //自定義解析路徑,建議開發時注釋掉這段代碼,打包時取消注釋 //new CustomToluaLoader(); luaState = new LuaState(); luaState.Start(); //初始化委託工廠,沒有初始化無法使用委託 DelegateFactory.Init(); }
8.C#使用lua中的函數(有參數,有返回)
print("do Test.lua succeed") --定義函數 --有參有返回 function testFun2(a) print("有參有返回") return a + 10 end
void Start() { LuaManager.Instance.Require("Main"); //方法一:通過luaFunction的Call方法執行 LuaFunction function = LuaManager.Instance.LuaState["testFun2"] as LuaFunction; //開始調用 function.BeginPCall(); //傳遞參數 function.Push(234); //得到返回值 function.PCall(); //得到返回值 int result = (int)function.CheckNumber(); Debug.Log(result); //執行結束 function.EndPCall(); //方法二:通過luaFunction的Invoke方法執行 //最後一個泛型為返回值類型,前面的泛型指定參數類型 result = function.Invoke<int,int>(78); Debug.Log(result); //方法三:使用委託轉存,執行委託 Func<int,int> func = function.ToDelegate<Func<int,int>>(); result = func(645); Debug.Log(result); function.Dispose(); //方法四:直接執行 //使用LuaState的Invoke成員方法執行 Debug.Log(LuaManager.Instance.LuaState.Invoke<int,int>("testFun2",400,true)); }
9.C#使用lua中的函數(多返回值)
print("do Test.lua succeed") --定義函數 --多返回值 function testFun3(a) print("多返回值") return a-10,a,a+10,a>0 end
public delegate int CustomDelegate(int a,out int a2,out int a3,out bool b1); public class CallFunction : MonoBehaviour { void Start() { LuaManager.Instance.Require("Main"); //方法一:通過Call調用 LuaFunction function = LuaManager.Instance.LuaState["testFun3"] as LuaFunction; //開啟使用 function.BeginPCall(); //傳遞參數 function.Push(3); //執行函數 function.PCall(); //得到多返回值 int a1 = (int)function.CheckNumber(); int a2 = (int)function.CheckNumber(); int a3 = (int)function.CheckNumber(); bool b1 = (bool)function.CheckBoolean(); //結束使用 function.EndPCall(); Debug.Log(a1 + "_" + a2 + "_" + a3 + "_" + b1); //方法二:通過out或者ref類型的委託接收,這裡測試了out,ref是一樣可以使用的 CustomDelegate customDelegate = function.ToDelegate<CustomDelegate>(); a1 = customDelegate(100,out a2,out a3,out b1); Debug.Log(a1 + "_" + a2 + "_" + a3 + "_" + b1); } }
注意:在tolua中使用自定義的委託時,除了要初始化委託工廠類,還需要將自定義的委託註冊到tolua中。
1)打開Editor目錄下的CustomSetting類
2)找到customDelegateList變量,在這個數組中添加自定義的委託類型(下圖中選取的位置就是剛才代碼中使用的對返回值委託類型)
3)在Unity中生成相應的委託代碼,點擊Gen Lua Delegates或者Generate All都可以
10.C#使用lua函數(變長參數)
print("do Test.lua succeed") --定義函數 --變長參數 function testFun4(a,...) print("變長參數") print(a) arg = {...} for k,v in pairs(arg) do print(k,v) end end
public delegate void CustomDelegate(int a,params object[] objs); public class CallFunction : MonoBehaviour { void Start() { LuaManager.Instance.Require("Main"); //方法一:通過自定義委託執行變長參數 LuaFunction function = LuaManager.Instance.LuaState.GetFunction("testFun4"); CustomDelegate customDelegate = function.ToDelegate<CustomDelegate>(); customDelegate(100,true,false,"movin",12,3.5); //方法二:通過LuaFunction中的Call方法執行,沒有返回值可以使用這種方式,使用泛型指定參數類型 function.Call<int,bool,bool,string,int,float>(100,true,false,"movin",12,3.5f); } }
總結:
1)和xlua相比,tolua中C#使用lua函數的方法更多;
2)和剛才使用變量的思路類似,在xlua中,提供了LuaTable類和LuaFunction類對應lua中的表和函數,而lua中的所有全局變量都存儲在_G表中,因此xlua提供了一個特殊的可以直接訪問的_G表對象(LuaTable類對象),LuaTable類中提供了Get方法(可以指定泛型)和Set方法用於獲取和設置參數的值,所以我們可以通過_G表對象和其中的Get、Set成員方法訪問到lua中的變量;而tolua也可以理解為有類似的機制,但是又對_G表對象進行了進一步的封裝(我們看不到tolua中的_G表),從剛才的獲取變量和獲取各種函數來看,使用了CheckXXX系列方法來封裝變量的獲取,使用GetFunction方法來封裝函數的獲取,除了方法封裝還提供了索引器的getset方法封裝,所以獲取方式很多樣;
3)xlua中對於通過Get方法從表中獲得的不同類型的變量就可以使用C#中不同的類型接收,如果是lua中的number、boolean等基礎數據類型就使用對應的數據類型接收,如果是函數類型就使用委託接收,當然像lua中table類型可以使用數組、集合等接收(根據實際需求和表特點確定);tolua獲取到的函數類型是LuaFunction類型的,但是tolua和xlua中的LuaFunction類並不相同,它們有相同點(如使用後都需要dispose)也有不同點(比如tolua中的執行函數的方式更多),tolua獲取到的變量類型通過CheckXXX系列方法獲取,然後進行類型轉換後使用(得到的原始類型時object類型的);
4)獲取函數時,xlua不推薦使用LuaFunction類,推薦直接使用委託接收穫取的lua函數;tolua不論是通過GetFunction還是索引器獲取函數,都需要先存儲為LuaFunction類型,可以使用LuaFUnction直接執行函數(Invoke方法執行無返回值函數,Call方法執行無參無返回值函數,PCall系列方法各種函數都可以執行),也可以通過ToDelegate方法得到lua函數轉化的委託再執行委託;
5)不論時tolua還是xlua,自定義的委託類型或者其他類型都需要讓框架生成相應的代碼後使用。xlua使用[CSharpCallLua]和[LuaCallCSharp]兩個特性指定需要生成代碼的委託等C#類型,然後使用框架定義好的編輯器生成代碼;tolua則不是使用特性,而是將需要生成代碼的C#類型在CustomSettings類中指定,然後在使用前通過相應工廠的Init靜態方法來初始化(如使用委託前需要調用DelegateFactory類的靜態方法Init來初始化),最後再使用框架定義好的編輯器生成代碼。
11.C#使用lua中的table表現的list和dictionary
print("do Test.lua succeed") --list和dictionary --table表現的list testList = {1,3,5,7,8,9} testList2 = {"movin","加油",true,5,89.3} --table表現的dictionary testDic = { ["a"] = 1, ["b"] = 2, ["c"] = 4, ["s"] = 87, } testDic2 = { ["movin"] = 34, [true] = "hehehe", ["2"] = false, }
void Start() { LuaManager.Instance.Require("Main"); //通過LuaTable來獲取 //獲取table表現的list LuaTable table = LuaManager.Instance.LuaState.GetTable("testList"); Debug.Log(table[1]); Debug.Log(table[2]); Debug.Log(table[3]); Debug.Log(table[4]); Debug.Log(table[5]); LuaTable table2 = LuaManager.Instance.LuaState.GetTable("testList2"); Debug.Log(table2[1]); Debug.Log(table2[2]); Debug.Log(table2[3]); Debug.Log(table2[4]); Debug.Log(table2[5]); //遍歷,先將luatable轉換為object類型的數組,再遍歷 object[] objs = table.ToArray(); foreach (var item in objs) { Debug.Log("遍歷出來的" + item); } //引用拷貝,luatable中的值,lua中的值也會改變 table[1] = 99; Debug.Log(LuaManager.Instance.LuaState.GetTable("testList")[1]); //獲取table表現的dictionary LuaTable dic = LuaManager.Instance.LuaState.GetTable("testDic"); Debug.Log(dic["a"]); Debug.Log(dic["b"]); Debug.Log(dic["c"]); Debug.Log(dic["s"]); //LuaTable對象通過中括號得到值的方式中括號中的鍵只支持int和string //對於像bool類型的鍵,使用ToDicTable方法將LuaTable進行轉換後才能獲取值,通過泛型指定鍵值類型 LuaTable dic2 = LuaManager.Instance.LuaState.GetTable("testDic2"); LuaDictTable<object,object> luaDic2 = dic2.ToDictTable<object,object>(); Debug.Log(luaDic2["movin"]); Debug.Log(luaDic2[true]); Debug.Log(luaDic2["2"]); //LuaDictTable對象使用迭代器遍歷 IEnumerator<LuaDictEntry<object,object>> ie = luaDic2.GetEnumerator(); while(ie.MoveNext()){ Debug.Log(ie.Current.Key + "_" + ie.Current.Value); } //引用拷貝 dic["a"] = 8848; Debug.Log(LuaManager.Instance.LuaState.GetTable("testDic")["a"]); }
12.C#使用lua中的table
print("do Test.lua succeed") --自定義table testClass = { testInt = 2, testBool = false, testFloat = 1.5, testString = "movin", testFun = function() print("class中的函數") end }
void Start() { LuaManager.Instance.Require("Main"); //通過GetTable方法獲取 LuaTable table = LuaManager.Instance.LuaState.GetTable("testClass"); //訪問table中的變量 Debug.Log(table["testInt"]); Debug.Log(table["testBool"]); Debug.Log(table["testFloat"]); //獲取其中的函數 LuaFunction function = table.GetLuaFunction("testFun"); function.Call(); }
和xlua對比,tolua在表的使用上更加通用。xlua通過LuaTable類對象調用Get方法獲取table中的各種類型,調用Set方法設置參數的值,在lua解析器中提供了一個特殊的LuaTable類對象對應lua中的_G表,然後通過_G表對象獲取全局變量。Get方法可以通過泛型指定將lua中的全局變量映射到C#中的接收對象類型(類對象、接口、集合、函數及字符串、整型、浮點型等),對這些類型還需要使用特性生成代碼後使用。tolua將_G表封裝了起來,提供了GetTable、GetFunction等方法獲取lua中的變量,在C#端也提供了對應Lua端的類型對象,使用這些類型對象接收穫取到的lua變量,同時,提供了直接使用變量的方法和將變量轉化為C#類型的方法。最後,對lua中表的引用在tolua和xlua中都是地址引用,也就是在C#中將得到的類型對象值改變,lua的值一併改變。
13.C#使用lua中的協程
print("do Test.lua succeed") --定義協程,這些內容是tolua提供的協程 local coDelay = nil --開啟協程 StartDelay = function() coDelay = coroutine.start(Delay) end Delay = function() local c = 1 while true do --等待1s,執行一次 coroutine.wait(1) print("Count:"..c) c = c + 1 if c > 8 then StopDelay() break end end end --關閉協程 StopDelay = function() coroutine.stop(coDelay) end
void Start() { LuaManager.Instance.Require("Main"); //直接在lua中調用start函數就可以開啟協程 LuaFunction function = LuaManager.Instance.LuaState.GetFunction("StartDelay"); function.Call(); function.Dispose(); }
注意:C#使用lua中的協程時,tolua為我們提供了一種協程的書寫格式,這個知識點重點在lua端如何定義協程,C#端只需要開啟一個方法的調用。更重要的是,這樣直接調用並不能使協程跑起來,還需要在Unity中添加腳本,並將腳本和lua解析器綁定,這段代碼定義在LuaManager中的Init函數中:
//使用協程時需要添加一個腳本 LuaLooper loop = this.gameObject.AddComponent<LuaLooper>(); //將解析器和lualooper綁定 loop.luaState = luaState;
運行結果:
14.在C#調用lua代碼中常用的API類圖對比(tolua和xlua對比,前3張圖片為tolua,最後一張圖片為xlua,只是梳理了學習過程中用到的API,還可以繼續完善):
15.對比tolua和xlua在C#使用lua時的異同
1)在xlua中,我們可以簡單粗暴地在C#端獲取lua端地變量和方法等。xlua地lua運行環境類提供了一個LuaTable類型的Global屬性,這個屬性只提供了get方法,返回的就是_G表對象,對應了lua中的_G表。使用LuaTable類型的_G表對象的Get方法就可以獲取各種lua中的全局變量,Get方法使用泛型指定獲取的值類型,參數傳遞變量名稱字符串。可以說,我們可以使用這個單一的方法獲取各種全局變量,只是根據獲取到的對象的不同需要作一些不同的處理(一些自定義的對象需要生成代碼才能使用);一般情況下,有Get就應當有Set,使用Set方法在C#端也可以簡單粗暴地把值、方法、表等設置到lua中。
2)在tolua中,C#端使用lua端的變量和函數等的方式就要複雜一些,因為tolua將_G表封裝了起來,提供了固定的獲取各種類型參數的方法。LuaState運行環境類提供了索引器獲取各種變量,獲取到的類型是object類型的,需要轉換類型後使用;提供了GetFunction和GetTable方法獲取全局的函數和表。tolua中使用LuaTable和LuaFunction類對應lua中的表和函數(xlua中同樣有這兩個類,但是除了LuaTable地Get和Set方法很少使用),通過GetFunction和GetTable得到的函數和表也存儲為LuaFunction和LuaTable類型。LuaFunction類中提供了三種執行不同類型函數的方法,也提供了將自身轉化為C#的委託的方法;LuaTable類中提供了根據索引或者鍵名訪問和修改表中值的索引器,也提供了將對象自身轉化為C#中數組或者字典的方法,還提供了遍歷的方式。總的來說,tolua在LuaState類中提供了將全局變量取出的方法(索引器或者GetTable、GetFunction等),如果取出的是表或者函數,存儲為LuaTable或者LuaFunction類型,這兩種類型對象可以直接使用也可以轉化為C#中對應的對象使用;如果取出的是string或者bool等基本數據類型的變量,直接強轉使用。
3)不論是tulua還是xlua,得到的表都是引用類型。
4)不論是tolua還是xlua,委託、類等自定義的類型要想使用都需要生成代碼,只是兩者生成代碼的方式不同。xlua通過特性指定需要生成代碼的位置,tolua通過添加配置表指定需要生成代碼的位置。
5)xlua在lua運行環境類中提供了方便的AddLoader方法進行lua代碼重定向。tolua在lua運行環境類中提供了AddSearchPath類方便地添加讀取lua代碼地路徑,但是實際使用中往往要從AB包中讀取lua代碼,這種方式並不使用,但是tolua進行代碼重定向麻煩一些,通過繼承重寫的方式進行重定向。
三.lua調用C#
1.tolua調用C#的類,使用方法和xlua基本相同
print("do CallClass.lua succeed") --在tolua中訪問C#和xlua非常相似 --xlua使用CS.命名控件.類名,tolua使用命名空間.類名,相當於不寫CS local obj1 = UnityEngine.GameObject() local obj2 = UnityEngine.GameObject("movin") --可以定義全局變量存儲常用的C#類(取別名)以方便使用、節約性能 GameObject = UnityEngine.GameObject local obj3 = GameObject("movin2") --類中的靜態對象直接使用.調用,成員屬性使用.調用,成員方法使用:調用 local position = GameObject.Find("movin").transform.position print(position.x) local rigidbody = GameObject.Find("movin2"):AddComponent(typeof(UnityEngine.Rigidbody)) print(rigidbody) --如果發現使用過程中lua不認識這個類,檢查這個類是否添加到了自定義設置文件中並生成代碼 Debug = UnityEngine.Debug Debug.Log(position.y)
注意:與xlua類似的,在使用過程中如果出現不識別的類,tolua需要將類添加到配置文件中(xlua是添加特性),配置文件所在位置和添加位置如下圖:
上圖中鼠標選中的部分分別是配置文件和自己添加的類(Unity的Debug類需要自己添加),添加完成後不要忘記生成代碼。tolua麻煩的地方還有在使用Unity的類之前需要綁定lua解析器(LuaState對象),在LuaManager類中的Init方法中需要添加如下代碼:
//lua使用Unity中的類需要綁定 LuaBinder.Bind(luaState);
2.tolua調用C#的枚舉
/// <summary> /// 自定義枚舉 /// </summary> public enum E_MyEnum{ Idle, Move, Atk, }
print("do CallEnum.lua succeed") --枚舉調用規則和類的調用規則相同 --調用Unity自帶的枚舉 PrimitiveType = UnityEngine.PrimitiveType GameObject = UnityEngine.GameObject local obj = GameObject.CreatePrimitive(PrimitiveType.Cube) --調用自定義枚舉 local c = E_MyEnum.Idle print(c) --枚舉轉字符串 print(tostring(c)) --枚舉轉數字 print(c:ToInt()) print(E_MyEnum.Move:ToInt()) --數字轉枚舉 print(E_MyEnum.IntToEnum(2))
和xlua相比,使用方式基本相同,但是需要注意以下幾點:1)如果lua不能識別C#中的類或枚舉等,xlua是添加特性並生成代碼,tolua是在配置文件中添加相應的類或枚舉類型並生成代碼;2)在枚舉使用過程中枚舉對應的數字和枚舉的相互轉換兩者的處理方法並不相同;3)xlua提供了字符串轉枚舉的方法,tolua沒有提供;4)xlua直接print打印獲取到的枚舉類型打印出的是字符串,而tolua打印出的是userdata類型(tolua在存儲獲取到的枚舉是沒有轉換類型,存儲的還是C#中的枚舉)
3.tolua調用C#的數組
/// <summary> /// 自定義類中定義了一個數組 /// </summary> public class CustomClass{ public int[] array = new int[]{1,23,4,6,4,5}; }
print("do CallArray.lua succeed") --獲取類 local customClass = CustomClass() --獲取類中的數組,按照C#的規則使用 print(customClass.array.Length) print(customClass.array[1]) --查找元素 print(customClass.array:IndexOf(3)) --遍歷 for i = 0,customClass.array.Length - 1 do print("position "..i.." in array is "..customClass.array[i]) end --tolua比xlua多了一些遍歷方式 --迭代器遍歷 local iter = customClass.array:GetEnumerator() while iter:MoveNext() do print("iter:"..iter.Current) end --轉成table遍歷 local t = customClass.array:ToTable() for i=1,#t do print("table:"..t[i]) end --創建數組 local array2 = System.Array.CreateInstance(typeof(System.Int32),7) print(array2.Length) print(array2[0]) array2[0] = 99 print(array2[0])
4.tolua使用C#的list和dictionary
/// 自定義類中定義了一個數組 /// </summary> public class CustomClass{ public int[] array = new int[]{1,23,4,6,4,5}; public List<int> list = new List<int>(); public Dictionary<int,string> dic = new Dictionary<int, string>(); }
print("do CallListDic.lua succeed") local customClass = CustomClass() --使用list --向list中添加元素 customClass.list:Add(2) customClass.list:Add(45) customClass.list:Add(87) --獲取元素 print(customClass.list[1]) --長度 print(customClass.list.Count) --遍歷 for i = 0,customClass.list.Count - 1 do print(customClass.list[i]) end --創建list --tolua對泛型支持不好,需要自己在配置文件中添加對應的泛型類型生成後才能使用 --如List<string>、List<int>等等需要一一添加 local list2 = System.Collections.Generic.List_string() list2:Add("movin") print(list2[0]) --使用dictionary customClass.dic:Add(1,"movin") customClass.dic:Add(2,"乾飯人") customClass.dic:Add(3,"乾飯魂") --獲取值 print(customClass.dic[2]) --遍歷 --tolua中不支持使用lua的pairs遍歷方式進行遍歷,需要使用迭代器 local iter = customClass.dic:GetEnumerator() while iter:MoveNext() do local v = iter.Current print(v.Key,v.Value) end local keyIter = customClass.dic.Keys:GetEnumerator() while keyIter:MoveNext() do print(keyIter.Current,customClass.dic[keyIter.Current]) end local valueIter = customClass.dic.Values:GetEnumerator() while valueIter:MoveNext() do print(valueIter.Current) end --創建dictionary local dic2 = System.Collections.Generic.Dictionary_int_string() dic2:Add(4,"movin") print(dic2[4]) --如果鍵是字符串,tolua不能通過索引器訪問值,可以使用TryGetValue方法 local dic3 = System.Collections.Generic.Dictionary_string_int() dic3:Add("movin",455) local b,v = dic3:TryGetValue("movin",nil) print(v)
和xlua對比,使用上基本一致,都是調用C#的相關方法使用就可以了,區別主要在於:1)xlua和tolua的遍歷方式不同,不論是數組、列表還是字典,tolua一般使用迭代器遍歷,而xlua使用lua的pairs方式遍歷;2)創建list或者dictionary時,tolua的寫法和xlua的寫法不一樣,但是兩者都遵循各自的固定寫法;3)對於泛型的支持tolua非常差,如果要使用泛型定義list或者dictionary,需要在配置文件中配置自己使用的泛型類型,並生成代碼。
5.拓展方法
public static class Tools{ public static void Move(this CallFunctions cfs){ Debug.Log(CallFunctions.name + " is moving"); } } public class CallFunctions{ public static string name = "movin"; public void Speak(string str){ Debug.Log(str); } public static void Eat(){ Debug.Log(name + " is eating"); } }
print("do CallFunction.lua succeed") --靜態方法使用.執行 CallFunctions.Eat(); --成員方法使用冒號執行 local callFunctions = CallFunctions() callFunctions:Speak("movin move") --如果使用拓展方法,需要在配置文件中配置 callFunctions:Move()
注意:使用拓展方法時,在C#中的配置文件中進行的配置不太一樣,如下圖:
與xlua對比,xlua中使用拓展方法加上特性生成代碼即可,和其他特性的添加相同,而tolua則需要添加不太一樣的代碼配置。使用方法上都是把拓展方法當作成員方法使用即可。
6.tolua使用C#的ref和out參數方法
public int RefFun(int a,ref int b,ref int c,int d){ b = a + d; c = a - d; return 100; } public int OutFun(int a,out int b,out int c,int d){ b = a + d; c = a - d; return 200; } public int RefOutFun(int a,out int b,ref int c,int d){ b = a + d; c = a - d; return 300; }
print("do CallFunction.lua succeed") --通過多返回值的方式使用ref和out --第一個返回值為默認返回值,之後返回值是ref和out的返回值 --out參數使用任意值佔位,ref參數需要傳遞 local obj = CallFunctions() print(obj:RefFun(12,32,42,1)) print(obj:OutFun(12,0,0,5)) print(obj:RefOutFun(12,0,43,5))
和xlua對比,使用基本相似,區別在於xlua中out參數省略,而tolua中out參數任意值佔位(nil都可以),但是不能省略。此外,如果出現使用ref或out都可以的情況,推薦在tolua中使用out(官方沒有講到ref的使用,只講到了out的使用)。
7.tolua使用C#中的重載函數
public class CallFunctions{ public int Calc(){ return 100; } public int Calc(int a){ return a; } public string Calc(string a){ return a; } public int Calc(int a,int b){ return a + b; } public int Calc(int a,out int b){ b = 10; return a + b; } }
print("do CallFunction.lua succeed") --使用重載方法 local obj = CallFunctions() --lua中只有Number一種數值類型,所以xlua和tolua都對C#中的整型、浮點型等重載支持不好 print(obj:Calc()) print(obj:Calc(1)) print(obj:Calc(1.4)) print(obj:Calc("123")) --對於同樣類型參數的兩個重載函數,一個參數有out,一個參數沒有out --根據參數的值確定調用的函數,out參數使用nil佔位,非out參數不使用nil print(obj:Calc(13,24)) print(obj:Calc(13,nil)) --官方不推薦使用ref參數,這裡如果是ref參數和沒有ref參數的重載不能分清是一個重要原因
和xlua對比,tolua中重載函數並不需要特別聲明,像其他函數一樣傳遞參數調用就好,只是應該調用哪個重載是需要tolua去分辨的,由於lua和C#的差異性,導致重載函數使用過程中產生了兩個問題:一是lua中只有Number一種數值類型,而C#中有浮點型、整型等總共超過十種數值類型,C#中數值類型的重載函數該調用哪一個lua是分不清楚的;二是ref類型參數和沒有ref類型參數的問題,對於out類型和非out類型,可以通過傳遞nil值來區分,但是ref類型參數本來就需要初始化,和非ref類型參數的重載無法分辨。在xlua中重載函數的調用非常相似,只是out參數不用傳遞值,但是這兩個問題同樣存在,不過xlua提供了通過反射讓lua分清楚數值類型重載的方式,效率低就是了。
8.tolua使用C#中委託和事件
public class CallDelegates{ public UnityAction actions; public event UnityAction events; public void DoAction(){ if(actions != null) actions(); } public void DoEvent(){ if(events != null) events(); } public void ClearEvent(){ events = null; } }
print("do CallDelegate.lua succeed") --定義函數存儲到委託中 local obj = CallDelegates() local fun = function() print("lua function fun") end --第一次添加委託需要使用等號,之後使用+=(在lua中不支持+=,需要寫全) obj.actions = fun obj.actions = obj.actions + fun obj.actions = obj.actions + function() print("the third function in actions") end --tolua中無法直接執行委託,需要在C#中提供執行委託的方法 obj:DoAction() --添加函數使用+=,移除函數自然使用-= obj.actions = obj.actions - fun obj.actions = obj.actions - fun obj:DoAction() obj.actions = nil obj:DoAction() --tolua中事件的使用和委託基本相同,區別只在事件只能+=,不能= obj.events = obj.events + fun obj.events = obj.events + fun obj.events = obj.events + function() print("the third function in events") end obj:DoEvent() --移除事件和移除委託相同 obj.events = obj.events - fun obj.events = obj.events - fun obj:DoEvent() --清空事件,在C#中必須提供清空事件的方法 obj:ClearEvent() obj:DoEvent()
和xlua對比,委託的使用基本相同,只是tolua不能直接執行委託,需要在C#端提供執行委託的封裝方法,而xlua可以直接執行委託;事件的使用上,xlua和tolua添加和移除事件的方式不同,執行事件的方式基本相同(都要在C#中提供方法封裝,執行這個方法間接執行事件)。
9.tolua使用C#中的協程
print("do CallCoroutine.lua succeed") --記錄協程 local coDelay = nil --tolua提供了一些方便開啟協程的方法 StartDelay = function() --使用StartCoroutine方法開啟協程(tolua提供) coDelay = StartCoroutine(Delay) end --協程函數 Delay = function() local c = 1 while true do --使用WaitForSeconds方法等待(tolua提供) WaitForSeconds(1) --tolua還提供了其他的協程方法 --Yield(0) --WaitForFixedUpdate() --WaitForEndOfFrame() --Yield(返回值) print("Count:"..c) c = c + 1 if c > 6 then StopDelay() break end end end --停止協程函數 StopDelay = function() StopCoroutine(coDelay) coDelay = nil end --開始調用協程 StartDelay()
注意:要想在tolua中使用其定義的StartCoroutine等協程函數,需要註冊協程。在LuaManager中的Init函數中添加註冊代碼,添加後的Init函數如下:
private void Init(){ //自定義解析路徑,建議開發時注釋掉這段代碼,打包時取消注釋 //new CustomToluaLoader(); luaState = new LuaState(); luaState.Start(); //初始化委託工廠,沒有初始化無法使用委託 DelegateFactory.Init(); //使用協程時需要添加一個腳本 LuaLooper loop = this.gameObject.AddComponent<LuaLooper>(); //將解析器和lualooper綁定 loop.luaState = luaState; //使用tolua提供的協程方法,需要進行lua協程註冊 LuaCoroutine.Register(luaState,this); //lua使用Unity中的類需要綁定 LuaBinder.Bind(luaState); }
和xlua對比,tolua使用協程相對來說更加簡單,它為我們提供了一些協程函數可以直接調用,只要註冊協程後就可以使用,而xlua沒有為我們提供協程函數,在定義協程時只能使用lua提供的協程函數。
四.最後將最終版的LuaManager類粘貼到這裡(和前面的版本相比,主要在Init方法中增添了一些代碼)
/// <summary> /// 管理唯一的tolua解析器 /// </summary> public class LuaManager : MonoBehaviour { //持有的全局唯一的解析器 private LuaState luaState; //提供給外部訪問的解析器 public LuaState LuaState{ get{ return luaState; } } //單例模塊,需要繼承MonoBehaviour的單例,自動創建空物體並掛載自身 private static LuaManager instance; public static LuaManager Instance { get { //如果沒有單例,自動創建一個空物體並掛載腳本,設置過場景不移除 if (instance == null) { GameObject obj = new GameObject("LuaManager"); DontDestroyOnLoad(obj); instance = obj.AddComponent<LuaManager>(); } return instance; } } //在Awake中初始化解析器 private void Awake() { Init(); } /// <summary> /// 初始化解析器方法,為解析器賦值 /// </summary> private void Init(){ //自定義解析路徑,建議開發時注釋掉這段代碼,打包時取消注釋 //new CustomToluaLoader(); luaState = new LuaState(); luaState.Start(); //初始化委託工廠,沒有初始化無法使用委託 DelegateFactory.Init(); //使用協程時需要添加一個腳本 LuaLooper loop = this.gameObject.AddComponent<LuaLooper>(); //將解析器和lualooper綁定 loop.luaState = luaState; //使用tolua提供的協程方法,需要進行lua協程註冊 LuaCoroutine.Register(luaState,this); //lua使用Unity中的類需要綁定 LuaBinder.Bind(luaState); } /// <summary> /// 提供給外部執行單句lua代碼 /// </summary> /// <param name="luaCode">lua代碼</param> /// <param name="chunkName">lua代碼出處</param> public void DoString(string luaCode,string chunkName = "LuaManager.cs"){ //判空 if(luaState == null) Init(); luaState.DoString(luaCode,chunkName); } /// <summary> /// 提供給外部執行lua文件的方法 /// 只封裝require,不提供dofile加載(require加載不會重複執行lua代碼) /// </summary> /// <param name="fileName"></param> public void Require(string fileName){ //判空 if(luaState == null) Init(); luaState.Require(fileName); } public void Dispose(){ //校驗是否為空,解析器為空就不用再執行了 if(luaState == null) return; luaState.CheckTop(); luaState.Dispose(); //需要置空,不置空還會在棧內存儲引用 luaState = null; } }