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

movin2333 發表於 2021-04-06

一.熱更新方案簡介

  在Unity遊戲工程中,C#程式碼(編譯型語言)資源和Resources資料夾下的資源打包後都不可以更改,因此這部分內容不能進行熱更新,而lua程式碼(解釋型語言)邏輯不需要進行預編譯再執行,可以在遊戲執行過程中進行修改,AB包資源也可以在遊戲執行過程中下載解壓縮並使用其中的資源。因此客戶端可以在啟動時檢驗伺服器端的AB包資源是否有更新,如果有更新先下載更新,將lua程式碼資源和其他更新資源打包為AB包放在伺服器端,客戶端下載後直接在執行過程中解壓縮並使用更新資源,實現了客戶端不中斷執行即完成更新的目的,也就是熱更新。

二.xlua熱更新方案簡介

  xlua框架提供了C#和lua相互呼叫的功能及Hotfix熱補丁的功能,主要目的是方便我們將純C#工程在不重做的情況下改造成具備熱更新功能的工程。

三.準備工作--說明:使用的Unity版本為2019.4.18f1c1,匯入的xlua為2021年4月4日從GitHub上直接clone的工程檔案,沒有下載release版本。

  1.xlua框架匯入

    在GitHub上搜尋xlua,找到騰訊的xlua專案,下載專案的壓縮包。

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

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

    下載完成後解壓,發現下載的是一個Unity工程檔案:

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

    在工程檔案中,核心程式碼是Assets目錄下的Plugins和XLua這兩個資料夾中的內容,將其複製到自己的工程檔案中即可。

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

    將這兩個資料夾複製到自己的工程中後稍等一會兒,就會發現在選單欄中Windows選單選項左側出現了XLua選單選項,沒有報錯的話說明成功匯入。

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

  2.AB包工具匯入

    在Unity中通過PackageManager匯入AB包工具,匯入方法詳見:熱更新基礎--AssetBundle學習筆記

  3.AB包管理器

    為了方便載入AB包,我們可以製作一個AB包的管理器指令碼,指令碼詳見:熱更新基礎--AssetBundle學習筆記

四.C#呼叫lua

  1.lua解析器

    void Start()
    {
        //Lua解析器,能夠在Unity中執行Lua
        LuaEnv env = new LuaEnv();

        //執行單行Lua語言,使用DoString成員方法
        env.DoString("print('hello world!')");

        //執行lua指令碼,一般都是直接呼叫lua語言的require關鍵字執行lua指令碼
        //預設尋找指令碼的路徑是在Resources下
        //lua字尾Unity不能識別,需要將lua檔案新增上.txt以便Unity識別
        env.DoString("require('Main')");

        //清除lua中沒有手動釋放的物件,相當於垃圾回收,一般在幀更新中定時執行或者切換場景時執行
        env.Tick();

        //銷燬lua解析器,但是一般不會銷燬,因為最好保持解析器的唯一性以節約效能
        env.Dispose();
    }

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

  2.lua檔案載入重定向,即更改使用require關鍵字載入lua檔案時尋找lua檔案的位置(預設lua指令碼在Resources下才能識別,這和熱更新的目的衝突)

    void Start()
    {
        LuaEnv env = new LuaEnv();

        //使用AddLoader方法新增重定向,即自定義檔案載入的規則
        //引數為一個委託,這個委託有一個ref引數,自動執行傳入require執行的指令碼檔名,在委託中拼接好完整的路徑;委託的返回值為lua檔案轉化出的位元組陣列
        //新增委託後,委託在執行require語句時自動執行
        env.AddLoader(MyCustomLoader);

        //使用require語句執行lua檔案,會自動先呼叫新增的重定向方法尋找lua檔案,如果找不到再到預設路徑Resources下尋找
        env.DoString("require('Main')");
    }

    /// <summary>
    /// 重定向方法
    /// </summary>
    /// <param name="filePath">檔名</param>
    /// <returns></returns>
    private byte[] MyCustomLoader(ref string filePath)
    {
        //拼接完整的lua檔案所在路徑
        string path = Application.dataPath + "/Lua/" + filePath + ".lua";
        Debug.Log(path);
        //判斷檔案是否存在,存在返回讀取的檔案位元組陣列,不存在列印提醒資訊,返回null
        if (File.Exists(path))
        {
            return File.ReadAllBytes(path);
        }
        else
        {
            Debug.Log("MyCustomLoader重定向失敗,檔名為" + filePath);
        }
        return null;
    }

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

  3.lua解析器管理器:對lua解析器的進一步封裝以方便使用

