1. 程式人生 > 程式設計 >ToLua框架下C#與Lua程式碼的互調操作

ToLua框架下C#與Lua程式碼的互調操作

Lua是目前國內使用最多的熱更語言,基於Lua的熱更框架也非常多,最近學習了一下ToLua的熱更框架,主要使用的問題在於C#和Lua之間的互調,因此做一下學習記錄以備後查。

所謂“互調”,當然要包括兩個方面,一是通過C#呼叫Lua程式碼,二是通過Lua程式碼呼叫C#指令碼,第二點還包括註冊在C#腳本里的Unity物體。

1. ToLua的簡單實現原理

ToLua框架主要是通過靜態繫結來實現C#與Lua之間的互動的,基本原理是通過建立一個Lua虛擬機器來對映C#指令碼,然後再通過這個虛擬機器來執行Lua指令碼,Lua指令碼在執行時可以通過虛擬機器反過來呼叫C#腳本里註冊過的物體,這種方式的優勢在於比起使用反射的uLua來說效率更高。

ToLua框架下可以將實現分成三大部分:普通的Unity+C#部分、ToLua虛擬機器部分和Lua指令碼部分,結構見下圖:

ToLua框架下C#與Lua程式碼的互調操作

ToLua結構

目前國內需要熱更的手遊一般都將主要的邏輯框架和元件功能用C#實現,而具體功能和呼叫放在Lua中,因為C#是不能被打包進AssetBundle中的,所以無法通過AssetBundle對程式碼進行改動,但是Lua是即時編譯型語言,並且可以被打包進入AssetBundle中,在需要修改簡單功能時,將Lua程式碼通過AssetBundle進行更新即可。

2. ToLua的下載的安裝

首先是下載地址:

ToLua

這是作者的github地址,進入以後點選下載Zip,完成後解壓到自己需要的目錄,再用Unity開啟即可。

ToLua框架下C#與Lua程式碼的互調操作

點選下載zip即可

第一次開啟工程時會提示是否需要自動生成註冊檔案,新手可以選擇直接生成,若選擇了取消,也可以在編輯器選單中手動註冊。——這是一個非常重要的操作,後文也會提到。

下面開始關於使用的正文。

3. ToLua的基本使用

前面有提到過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程式')

ToLua框架下C#與Lua程式碼的互調操作

Lua掛載

ToLua直接呼叫Lua程式碼的方式有兩種,一種是DoString,另一種是DoFile;此外還有一個Require方法,這個方法和前兩個方法不同的是,ToLua會將呼叫的Lua檔案載入Lua棧中,而前兩者只是執行一次,執行之後儲存在快取中,雖然也可以後續呼叫,但是會。

在上述程式碼中要注意,使用DoFile和Require方法時,要手動給目標檔案新增一個檔案搜尋位置。

執行結果如下:

ToLua框架下C#與Lua程式碼的互調操作

Lua執行結果

最後,使用完畢記得清理虛擬機器,我使用null==state來進行判斷,最後輸出“true”,說明呼叫LuaState.Dispose()後,虛擬機器已經被清理。

4. C#中呼叫Lua變數/函式

我們上面實現了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中封裝的一套實現,其本質和上一種是一樣的。

4.table

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();
 }
}

ToLua框架下C#與Lua程式碼的互調操作

Lua訪問變數

5. Lua中呼叫C#方法/變數

之前在 @羅夏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,"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",SetAccesVar);
 L.EndClass();
 L.EndModule();
 }
 
 private int PrintCall(System.IntPtr L)
 {
 try
 {
  ToLua.CheckArgsCount(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,"attempt to index AccessVar on a nil value");
 }
 }
 private int SetAccesVar(System.IntPtr L)
 {
 object o = null;
 
 try
 {
  o = ToLua.ToObject(L,2);//將要修改的值進行設定
  return 1;
 }
 catch (System.Exception e)
 {
  return LuaDLL.toluaL_exception(L,"attempt to index AccessVar on a nil value");
 }
 }
}

執行結果

ToLua框架下C#與Lua程式碼的互調操作

Lua呼叫C#

那麼最後,我們回到本節開始, @羅夏L的文章裡是哪裡出現了問題?

我在lua中加入了一行access:PrintArg("PrintArg")呼叫方法,發現Unity報了這樣的錯誤:

ToLua框架下C#與Lua程式碼的互調操作

直接呼叫方法名報錯.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,完成後立即彈出了資料自動生成的對話方塊,點選確認,

ToLua框架下C#與Lua程式碼的互調操作

重新生成註冊

ToLua框架下C#與Lua程式碼的互調操作

自動生成

接下來我重新運行了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框架下C#與Lua程式碼的互調操作

成功執行

成功執行,說明ToLua實現了一整套繫結方案,只需要將所需要的內容配置完成即可。

6.總結

原本只是想簡單寫一寫呼叫方式,最後又寫成了一篇長文,但是從有計劃開始到一整篇結束卻花掉了近一整天的時間。

雖然如此,收穫還是非常大的,對這套工具的使用熟練度又上了一個層次,以後也要加強總結。

以上這篇ToLua框架下C#與Lua程式碼的互調操作就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支援我們。