您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關在ToLua框架中如何使用C#與Lua,文章內容質量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關知識有一定的了解。
Lua是目前國內使用最多的熱更語言,基于Lua的熱更框架也非常多,最近學習了一下ToLua的熱更框架,主要使用的問題在于C#和Lua之間的互調,因此做一下學習記錄以備后查。
所謂“互調”,當然要包括兩個方面,一是通過C#調用Lua代碼,二是通過Lua代碼調用C#腳本,第二點還包括注冊在C#腳本里的Unity物體。
ToLua框架主要是通過靜態綁定來實現C#與Lua之間的交互的,基本原理是通過建立一個Lua虛擬機來映射C#腳本,然后再通過這個虛擬機來運行Lua腳本,Lua腳本在運行時可以通過虛擬機反過來調用C#腳本里注冊過的物體,這種方式的優勢在于比起使用反射的uLua來說效率更高。
ToLua框架下可以將實現分成三大部分:普通的Unity+C#部分、ToLua虛擬機部分和Lua腳本部分,結構見下圖:
ToLua結構
目前國內需要熱更的手游一般都將主要的邏輯框架和組件功能用C#實現,而具體功能和調用放在Lua中,因為C#是不能被打包進AssetBundle中的,所以無法通過AssetBundle對代碼進行改動,但是Lua是即時編譯型語言,并且可以被打包進入AssetBundle中,在需要修改簡單功能時,將Lua代碼通過AssetBundle進行更新即可。
首先是下載地址:
ToLua
這是作者的github地址,進入以后點擊下載Zip,完成后解壓到自己需要的目錄,再用Unity打開即可。
點擊下載zip即可
第一次打開工程時會提示是否需要自動生成注冊文件,新手可以選擇直接生成,若選擇了取消,也可以在編輯器菜單中手動注冊。——這是一個非常重要的操作,后文也會提到。
下面開始關于使用的正文。
前面有提到過ToLua的基本實現方式,這里可以再細化一點:創建虛擬機——綁定數據——調用Lua代碼,這套步驟在框架自帶的Example里也非常清晰。
首先脫離Example實現一下這三個步驟。
ToLua虛擬機的創建非常簡單,只需要new一個LuaState即可,我們建立一個C#腳本作為入口,引用LuaInterface命名空間,輸入以下代碼,將文件掛載到場景中的一個空物體上即可。
using LuaInterface; using UnityEngine; public class LuaScene : MonoBehaviour { string luaString = @" print('這是一個使用DoString的Lua程序') "; string luaFile = "LuaStudy"; LuaState state; void Start() { state = new LuaState();//建立Lua虛擬機 state.Start();//啟動虛擬機 //使用string調用Lua state.DoString(luaString); //使用文件調用Lua //手動添加一個lua文件搜索地址 string sceneFile = Application.dataPath + "/LuaStudy"; state.AddSearchPath(sceneFile); state.DoFile(luaFile); state.Require(luaFile); state.Dispose();//使用完畢回收虛擬機 Debug.LogFormat("當前虛擬機狀態:{0}", null == state);//驗證虛擬機狀態 } }
這里使用的Lua腳本也非常簡單
print('這是一個使用DoFile的Lua程序')
Lua掛載
ToLua直接調用Lua代碼的方式有兩種,一種是DoString,另一種是DoFile;此外還有一個Require方法,這個方法和前兩個方法不同的是,ToLua會將調用的Lua文件載入Lua棧中,而前兩者只是運行一次,運行之后保存在緩存中,雖然也可以后續調用,但是會。
在上述代碼中要注意,使用DoFile和Require方法時,要手動給目標文件添加一個文件搜索位置。
運行結果如下:
Lua運行結果
最后,使用完畢記得清理虛擬機,我使用null==state來進行判斷,最后輸出“true”,說明調用LuaState.Dispose()后,虛擬機已經被清理。
我們上面實現了C#調用Lua文件和string,其實對于ToLua而且,直接調用string和文件并沒有本質區別,最后都會轉換成byte[]進行載入。
接下來實現一下ToLua調用指定Lua變量和函數,這里通過文件導入Lua代碼。
首先是我們的Lua代碼,這一段Lua代碼一共有一個普通變量、一個帶有函數的表,一個無參函數,一個有參函數,功能非常簡單,并且在這一段代碼中沒有調用。
num = 0 mytable={1,2,3,4} mytable.tableFunc=function() print('調用TableFunc'); end function Count() num=num+1 print('計數器+1,當前計數器為'..num) return num; end function InputValue( param) print('[lua中調用:]InputValue方法傳入參數:'..tostring( param)) end
然后是C#代碼,還是一樣的套路,先創建虛擬機,讀入Lua文件。下面依次說明普通變量、無參函數、有參函數和table的調用。
注意:如果帶有local標識,那么C#中無法直接獲取
普通變量
普通變量的調用非常簡單,在載入文件后,通過LuaState[string]的形式就可以直接獲取到,也可以通過這個表達式來直接賦值。
無參函數
函數的調用有兩種方式,一是先緩存為LuaFunction類型后調用,二是直接能過Call方法調用。
有參函數
有參函數和無參函數調用的區別在于參數的傳入,在ToLua中重載了非常多的傳參函數,與無參函數的調用方法相同,有參函數也有兩種調用方式,這里具體說明一下傳入參數的不同方式。
傳入參和調用分離。
這種方式一般需要先將函數緩存為LuaFunction,然后使用BeginPcall方法標記函數,再使用Push或者PushArgs方法將參數傳入函數,最后調用PCall,還可以調用EndPcall標記結束。
//對方法傳入參數 LuaFunction valueFunc = state.GetFunction("InputValue"); valueFunc.BeginPCall(); valueFunc.Push("--push方法從C#中傳入參數--"); valueFunc.PCall();
調用時直接傳入參數。
這是最符合一般操作邏輯的方式,但是查看實現代碼會發現,事實上只是LuaFunction中封裝的一套實現,其本質和上一種是一樣的。
table是lua中的一個百寶箱,一切東西都可以往里裝,table里可以有普通的變量,還可以有table,也可以有方法。
在ToLua里對table的數據結構進行了解析,實現了非常多的方法,這里完全可以將table看一個LuaState來進行操作,兩者沒有什么區別。
以下是完整的C#代碼,運行結果后附。
using LuaInterface; using UnityEngine; public class LuaAccess : MonoBehaviour { string luaFile = "LuaAccess"; LuaState state; void Start() { state = new LuaState(); state.Start(); //使用文件調用Lua //手動添加一個lua文件搜索地址 string sceneFile = Application.dataPath + "/LuaStudy"; state.AddSearchPath(sceneFile); state.Require(luaFile);//載入文件 //獲取Lua變量 Debug.Log("獲取文件中變量:" + state["num"]); state["num"] = 10; Debug.Log("設置文件中變量為:" + state["num"]); //調用Lua方法 LuaFunction luaFunc = state.GetFunction("Count"); luaFunc.Call(); Debug.Log("C#調用LuaFunc,函數返回值:" + state["num"]); Debug.Log("C#直接調用Count方法。"); state.Call("Count", false); //對方法傳入參數 LuaFunction valueFunc = state.GetFunction("InputValue"); valueFunc.BeginPCall(); valueFunc.Push("--push方法從C#中傳入參數--"); valueFunc.PCall(); valueFunc.EndPCall(); valueFunc.Call("--直接Call方法從C#傳入參數--"); //獲取LuaTable LuaTable table = state.GetTable("mytable"); table.Call("tableFunc"); LuaFunction tableFunc = table.GetLuaFunction("tableFunc"); Debug.Log("C#調用table中的func"); tableFunc.Call(); Debug.Log("獲取table中的num值:"+table["num"]); //通過下標直接獲取 for (int i = 0; i < table.Length; i++) { Debug.Log("獲取table的值:" + table[i]); } //轉換成LuaDictTable LuaDictTable dicTable = table.ToDictTable(); foreach (var item in dicTable) { Debug.LogFormat("遍歷table:{0}--{1}", item.Key, item.Value); } state.Dispose(); } }
Lua訪問變量
之前在 @羅夏L的文章里看過一篇他關于lua調用C#的筆記,但總覺得少了點什么,所以在我自己記筆記的時候特別注意了一下具體的實現。
在@羅夏L的文章中,將一個C#對象作為參數傳入列表中,然后直接在Lua代碼里運行對應的方法名,其中少了幾個關鍵的步驟,如果只是進行了這幾步,是實現不了在Lua里引用的。
首先還是從實現原理說起,在文章開始的第一節我提過ToLua的基本實現思路,并且將這套系統分成了三部分,在這三部分之中,ToLua作為一個橋梁實現了溝通Lua腳本和C#的功能,我們知道Lua的實質是通過字節碼對C進行了一套封裝,具有即時編譯的特點,從C#或者其他語言中來調用Lua都不算太困難,只需要提前約定特定方法名然后載入腳本即可,但C#是需要提前編譯的,怎么通過一段解釋器來調用C#中的對象就是主要的難點了,ToLua實現的就是這兩方面的功能。
從這方面來分析,我覺得大多數人會想到的最直接的實現思路大概都是通過反射來實現,uLua也是通過反射來實現的,但是反射的效率非常低,雖然確實可以實現,但問題還是非常明顯。
ToLua是通過方法名綁定的方式來實現這個映射的,首先構造一個Lua虛擬機,在虛擬機啟動后對所需的方法進行綁定,在虛擬機運行時可以在Lua中調用特定方法,虛擬機變相地實現了一個解釋器的功能,在Lua調用特定方法和對象時,虛擬機會在已綁定的方法中找到對應的C#方法和對象進行操作,并且ToLua已經自動實現了一些綁定的方法 。
基本原理大概了解以后,我們就可以來看看它的具體實現了。
第一步還是建立虛擬機并且啟動,為了實現Lua對C#的調用,首先我們要調用一下綁定方法,于是我們的代碼變成了下面這樣。可以看到,這里和之前的唯一區別是增加了LuaBinder.Bind(state)方法,這一個方法內部其實是對許多定義好的方法的綁定,也就是上面說的綁定方法。
using LuaInterface; using UnityEngine; public class CSharpAccess : MonoBehaviour { private string luaFile = "LuaCall"; LuaState state; void Start() { state = new LuaState(); state.Start(); string sceneFile = Application.dataPath + "/LuaStudy"; state.AddSearchPath(sceneFile); // 注冊方法調用 LuaBinder.Bind(state); state.Require(luaFile);//載入文件 } }
然后我們加入一個變量和一個方法,我們要實現的是完成在Lua中對這個方法和變量的調用。
public string AccessVar = "++這是初始值++"; public void PrintArg(string arg) { Debug.Log("C#輸出變量值:" + arg); }
在有了目標方法之后,我們要將這個變量和方法綁定進入虛擬機中。
查看LuaState的實現代碼,可以發現綁定主要有RegFunction、RegVar和RegConstant三個方法,分別用于綁定函數/委托、變量和常量。在這里ToLua是通過一個委托來實現方法的映射,這個委托需要傳入一個luaState變量,類型是IntPtr,這個變量的實質是一個句柄,在實際操作中,會將虛擬機作為變量傳入。
public delegate int LuaCSFunction(IntPtr luaState); public void RegFunction(string name, LuaCSFunction func); public void RegVar(string name, LuaCSFunction get, LuaCSFunction set); public void RegConstant(string name, double d); public void RegConstant(string name, bool flag);
總結一下幾個方法的特點:
這幾個方法都需要傳入一個string name,這個name就是之后在Lua中調用的變量或方法名。
RegConstant方法比較簡單,傳入一個name再傳入一個常量即可;
RegFunction和RegVar都是通過LuaCSFunction類型的委托實現;
RegFunction需要一個LuaCSFunction委托,這個委托需要對原方法重新進行一次實現;
RegVar除了name之外,還需要兩個LuaCSFunction委托,可以理解為一個變量的get/set方法,如果只有get或set,另一個留null即可。
接下來我們對AccessVar和PrintArg方法進行一下LuaCSFunction形式的實現。
private int PrintCall(System.IntPtr L) { try { ToLua.CheckArgsCount(L, 2); //對參數進行校驗 CSharpAccess obj = (CSharpAccess)ToLua.CheckObject(L, 1, typeof(CSharpAccess));//獲取目標對象并轉換格式 string arg0 = ToLua.CheckString(L, 2);//獲取特定值 obj.PrintArg(arg0);//調用對象方法 return 1; } catch (System.Exception e) { return LuaDLL.toluaL_exception(L, e); } } private int GetAccesVar(System.IntPtr L) { object o = null; try { o = ToLua.ToObject(L, 1); //獲得變量實例 CSharpAccess obj = (CSharpAccess)o; //轉換目標格式 string ret = obj.AccessVar; //獲取目標值 ToLua.Push(L, ret);//將目標對象傳入虛擬機 return 1; } catch (System.Exception e) { return LuaDLL.toluaL_exception(L, e, o, "attempt to index AccessVar on a nil value"); } } private int SetAccesVar(System.IntPtr L) { object o = null; try { o = ToLua.ToObject(L, 1);//獲得變量實例 CSharpAccess obj = (CSharpAccess)o;//轉換目標格式 obj.AccessVar = ToLua.ToString(L, 2);//將要修改的值進行設定,注意這里如果是值類型可能會出現拆裝箱 return 1; } catch (System.Exception e) { return LuaDLL.toluaL_exception(L, e, o, "attempt to index AccessVar on a nil value"); } }
可以看到這三個方法的格式都是一致的,通用的步驟如下:
使用ToLua中的方法對L句柄進行校驗,出現異常則拋出,本例中使用ToLua.CheckArgsCount方法;
獲得目標類的實例,并轉換格式,具體轉換方法較多,可以根據需要在ToLua類中選擇,本例中使用了ToLua.CheckObject和ToLua.ToObject等方法;
調用對應方法,不同的方法調用略有區別。
值得注意的是,在ToLua的ToObjectQuat、ToObjectVec2等獲取值類型的方法中,會出現拆裝箱的情況。
下一步將幾個方法注冊進lua虛擬機。
注意這里有兩對方法,分別是BeginModule\EndModule和BeginClass\EndClass,BeginModule\EndModule用于綁定命名空間,可以逐層嵌套;而BeginClass\EndClass用于開啟具體的類型空間,具體的方法和變量綁定必須在這成對的方法之中,否則會導致ToLua崩潰(百試百靈,別問我怎么知道的)。
private void Bind(LuaState L) { L.BeginModule(null); L.BeginClass(typeof(CSharpAccess), typeof(UnityEngine.MonoBehaviour)); state.RegFunction("Debug", PrintCall); state.RegVar("AccessVar", GetAccesVar, SetAccesVar); L.EndClass(); L.EndModule(); }
最后是我們的Lua代碼,非常簡單,注意Debug和AccessVar調用的區別。
print('--進入Lua調用--') local go = UnityEngine.GameObject.Find("LuaScene") local access=go:GetComponent("CSharpAccess") access:Debug("Lua調用C#方法") access.AccessVar="--這是修改值--" print('--Lua調用結束--')
完整C#代碼
using LuaInterface; using UnityEngine; public class CSharpAccess : MonoBehaviour { private string luaFile = "LuaCall"; LuaState state; void Start() { state = new LuaState(); state.Start(); string sceneFile = Application.dataPath + "/LuaStudy"; state.AddSearchPath(sceneFile); // 注冊方法調用 LuaBinder.Bind(state); Bind(state); Debug.Log("AccessVar初始值:" + AccessVar); state.Require(luaFile);//載入文件 Debug.Log("C#查看:" + AccessVar); state.Dispose(); } private void Bind(LuaState L) { L.BeginModule(null); L.BeginClass(typeof(CSharpAccess), typeof(UnityEngine.MonoBehaviour)); state.RegFunction("Debug", PrintCall); state.RegVar("AccessVar", GetAccesVar, SetAccesVar); L.EndClass(); L.EndModule(); } private int PrintCall(System.IntPtr L) { try { ToLua.CheckArgsCount(L, 2); //對參數進行校驗 CSharpAccess obj = (CSharpAccess)ToLua.CheckObject(L, 1, typeof(CSharpAccess));//獲取目標對象并轉換格式 string arg0 = ToLua.CheckString(L, 2);//獲取特定值 obj.PrintArg(arg0);//調用對象方法 return 1; } catch (System.Exception e) { return LuaDLL.toluaL_exception(L, e); } } public void PrintArg(string arg) { Debug.Log("C#輸出變量值:" + arg); } [System.NonSerialized] public string AccessVar = "++這是初始值++"; private int GetAccesVar(System.IntPtr L) { object o = null; try { o = ToLua.ToObject(L, 1); //獲得變量實例 CSharpAccess obj = (CSharpAccess)o; //轉換目標格式 string ret = obj.AccessVar; //獲取目標值 ToLua.Push(L, ret);//將目標對象傳入虛擬機 return 1; } catch (System.Exception e) { return LuaDLL.toluaL_exception(L, e, o, "attempt to index AccessVar on a nil value"); } } private int SetAccesVar(System.IntPtr L) { object o = null; try { o = ToLua.ToObject(L, 1);//獲得變量實例 CSharpAccess obj = (CSharpAccess)o;//轉換目標格式 obj.AccessVar = ToLua.ToString(L, 2);//將要修改的值進行設定 return 1; } catch (System.Exception e) { return LuaDLL.toluaL_exception(L, e, o, "attempt to index AccessVar on a nil value"); } } }
運行結果
Lua調用C#
那么最后,我們回到本節開始, @羅夏L的文章里是哪里出現了問題?
我在lua中加入了一行access:PrintArg("PrintArg")調用方法,發現Unity報了這樣的錯誤:
直接調用方法名報錯.png
說明單純這樣是做不到直接調用方法的,仔細看文章,我發現他有提到這樣的內容:
首先將自己寫的類 放到 CustomSettings 里 就是CallLuafunction
BindType[] customTypeList
放到這個數組里 注冊進去供lua使用
這里是不是他說得不夠詳細?我找到這個類,發現這個類里記錄了非常多的Unity自帶類,這讓我想起了第一次啟動Lua時的提示,心里生出了一個疑問:這些數據是不是用于自動注冊生成類的呢?
//在這里添加你要導出注冊到lua的類型列表 public static BindType[] customTypeList = { _GT(typeof(LuaInjectionStation)), _GT(typeof(InjectType)), _GT(typeof(Debugger)).SetNameSpace(null),
...以下部分省略
沿著調用鏈,我找到了這個變量的引用,果然,最這個數據是用于類型注冊的。
我將這個類放到了數組的最后,點擊Clear wrap files,完成后立即彈出了數據自動生成的對話框,點擊確認,
重新生成注冊
自動生成
接下來我重新運行了lua腳本:
print('--進入Lua調用--') local go = UnityEngine.GameObject.Find("LuaScene") local access=go:GetComponent("CSharpAccess") access:Debug("Lua調用C#方法") access.AccessVar="--這是修改值--" print('--Lua調用結束--') access:PrintArg("PrintArg")
成功運行
成功運行,說明ToLua實現了一整套綁定方案,只需要將所需要的內容配置完成即可。
關于在ToLua框架中如何使用C#與Lua就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。