熱更新解決方案--tolua學習筆記

movin2333發表於2021-04-13

一.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;
    }
}

 

相關文章