/// <summary>
/// lua管理器,對lua解析器的進一步封裝,保證lua解析器的唯一性
/// </summary>
public class LuaManager
{
    //單例模組
    private static LuaManager instance;
    public static LuaManager Instance
    {
        get
        {
            if (instance == null)
                instance = new LuaManager();
            return instance;
        }
    }
    private LuaManager()
    {
        //在構造方法中就為唯一的lua解析器賦值
        luaEnv = new LuaEnv();
        //載入lua指令碼重定向
        //重定向到lua資料夾下
        luaEnv.AddLoader((ref string filePath) =>
        {
            //拼接完整的lua檔案所在路徑
            string path = Application.dataPath + "/Lua/" + filePath + ".lua";
            //判斷檔案是否存在,存在返回讀取的檔案位元組陣列,不存在列印提醒資訊,返回null
            if (File.Exists(path))
            {
                return File.ReadAllBytes(path);
            }
            else
            {
                Debug.Log("MyCustomLoader重定向失敗,檔名為" + filePath);
            }
            return null;
        });
        //重定向載入AB包中的lua指令碼
        luaEnv.AddLoader((ref string filePath) =>
        {
            /*//載入AB包
            string path = Application.streamingAssetsPath + "/lua";
            AssetBundle bundle = AssetBundle.LoadFromFile(path);

            //載入lua檔案,返回
            TextAsset texts = bundle.LoadAsset<TextAsset>(filePath + ".lua");
            //返回載入到的lua檔案的byte陣列
            return texts.bytes;*/

            /*//使用AB包管理器載入,非同步載入
            byte[] luaBytes = null;
            AssetBundleManager.Instance.LoadResAsync<TextAsset>("lua", filePath + ".lua", (lua) =>
            {
                 if (lua != null)
                     luaBytes = lua.bytes;
                 else
                     Debug.Log("重定向失敗,從AB包載入lua檔案失敗");
             });
            return luaBytes;*/

            //使用AB包管理器載入,同步載入
            return AssetBundleManager.Instance.LoadRes<TextAsset>("lua", filePath + ".lua").bytes;
        });
    }

    //持有一個唯一的lua解析器
    private LuaEnv luaEnv;

    //luaEnv中的大G表,提供給外部呼叫
    public LuaTable Global
    {
        get
        {
            //校驗一下instance是否是null,避免dispose後無法獲取的情況出現
            if (instance == null)
                instance = new LuaManager();
            return luaEnv.Global;
        }
    }

    /// <summary>
    /// 執行單句lua程式碼
    /// </summary>
    /// <param name="luaCodeString"></param>
    public void DoString(string luaCodeString)
    {
        luaEnv.DoString(luaCodeString);
    }
    /// <summary>
    /// 執行lua檔案的程式碼,直接提供檔名即可執行檔案,不需要再書寫lua的require語句,在方法內部拼接lua語句
    /// </summary>
    /// <param name="fileName">lua檔名</param>
    public void DoLuaFile(string fileName)
    {
        luaEnv.DoString("require('" + fileName + "')");
    }
    /// <summary>
    /// 釋放解析器
    /// </summary>
    public void Tick()
    {
        luaEnv.Tick();
    }
    /// <summary>
    /// 銷燬解析器
    /// </summary>
    public void Dispose()
    {
        luaEnv.Dispose();
        //銷燬解析器後將lua解析器物件和單例變數都置空,下次呼叫時會自動呼叫建構函式建立lua解析器,以免報空
        luaEnv = null;
        instance = null;
    }
}

  4.訪問變數

void Start()
    {
        LuaManager.Instance.DoLuaFile("Main");

        //使用_G表獲取luaenv中的global變數值
        Debug.Log(LuaManager.Instance.Global.Get<int>("testNumber"));
        Debug.Log(LuaManager.Instance.Global.Get<bool>("testBool"));
        Debug.Log(LuaManager.Instance.Global.Get<float>("testFloat"));

        //使用_G表修改luaenv中的global變數值
        LuaManager.Instance.Global.Set("testBool",false);
        Debug.Log(LuaManager.Instance.Global.Get<bool>("testBool"));

        //不能直接獲取和設定本地變數
    }

  5.訪問函式,使用委託接收

//自定義委託,對於有引數和返回值的委託,必須加上特性[CSharpCallLua],否則無法處理,無參無返回值的委託不需要
//特性起作用還需要在Unity中生成指令碼
[CSharpCallLua]
public delegate void CustomCall(int a);
//自定義含有out或者ref引數的委託用於接收多返回值函式,out和ref根據需要選擇,都可以用於接收多返回值
[CSharpCallLua]
public delegate int CustomCall2(out int a, out int b);
[CSharpCallLua]
public delegate void CustomCall3(params int[] args);
    void Start()
    {
        LuaManager.Instance.DoLuaFile("Main");

        //獲取函式,使用委託儲存
        UnityAction npnr = LuaManager.Instance.Global.Get<UnityAction>("NoParamNoReturn");
        npnr();

        //xlua提供了獲取函式的方法,但是不推薦使用,推薦使用Unity或者C#提供的委託或者自定義委託儲存
        LuaFunction luaFun = LuaManager.Instance.Global.Get<LuaFunction>("NoParamNoReturn");
        luaFun.Call();

        //有參無返回值
        //使用自定義的委託需要宣告特性且在Unity中生成程式碼
        CustomCall hpnr = LuaManager.Instance.Global.Get<CustomCall>("HaveParamNoReturn");
        hpnr(2);

        //使用C#提供的委託儲存,不用宣告特性
        Action<int> hpnr2 = LuaManager.Instance.Global.Get<Action<int>>("HaveParamNoReturn");
        hpnr2(2);

        //多返回值
        //不能使用系統自帶的委託,多返回值需要自定義委託
        CustomCall2 mr = LuaManager.Instance.Global.Get<CustomCall2>("ManyReturns");
        int m;
        int n;
        int p = mr(out m, out n);
        Debug.Log(m + "-" + n + "-" + p);

        //變長引數
        CustomCall3 vp = LuaManager.Instance.Global.Get<CustomCall3>("VariableParams");
        vp(1, 2, 3, 4, 5);
    }

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

  6.表對映為List或者Dictionary

    void Start()
    {
        LuaManager.Instance.DoLuaFile("Main");

        //得到List
        List<int> list = LuaManager.Instance.Global.Get<List<int>>("ListTable");
        foreach (int i in list)
        {
            Debug.Log(i);
        }

        //得到Dictionary
        Dictionary<string, int> dic = LuaManager.Instance.Global.Get<Dictionary<string, int>>("DictionaryTable");
        foreach (KeyValuePair<string,int> pair in dic)
        {
            Debug.Log(pair.Key + ":" + pair.Value);
        }
    }

  7.表對映到類物件

/// <summary>
/// 宣告一個類來和lua中的類進行對映,變數名稱必須和lua中的對應類一致,但是不必一一對應,對映時會自動丟棄找不到的內容
/// </summary>
public class CallLuaClass
{
    public string name;
    public int age;
    public int sex;
    public UnityAction eat;
}
    void Start()
    {
        LuaManager.Instance.DoLuaFile("Main");

        //獲得類物件
        CallLuaClass clc = LuaManager.Instance.Global.Get<CallLuaClass>("testClass");
        Debug.Log(clc.name + "-" + clc.age + "-" + clc.sex);
        clc.eat();
    }

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

  8.表對映到介面

/// <summary>
/// 使用一個介面接收表的對映,但是介面中的變數不允許被賦值,這裡使用屬性
/// 必須加上特性[CSharpCallLua]
/// </summary>
[CSharpCallLua]
public interface ICSharpCallLua
{
    string name { get; set; }
    int age { get; set; }
    int sex { get; set; }
    Action eat { get; set; }
}
    void Start()
    {
        LuaManager.Instance.DoLuaFile("Main");

        //得到介面物件
        ICSharpCallLua icscl = LuaManager.Instance.Global.Get<ICSharpCallLua>("testClass");
        Debug.Log(icscl.name + "-" + icscl.age + "-" + icscl.sex);
        icscl.eat();
    }

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

  注意:之前實現的所有拷貝都是引用拷貝,也就是c#中的拷貝值發生改變,lua程式碼不受影響,但是介面的拷貝是引用拷貝,也就是改變C#中的拷貝的值,lua中的值也發生了改變。

  9.對映到luaTable類

    void Start()
    {
        LuaManager.Instance.DoLuaFile("Main");

        //得到LuaTable,對應lua中的table。
        //本質上Global也是LuaTable型別的變數,使用方法和之前通過Global獲取各種變數函式等方法相同
        //不推薦使用LuaTable和LuaFunction,效率低
        //LuaTable的拷貝是引用拷貝
        LuaTable table = LuaManager.Instance.Global.Get<LuaTable>("testClass");
        Debug.Log(table.Get<int>("age"));
        Debug.Log(table.Get<string>("name"));
        Debug.Log(table.Get<int>("sex"));
        table.Get<LuaFunction>("eat").Call();
    }

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

  LuaTable類對應Lua中的表,本質上Global變數也是LuaTable型別,所以LuaTable的使用方法和之前講的通過Global獲取各種變數的方法相同。LuaTable和LuaFunction使用後記得呼叫dispose方法釋放垃圾,否則容易造成記憶體洩漏。

五.lua呼叫C#

  1.在Unity中無法直接執行lua,因此需要使用C#指令碼作為lua指令碼的主入口啟動lua指令碼的執行,接下來都不再贅述這一步驟,所有的lua程式碼也都在這個特定的lua指令碼中編寫。

public class Main : MonoBehaviour
{
    private void Start()
    {
        //在這個指令碼中啟動特定的lua指令碼,接下來的lua程式碼都在這個指令碼中編寫
        LuaManager.Instance.DoLuaFile("Main");
    }
}

  Main.lua這個指令碼作為lua指令碼的入口,接下來再在這個Main.lua指令碼中呼叫其他指令碼。

require("CallClass")

  2.建立類物件

--lua中呼叫C#指令碼

--建立類物件
--Unity中的類如GameObject、Transform等類都儲存在CS表中
--使用CS.名稱空間.類名的方式呼叫Unity中的類
local obj1 = CS.UnityEngine.GameObject("使用lua建立的第一個空物體")

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

--lua中呼叫C#指令碼

--建立類物件
--Unity中的類如GameObject、Transform等類都儲存在CS表中
--使用CS.名稱空間.類名的方式呼叫Unity中的類
--每次都寫名稱空間太麻煩,可以定義全域性變數先把類儲存起來,也能節約效能
GameObject = CS.UnityEngine.GameObject
local obj = GameObject("movin")

--使用點來呼叫靜態方法
local obj2 = GameObject.Find("movin")

--使用.來呼叫物件中的成員變數
Log = CS.UnityEngine.Debug.Log
Log(obj.transform.position)

Vector3 = CS.UnityEngine.Vector3
--使用物件中的成員方法必須使用:呼叫
obj.transform:Translate(Vector3.right)
Log(obj.transform.position)

--自定義類的呼叫
--直接使用CS點的方式呼叫
local customClass = CS.CustomClass()
customClass.name = "movin"
customClass:Eat()

--有名稱空間的類再點一層
local customClassInNamespace = CS.CustomNamespace.CustomClassInNamespace()
customClassInNamespace.name = "movin2"
customClassInNamespace:Eat()

--繼承了mono的類不能new出來,只能獲取元件
--xlua提供了typeof的方法得到類的Type
--自定義的指令碼元件直接用CS點出來即可
obj:AddComponent(typeof(CS.Main))
--系統自帶的指令碼一般在UnityEngine名稱空間下
obj:AddComponent(typeof(CS.UnityEngine.Rigidbody))
/// <summary>
/// 自定義類
/// </summary>
public class CustomClass
{
    public string name;
    public void Eat()
    {
        Debug.Log(name + "在吃飯");
    }
}

/// <summary>
/// 自定義類,包裹在名稱空間中
/// </summary>
namespace CustomNamespace
{
    public class CustomClassInNamespace
    {
        public string name;
        public void Eat()
        {
            Debug.Log(name + "在吃飯");
        }
    }
}

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

  3.使用列舉

--呼叫列舉

--呼叫Unity提供的列舉
--Unity提供的列舉一般在UnityEngine中

PrimitiveType = CS.UnityEngine.PrimitiveType
GameObject = CS.UnityEngine.GameObject

local obj = GameObject.CreatePrimitive(PrimitiveType.Cube)

--呼叫自定義的列舉
E_CustomEnum = CS.E_CustomEnum

Log = CS.UnityEngine.Debug.Log
Log(E_CustomEnum.Idle)

--使用_CastFrom方法進行列舉型別轉換,可以從數字轉換成列舉或者字串轉換成列舉
Log(E_CustomEnum.__CastFrom(1))
Log(E_CustomEnum.__CastFrom("Atk"))

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

  4.使用List和Dictionary

local CustomClass = CS.CustomClass
local Log = CS.UnityEngine.Debug.Log

--呼叫陣列,使用C#的陣列相關API,不要使用lua的方法
obj = CustomClass();
Log(obj.array.Length)

--遍歷陣列,注意從0到length-1,按照C#的下標遍歷不是lua的
for i=0,obj.array.Length-1 do
    Log(obj.array[i])
end

Log("******************")
--建立陣列,利用陣列類Array的CreateInstance靜態方法建立陣列
--引數意思:建立陣列中儲存元素的型別、建立的陣列的長度
local arr = CS.System.Array.CreateInstance(typeof(CS.System.Int32),5)
Log(arr.Length)
Log(arr[1])

Log("******************")
--呼叫List,呼叫成員方法用:
obj.list:Add('M')
for i = 0,obj.list.Count-1 do
    Log(obj.list[i])
end

Log("******************")
--建立List
--老版,方法很麻煩
local list2 = CS.System.Collections.Generic["List`1[System.String]"]()
list2:Add("abcde")
Log(list2[0])
--新版 版本>v2.1.12  先建立一個類,再例項化出來list
local List_String = CS.System.Collections.Generic.List(CS.System.String)
local list3 = List_String()
list3:Add("aaaaaaaaaa")
Log(list3[0])

Log("******************")
--呼叫dic
obj.dic:Add(1,"abc")
obj.dic:Add(2,"def")
--遍歷
for k,v in pairs(obj.dic) do
    Log(k.."--"..v)
end

Log("******************")
--建立dic
local Dic_String_Vector3 = CS.System.Collections.Generic.Dictionary(CS.System.String,CS.UnityEngine.Vector3)
local dic2 = Dic_String_Vector3()
dic2:Add("abc",CS.UnityEngine.Vector3.right)
dic2:Add("def",CS.UnityEngine.Vector3.up)

Log(dic2["abc"])  --在lua中建立的字典使用這種方式得不到值,這句程式碼列印出的結果是空值
Log(dic2:get_Item("abc"))  --在lua中自己建立的字典使用get_Item方法得到值
dic2:set_Item("abc",CS.UnityEngine.Vector3.left)  --同樣地,通過set_Item方法設定字典地值
Log(dic2:get_Item("abc"))
print(dic2:TryGetValue("abc"))  --也可以通過TryGetValue方法獲取值

for k,v in pairs(dic2) do
    print(k,v)
end
/// <summary>
/// 自定義類
/// </summary>
public class CustomClass
{
    public string[] array = { "a","b","c","d","e","f","g","h" };
    public List<char> list = new List<char>{ 'A','B','C','D','E','F','G','H','I','J' };
    public Dictionary<int, string> dic = new Dictionary<int, string>();
}

  5.使用C#擴充方法

CustomClass = CS.CustomClass

--使用成員方法
local customClass = CustomClass()
customClass.name = "movin"
customClass:Eat()

--使用擴充方法,擴充方法一定是靜態方法,但是呼叫時和成員方法一樣的呼叫方式
--在定義擴充方法的工具類前一定加上特性[LuaCallCSharp],並且生成程式碼
customClass:Move()
/// <summary>
/// 自定義類
/// </summary>
public class CustomClass
{
    public string name;
    public void Eat()
    {
        Debug.Log(name + "在吃飯");
    }
}
/// <summary>
/// 工具類,定義擴充方法
/// </summary>
[LuaCallCSharp]
public static class Tools
{
    public static void Move(this CustomClass cc)
    {
        Debug.Log(cc.name + "在移動");
    }
}

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

  建議:要在lua中使用的C#類都可以加上[LuaCallCSharp]特性,這樣預先將程式碼生成,可以提高Lua訪問C#類的效能。

  6.使用含有ref和out引數的函式

CustomClass = CS.CustomClass
local obj = CustomClass()


--ref引數,使用多返回值形式接收
--如果函式有返回值,這個返回值是多返回值的第一個
--引數數量不夠,會預設使用預設值補位
local a,b,c = obj:RefFun(1,0,0,1)
print(a,b,c)

--out引數,還是以多返回值的形式接收
--out引數不需要傳遞值
local a,b,c = obj:OutFun(23,24)
print(a,b,c)

--綜合來看
--從返回值上看,ref和out都會以多返回值的形式返回,原來如果有返回值的話原來的返回值是多返回值中的第一個
--從引數看,ref引數需要傳遞,out引數不需要傳遞
local a,b,c = obj:RefOutFun(12,23)
print(a,b,c)
/// <summary>
/// 自定義類
/// </summary>
public class CustomClass
{
    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;
        c = d;
        return 200;
    }
    public int RefOutFun(int a,out int b,ref int c)
    {
        b = a * 10;
        c = a * 20;
        return 200;
    }
}

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

  7.使用過載函式

CustomClass = CS.CustomClass
local customClass = CustomClass()

--使用過載函式
--lua支援呼叫C#的過載函式
--lua中的數值型別只有number,所以對C#中多精度的過載函式支援不好,使用時可能出現問題
--如第四個過載函式呼叫結果為0(應該是11.4),所以應避免這種情況
print(customClass:Calc())
print(customClass:Calc(1))
print(customClass:Calc(2,3))
print(customClass:Calc(1.4))

--解決過載函式含糊的問題(效率低,僅作了解)
--運用反射
local m1 = typeof(CustomClass):GetMethod("Calc",{typeof(CS.System.Int32)})
local m2 = typeof(CustomClass):GetMethod("Calc",{typeof(CS.System.Single)})
--通過xlua提供的tofunction方法將反射得到的方法資訊轉化為函式
local f1 = xlua.tofunction(m1)
local f2 = xlua.tofunction(m2)
--再次呼叫函式,非靜態方法需要傳入物件
print(f1(customClass,10))
print(f2(customClass,1.4))
/// <summary>
/// 自定義類
/// </summary>
public class CustomClass
{
    public int Calc()
    {
        return 100;
    }
    public int Calc(int a,int b)
    {
        return a + b;
    }
    public int Calc(int a)
    {
        return a;
    }
    public float Calc(float a)
    {
        return a + 10;
    }
}

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

  8.委託和事件

local customClass = CS.CustomClass()

--委託中儲存的是函式,宣告函式儲存到委託中
local fun = function()
    print("函式fun")
end

--委託中第一次新增函式使用=新增
customClass.action = fun
--委託中第二次新增函式使用+=,lua中不支援+=運算子,需要分開寫
customClass.action = customClass.action + fun
--委託中也可以新增匿名函式
customClass.action = customClass.action + function()
    print("臨時函式")
end

--使用點呼叫委託還是冒號呼叫委託都可以呼叫,最好使用冒號
customClass:action()

print("********************")

--事件和委託的使用方法不一致(事件不能在外部呼叫)
--使用冒號新增和刪除函式,第一個引數傳入加號或者減號字串,表示新增還是修改函式
--事件也可以新增匿名函式
customClass:eventAction("+",fun)
--事件不能直接呼叫,必須在C#中提供呼叫事件的方法,這裡已經提供了DoEvent方法執行事件
customClass:DoEvent()
--同樣地,事件不能直接清空,需要在C#中提供對應地方法
/// <summary>
/// 自定義類
/// </summary>
public class CustomClass
{
    public UnityAction action;
    public event UnityAction eventAction;
    public void DoEvent()
    {
        if (eventAction != null)
            eventAction();
    }
}

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

  9.特殊問題

local customClass = CS.CustomClass()

--特殊問題一:得到二維陣列指定位置元素的值
--獲取二維陣列的長度
print("行:"..customClass.array:GetLength(0))
print("行:"..customClass.array:GetLength(1))

--不能通過C#的索引訪問元素(array[0,0]或array[0][0])
--使用陣列提供的成員方法GetValue訪問元素
print(customClass.array:GetValue(0,0))

--遍歷
for i=0,customClass.array:GetLength(0)-1 do
    for j=0,customClass.array:GetLength(1)-1 do
        print(customClass.array:GetValue(i,j))
    end
end


print("***********************")

--特殊問題二:lua中空值nil和C#中空值null的比較

--往場景物件上新增一個指令碼,存在就不加,不存在再加
GameObject = CS.UnityEngine.GameObject
Rigidbody = CS.UnityEngine.Rigidbody

local obj = GameObject("測試nil和null")
local rigidbody = obj:GetComponent(typeof(Rigidbody))
print(rigidbody)
--校驗空值,看是否需要新增指令碼
--nil和null並不相同,在lua中不能使用==進行判空,一定要使用Equals方法進行判斷
--這裡如果rigidbody為空,可能報錯,所以可以自己提供一個判空函式進行判空
--這裡為了筆記方便將函式定義在這裡,這個全域性函式最好定義在lua指令碼啟動的主函式Main中
function IsNull(obj)
    if obj == nil or obj:Equals(nil) then
        return true
    end
    return false
end
--使用自定義的判空函式進行判斷
if IsNull(rigidbody) then
    rigidbody = obj:AddComponent(typeof(Rigidbody))
end
print(rigidbody)

print("***********************")

--特殊問題三:讓lua和系統型別能夠相互訪問

--對於自定義的型別,可以新增CSharpCallLua和LuaCallCSharp這兩個特性使Lua和自定義型別能相互訪問,但是對於系統類或第三方程式碼庫,這種方式並不適用
--為系統類或者第三方程式碼庫加上這兩個特性的寫法比較固定,詳情見C#程式碼
/// <summary>
/// 自定義類
/// </summary>
public class CustomClass
{
    public int[,] array = new int[2, 3] { { 1, 2, 3 }, { 4, 5, 6 } };

    //實現為系統類新增[CSharpCallLua]和[LuaCallCSharp]特性
    [CSharpCallLua]
    public static List<Type> csharpCallLuaList = new List<Type>()
    {
        //將需要新增特性的類放入list中
        typeof(UnityAction<float>),
    };
    [LuaCallCSharp]
    public static List<Type> luaCallCsharpList = new List<Type>()
    {
        typeof(GameObject),
    };
}

   10.使用協程

--xlua提供了一個工具表,要使用協程必須先呼叫這個工具表
util = require("xlua.util")

GameObject = CS.UnityEngine.GameObject
WaitForSeconds = CS.UnityEngine.WaitForSeconds

local obj = GameObject("Coroutine")
local mono = obj:AddComponent(typeof(CS.LuaCallCSharp))

--被開啟的協程函式
fun = function()
    local a = 1
    while true do
        --lua中不能直接使用C#中的yield return返回
        --使用lua中的協程返回方法
        coroutine.yield(WaitForSeconds(1)) 
        print(a)
        a = a + 1
        if a>10 then 
            --協程的關閉,必須要將開啟的協程儲存起來
            mono:StopCoroutine(startedCoroutine)
        end
    end
end

--啟動協程
--寫法固定,必須使用固定表的cs_generate方法把xlua方法處理成mono能夠使用的協程方法
startedCoroutine = mono:StartCoroutine(util.cs_generator(fun))

  11.使用泛型函式

    lua中沒有泛型語法,對於C#中的泛型方法,可以直接傳遞引數(因為lua中不需要宣告型別),但是這種寫法並不是所有的泛型方法都支援,xlua只支援有約束且泛型作為引數的泛型函式,其他泛型函式不支援。如果要在lua中呼叫泛型函式,可以使用特定的語法。

local tank = CS.UnityEngine.GameObject.Find("Tank")

--xlua提供了得到泛型函式的方法get_generic_method,引數第一個為類名,第二個為方法名
local addComponentFunc = xlua.get_generic_method(CS.UnityEngine.GameObject,"AddComponent")
--接著呼叫這個泛型方法,引數為泛型的類,得到一個新方法
local addComponentFunc2 = addComponentFunc(CS.MonoForLua)
--呼叫,第一個引數是呼叫的物件,如果有其他引數在後面傳遞
addComponentFunc2(tank)

    使用限制:打包時如果使用mono打包,這種方式支援使用;如果使用il2cpp打包,泛型引數需要是引用型別或者是在C#中已經呼叫過的值型別。