天天看點

XLua在Unity中的用法摘要XLua在Unity中的用法摘要

XLua在Unity中的用法摘要

整理自官方教程:

https://github.com/Tencent/xLua/blob/master/Assets/XLua/Doc/XLua教程.md

https://github.com/Tencent/xLua/blob/master/Assets/XLua/Doc/configure.md

其他官方文檔連結:

常見問題解答:https://github.com/Tencent/xLua/blob/master/Assets/XLua/Doc/faq.md

XLua API:https://github.com/Tencent/xLua/blob/master/Assets/XLua/Doc/XLua_API.md

熱更新檔操作指南:https://github.com/Tencent/xLua/blob/master/Assets/XLua/Doc/hotfix.md

XLua增加删除第三方Lua庫:https://github.com/Tencent/xLua/blob/master/Assets/XLua/Doc/XLua增加删除第三方lua庫.md

提示: 搜尋關鍵字“建議”檢視官方建議用法,也可以搜尋其他關鍵字快速查找相關内容。

1. 安裝xLua與快速入門

1.1 下載下傳xLua

在下載下傳頁面中下載下傳所需資源,其中xlua_vx.x.x.zip和xlua_vx.x.x_luajit.zip是XLua架構的兩個不同版本,二者互斥,必須二選一。

  • xlua_vx.x.x.zip - 【必須/二選一】用于Unity的xLua的Lua版本,其中x.x.x為版本号
  • xlua_vx.x.x_luajit.zip - 【必須/二選一】用于Unity的xLua的LuaJit版本,性能更好
  • xlua_vx.x.x_example.zip - 【非必須】用法示例
  • xlua_vx.x.x_tutorial.zip - 【非必須】官方教程的配套代碼
  • xlua_vx.x.x_general.zip - 【非必須】xLua的通用版本,不局限于Unity

1.2 安裝xLua

以xlua_vx.x.x.zip為例,解壓xlua_vx.x.x.zip,将其中的Assets檔案夾與希望使用xLua的Unity工程的Assets檔案夾合并,不要更改Assets檔案夾的目錄結構。合并完成後,即可在代碼中使用xLua。

如果要将xLua安裝到其他目錄,請參考FAQ。

1.3 簡單說明與示例

1.3.1 執行個體化與釋放LuaEnv對象

使用xLua時,首先要執行個體化一個LuaEnv對象:

LuaEnv luavm = new LuaEnv();
                

每個LuaEnv執行個體對應一個Lua虛拟機。出于開銷考慮,建議建立一個全局唯一的LuaEnv執行個體,之後Lua方法調用,都通過該執行個體來完成。

當不再使用LuaEnv對象後,要将其釋放:

luavm.Dispose();
                

1.3.2 在C#中執行Lua代碼

通過LuaEnv.DoString(string)方法來在C#中執行Lua代碼,代碼的執行方式有兩種。

第一種方式是直接通過參數傳入Lua代碼文本,但不建議使用這種方式。下面的示例中傳入了一行Lua代碼

print('hello world')

,将會在Unity控制台列印hello world。

luavm.DoString("print('hello world')");
                

第二種方式是通過參數傳遞Lua代碼檔案名稱(或位置),建議使用這種方式。下面的示例中,将會查找名為

hello_world.lua

的Lua腳本檔案并執行該檔案中的Lua代碼。

luavm.DoString("require 'lua_script_file'");
                

建議的腳本加載方式是:整個程式中隻有一處

DoString("require 'main'")

,然後在main.lua中加載其他的Lua腳本(類似于在Lua指令行執行

$ lua main.lua

)。

指令

require

會依次調用不同的加載器去加載Lua檔案,當某個加載器成功加載Lua檔案後就不再調用其他加載器,如果所有加載器都沒能加載到參數中指定地檔案,則報告錯誤。xLua對Lua腳本檔案的存放位置有要求,如果要加載自定義位置的、來自網絡的、經過壓縮或加密的Lua檔案,則需要實作自定義加載器。下文中會介紹Lua檔案存放位置和自定義加載器的相關内容。

1.3.3 C#與Lua的互相調用

在C#中,使用

LuaEnv.Global.Get<T>("obj_name")

方法來擷取名為obj_name的Lua全局對象,該對象可以是任意能夠映射到Lua的C#類型;在Lua中,所有C#類都位于CS子產品中,可以直接使用

CS.命名空間.類名

CS.命名空間.類名.字段/屬性/方法

C#的類、字段、屬性和方法。例如,在Lua中調用Unity的Debug.Log()方法列印hello world:

luavm.DoString("CS.UnityEngine.Debug.Log('hello world')");

C#類型與Lua類型的映射方式以及更多方法調用細節将在下文進行說明。

1.3.4 生成代碼

通過Unity編輯器視窗的 XLua - Generate Code 選項可以生成用于實作C#和Lua互動的适配代碼。生成代碼後程式的性能更好,建議使用。如果沒有生成代碼,則xLua會使用反射進行互動,這種方式性能不高,但是能夠減小安裝包大小。

在Unity編輯器中,不生成代碼也能夠正常運作程式,建議在開發階段不要生成代碼。在打包手機版應用和做性能測試、調優前必須生成代碼。

用于生成代碼的C#特性标簽是

[CSharpCallLua]

[LuaCallCSharp]

,在下文中會有它們的使用示例。xLua中還有一些其他的用于控制代碼生成的特性标簽,可以在官方的xLua配置文檔中檢視它們的詳細資訊。

1.3.5 示例代碼

public class Example : MonoBehaviour
{
    private LuaEnv luavm;

    private void Start()
    {
        luavm = new LuaEnv();

        // 直接執行Lua代碼
        luavm.DoString("print('hello world')");
        // 查找Lua腳本檔案并執行其中的代碼
        //luavm.DoString("require 'lua_script_file'");
    }

    private void OnDestroy()
    {
        // 記得要釋放
        if(luavm != null)
        {
            luavm.Dispose();
        }
    }
}
                

2. Lua腳本檔案的加載位置與自定義加載器

2.1 Lua腳本檔案的加載位置

假設目前系統中沒有名為 not_exist.lua 的Lua腳本檔案,那麼在執行代碼

luavm.DoString("require 'not_exist'");

時,Unity控制台會列印如下資訊:

no field package.preload['not_exist']
no such builtin lib 'not_exist'
no such file 'not_exist' in CustomLoaders!
no such resource 'not_exist.lua'
no file 'D:\Unity\Unity2017.4.4\Editor\lua\not_exist.lua'
no file 'D:\Unity\Unity2017.4.4\Editor\lua\not_exist\init.lua'
no file 'D:\Unity\Unity2017.4.4\Editor\not_exist.lua'
no file 'D:\Unity\Unity2017.4.4\Editor\not_exist\init.lua'
no file 'D:\Unity\Unity2017.4.4\Editor\..\share\lua\5.3\not_exist.lua'
no file 'D:\Unity\Unity2017.4.4\Editor\..\share\lua\5.3\not_exist\init.lua'
no file '.\not_exist.lua'
no file '.\not_exist\init.lua'
no file 'D:\Unity\Unity2017.4.4\Editor\not_exist.dll'
no file 'D:\Unity\Unity2017.4.4\Editor\..\lib\lua\5.3\not_exist.dll'
no file 'D:\Unity\Unity2017.4.4\Editor\loadall.dll'
no file '.\not_exist.dll'
no such file 'not_exist.lua' in streamingAssetsPath!
           
在Lua中使用語句

print(package.path)

可以直接輸出Lua檔案的加載路徑,但是這樣輸出的是所有路徑名稱連在一起的字元串,不友善檢視。

從上表可以看到xLua會在哪些檔案夾中查找Lua腳本檔案,需要将Lua腳本檔案放在這些位置系統才能正确加載它們。除了在Unity安裝檔案夾中查找Lua腳本檔案外,xLua還會在項目的下列位置查找Lua腳本檔案:

  • 内置Lua庫
  • 自定義加載器
  • Resources檔案夾
  • 項目根目錄(與Assets檔案夾同級,打包後則與可執行exe檔案同級,下同)
  • 項目根目錄中同名檔案夾中的init.lua
  • 項目根目錄中的同名DLL檔案
  • Assets/StreamingAssets檔案夾

需要注意的地方是,Unity系統在打包應用時無法識别擴充名為lua的檔案,是以當需要将Lua腳本檔案作為TextAsset打包時(例如放到Resources檔案夾中),應該将Lua腳本檔案的擴充名改為txt,例如

my_script.lua.txt

如果需要将Lua腳本檔案放置到自定義位置,或者加載網絡檔案、壓縮檔案或者加密檔案,則需要實作自定義加載器。其中自定義Lua檔案加載位置的功能也可以通過直接在Lua腳本中向

package.path

中添加路徑名稱來實作。例如,添加 /Assets/myluafiles/ 檔案夾到加載路徑:

luavm.DoString (@"package.path = './Assets/myluafiles/?.lua;' .. package.path");
                

注意要對Lua檔案名使用半角問号(

?

)通配符,多個路徑之間使用半角分号(

;

)分隔,并且檔案夾層級使用斜杠(

/

)而不是反斜杠(

\

)表示。

2.2 自定義加載器

實作自定義加載器隻需要建立 CustomLoader 委托執行個體并通過

LuaEnv.AddLoader()

方法将其添加到LuaEnv執行個體中即可。

CustomLoader委托的簽名為:

public delegate byte[] CustomLoader(ref string filepath);
                

示例代碼:

public class CustomLoaderExample : MonoBehaviour
{
    private void Start()
    {
        LuaEnv luavm = new LuaEnv();
        // 添加自定義加載器
        luavm.AddLoader(MyCustomLoader);

        string filepath = Application.dataPath + "/myluafiles/test.lua";
        luavm.DoString(string.Format("require '{0}'", filepath));
        luavm.Dispose();
    }

    // 自定義的Lua檔案加載器。
    // 參數filepath:【require 'filepath'】中的【filepath】
    // 傳回值:檔案内容
    private byte[] MyCustomLoader(ref string filepath)
    {
        // 通過自定義filepath的解析方式來實作特殊加載功能

        // 1. 從指定的路徑加載Lua檔案
        if (filepath.Contains("/"))
        {
            if (File.Exists(filepath))
            {
                return File.ReadAllBytes(filepath);
                //string script = File.ReadAllText(filepath);
                //return System.Text.Encoding.UTF8.GetBytes(script);
            }
        }
        // 2. 從自定義的預設位置加載Lua檔案
        else
        {
            string defaultFolder = Application.dataPath + "/myluafiles/";
            string file = defaultFolder + filepath + ".lua";
            if (File.Exists(file))
            {
                return File.ReadAllBytes(file);
            }
        }

        // 其他加載方式:
        // 3. 加載網絡檔案
        // 4. 加載壓縮檔案并解壓
        // 5. 加載加密檔案并解密

        return null;
    }
}
                

3. 在C#中通路Lua資料結構

本章中提到的所有方法都可以在xLua/Assets/XLua/Tutorial/CSharpCallLua/CSCallLua.cs中找到使用示例。

在C#中通路Lua資料結構的主要方法是

LuaEnv.Global.Get<T>(string name)

,該方法具有多個重載,各個重載的具體差別請檢視xLua API文檔。

3.1 通路全局的基本資料類型

luavm.Global.Get<int>("a");  // 通路名為a的整型變量
luavm.Global.Get<bool>("b");  // 通路名為b的布爾變量
luavm.Global.Get<string>("c");  // 通路名為c的字元串變量
                

3.2 通路全局的table

3.2.1 将table映射到class或struct【值拷貝】

假設現在有如下的Lua資料結構:

my_table = {
    f1 = 1,
    f2 = 2,
    f3 = 'string',
    add = function(self, a, b)
        return a + b
    end
}
           

要将上面的Lua資料結構映射到C#,需要在C#中定義一個class或struct,其中含有同名的public字段,并且具有無參構造方法。以class為例:

public class TableClass
{
    public int f1;
    public int f2;
}
                

table和class的成員個數不必完全相同,在映射過程中,table中多出的成員會被忽略,class中多出的成員會被初始化成預設值。在此示例中,忽略了字元串f3和函數add()。

在使用這種方式時,可以為C#類型添加

[GCOptimize]

特性來降低生成開銷,具體說明請檢視xLua配置文檔。

需要注意的是,這一映射過程是值拷貝過程,對class對象的修改不會同步到table對象,反之亦然。

示例代碼:

TableClass table = luavm.Global.Get<TableClass>("my_table");
Debug.Log(table.f1 + table.f2);
                

3.2.2 将table映射到interface【引用形式】【建議用法】

将table映射到interface依賴代碼生成,如果沒有生成代碼會抛出

InvalidCastException

異常。接口方式實作的是引用形式的映射,對class對象的修改會同步到table對象,反之亦然。建議使用該方式進行映射。仍然以上一節中的Lua資料結構為例,現在需要定義一個與其相比對的C#接口,并為這個接口添加用于指明需要生成代碼的特性标簽

[CSharpCallLua]

[CSharpCallLua]
public interface ITable
{
    int f1 { get; set; }
    int f2 { get; set; }
    int add(int a, int b);
}
                

示例代碼:

ITable table = luavm.Global.Get<ITable>("my_table");
Debug.Log(table.add(table.f1, table.f2));
                

3.2.3 将table映射到Dictionary<TKey, TValue>和List【值拷貝】

如果不想定義class/struct或interface,可以選擇将table映射到Dictionary<TKey, TValue>或List這種更輕量級的方式。這種方式會選擇table中能夠比對上的成員進行映射,并且采用了值拷貝形式。

仍然以第一節中的Lua資料結構為例,将其映射到Dictionary<TKey, TValue>和List的示例代碼為:

// 映射到Dictionary<TKey, TValue>
// 因類型不比對,字元串f3和函數add()會被忽略
Dictionary<string, int> tableDict = luavm.Global.Get<Dictionary<string, int>>("my_table");
Debug.Log(tableDict["f1"] + tableDict["f2"]);

// 映射到List<T>
// 因類型不比對,字元串f3和函數add()會被忽略
List<int> tableList = luavm.Global.Get<List<double>>("my_table");
for(int i = 0; i < tableList.Count; i++)
{
    Debug.Log(tableList[i]);
}
                

3.2.4 将table映射到LuaTable類【引用形式】

将table映射到LuaTable類的好處是不需要生成代碼即可實作引用形式的映射,但其執行速度慢(比第2種方式要慢一個數量級),而且沒有類型檢查。

仍然以第一節中的Lua資料結構為例,将其映射到LuaTable的示例代碼為:

LuaTable luaTable = luavm.Global.Get<LuaTable>("my_table");
Debug.Log(luaTable.Get<int>("f1") + luaTable.Get<int>("f2") + luaTable.Get<string>("f3"));
                

3.3 通路全局的function

3.3.1 将function映射到delegate【建議用法】

将function映射到delegate是官方建議使用的方式。這種方式的好處是性能好,綁定一次即可重複使用,而且類型安全;其缺點是需要生成代碼,如果沒有生成代碼,則會抛出

InvalidCastException

異常。

在聲明delegate時,其通路權限應該是 public 的,delegate的每個普通參數和使用

ref

修飾的參數從左到右依次對應目标function的參數,

out

修飾的參數不會被映射到目标function的參數中;delegate的傳回值和使用

out

ref

修飾的參數從左到右依次對應function的(多個)傳回值。參數和傳回值支援各種基礎類型和複雜類型。

假設現在有如下的Lua function:

function luafunc(a, b, c, d)
    v3 = {x = a, y = b, z = c}
    sum = a + b + c + d
    pro = a * b * c * d
    return v3, sum, pro
end
                

則可以将其映射到下面的C# delegate中。在下面的示例代碼中,C# delegate的輸入參數a、b、c分别對應Lua function的參數a、b、c,C# delegate的傳回值和輸出參數sum、pro分别對應Lua function 的3個傳回值。C# delegate和Lua function的參數名稱不必完全相同,也不限定輸入輸出參數的順序,隻要類型比對即可。建議綁定一次重複使用,生成代碼後,通過C# delegate調用Lua function不會産生gc alloc。

// 聲明委托,輸出參數不是必須排在最後
[CSharpCallLua]
public delegate Vector3 LuaFuncDelegate(int a, int b, int c, out int sum, ref int pro);
// 綁定
LuaFuncDelegate luaFunc = luavm.Global.Get<LuaFuncDelegate>("luafunc");
// 調用
Vector3 v3;
int sum, pro = 4;
v3 = luaFunc(1, 2, 3, out sum, ref pro);
Debug.Log(v3 + " " + sum + " " + pro);
                

如果在釋放LuaEnv執行個體時報出

InvalidOperationException: try to dispose a LuaEnv with C# callback!

,說明代碼中有綁定了Lua function的委托執行個體沒有釋放,找到這個委托執行個體并将其釋放即可,具體資訊可以檢視官方的常見問題解答頁面。

3.3.2 将function映射到LuaFunction類

将function映射到LuaFunction類比較簡單,不需要生成代碼,但這種方式性能較差,而且沒有類型檢查。在LuaFunction類中有一個變參的

Call()

方法,可以傳遞任意類型的參數,這些參數對應Lua function的參數,這一方法的傳回值是一個object數組,其中的元素分别對應Lua function的多個傳回值。

仍然以上一節中給出的Lua function為例,相應的C#部分代碼是:

// 映射
LuaFunction luaFunc = luavm.Global.Get<LuaFunction>("luafunc");
// 調用
object[] results = luaFunc.Call(1, 2, 3, 4);
// 取值
// 注意,Lua function中的v3在這裡變成了LuaTable,其中含有x、y、z三個key
LuaTable table = results[0] as LuaTable;
Vector3 v3 = new Vector3(table.Get<int>("x"), table.Get<int>("y"), table.Get<int>("z"));
long sum = (long)results[1];
long pro = (long)results[2];
Debug.Log(v3 + " " + sum + " " + pro);
                

在上面的示例代碼中,sum和pro的類型由int變成了long,這是因為,在C#中參數(或字段)類型是object時,預設以long類型傳遞整數。如果要指明整數的類型,比如int,可以在Lua中使用XLua提供的

CS.XLua.Cast.Int32()

方法,例如:

function luafunc(a, b, c)
    v3 = {x = a, y = b, z = c}
    sum = CS.XLua.Cast.Int32(a + b + c + d)
    pro = CS.XLua.Cast.Int32(a * b * c * d)
    return v3, sum, pro
end
           

3.4 使用建議

在C#中通路Lua全局資料,尤其是通路table和function時,代價比較大,建議盡量減少通路次數。可以在程式初始化階段把要調用的Lua function綁定到C# delegate并緩存下來,以後直接調用這個delegate即可,table與之類似。

如果Lua方面的實作部分都以delegate和interface的方式提供,那麼使用方可以完全與xLua解耦 —— 由一個專門的子產品負責xLua的初始化以及delegate和interface的映射,然後把這些delegate和interface執行個體設定到要用到它們的地方。

4. 在Lua中調用C#

本章中提到的所有方法都可以在xLua/Assets/XLua/Tutorial/LuaCallCSharp/LuaCallCs.cs中找到使用示例。

在Lua中調用C#時,首先要注意以下幾點:

  1. xLua中所有的C#類都被放到了

    CS

    子產品中。
  2. Lua語言中沒有new關鍵字;
  3. Lua語言運算符:

    +

    ,

    -

    ,

    *

    ,

    /

    ,

    %

    ,

    ^

    ,

    ==

    ,

    ~=

    ,

    <

    ,

    >

    ,

    <=

    ,

    >=

    ,

    and

    ,

    or

    ,

    not

    ,

    ..

    ,

    #

  4. Lua語言不支援泛型
  5. Lua語言不支援類型轉換
  6. 辨別生成代碼的特性标簽:

    [LuaCallCSharp]

除此之外,在xLua中可以像寫普通的C#代碼那樣調用C#。

xLua支援以下功能:

  • 建立C#對象
  • 通過C#子類通路C#父類的靜态屬性和方法
  • 通過C#子類對象通路C#父類的成員屬性和方法
  • 帶有預設參數的C#方法
  • 帶有可變參數的C#方法
  • C#方法重載
  • C#擴充方法
  • C#操作符重載
  • C#枚舉
  • 自動轉換C#複雜類型和Lua table
  • C#的delegate和event

下面将對在Lua中通路C#時的幾點特殊情況加以說明,并在最後給出示例代碼。

4.1 Lua的點文法和冒号文法

在Lua中,使用點(

.

)文法調用對象的成員方法時方法的第一個參數應該傳入對象自身,而使用冒号(

:

)文法調用對象的成員方法時可以省略這一參數。建議使用冒号文法。示例代碼:

local gameObject = CS.UnityEngine.GameObject()
-- 使用冒号文法不用傳入對象自身
gameObject:SetActive(false)
-- 使用點文法需要傳入對象自身
gameObject.SetActive(gameObject, true)
           

4.2 C#複雜類型和Lua table的自動轉換

在Lua中可以直接使用table來代替帶有無參構造方法的C#複雜類型(class和struct)。下面示例中展示了C# Vector3和Lua table的自動轉換:

C#代碼:

[LuaCallCSharp]
public class MyClass
{
    public void ComplexStructTest(Vector3 v3)
    {
        Debug.Log("ComplexStructTest: " + v3);
    }
}
                

Lua代碼:

local myObj = CS.MyClass()
-- C# Vector3與Lua table的自動轉換
myObj:ComplexStructTest({x=1.0, y=2.0, z=3.0})
           

4.3 參數和傳回值的處理規則

參數處理規則:C#方法的普通參數和

ref

參數會按照從左到右的順序依次映射到Lua function的形參,

out

參數不會被映射到Lua function的形參。

傳回值處理規則:C#方法的傳回值會映射到Lua function的第一個傳回值,然後C#方法的

out

ref

參數會按照從左到右的順序依次映射到Lua function的其他傳回值。

C#示例代碼:

[LuaCallCSharp]
public class MyClass
{
    public int RefOutTest(int a, out int b, ref int c)
    {
        b = 32;
        return a + b + c;
    }
}
                

Lua示例代碼:

local myObj = CS.MyClass()
-- ret、1、2分别映射到C#方法的傳回值、參數a、參數c
local ret = myObj:RefOutTest(1, 2)
           

4.4 枚舉類型

在xLua中可以像使用C#類的靜态屬性一樣使用枚舉成員。枚舉的

__CastFrom()

方法可以将一個整數或字元串轉換到枚舉值。示例代碼:

C#代碼:

[LuaCallCSharp]
public enum MyEnum
{
    A, B, C
}
                

Lua代碼:

-- 通路枚舉成員
CS.MyEnum.A
-- 将整數轉換到枚舉值
CS.MyEnum.__CastFrom(1)
-- 将字元串轉換到枚舉值
CS.MyEnum.__CastFrom('C')
           

4.5 delegate和event

在xLua中可以像在C#中一樣使用

+

-

運算符向delegate調用鍊中添加方法,不過Lua中沒有

+=

-=

運算符。方法的添加順序會影響調用順序。需要注意的兩點是:

  1. 在C#中聲明delegate時需要為其添加一個預設的實作,否則在Lua中向其添加方法時會抛出異常;
  2. 調用delegate時應該使用點文法,如果使用冒号文法傳入的參數會變成

    nil

在xLua中為event添加和移除監聽的寫法有些不同,不能直接通過加減運算來實作,而是要使用

EventName('+', func_name)

EventName('-', func_name)

這種寫法來實作添加和移除監聽,并且需要使用冒号文法。另外,在xLua中不能直接通過

EventName(params)

這種形式來觸發事件,而是要在C#代碼中添加一個間接觸發方法。

C#示例代碼:

[LuaCallCSharp]
public class MyClass
{
    // 委托,需要有預設實作
    public Action<string> MyDelegate = (arg) => { };
    // 事件
    public event Action<string> MyEvent;
    // 在Lua中調用此方法間接觸發事件
    public void TriggerEvent(string arg)
    {
        if(MyEvent != null) MyEvent(arg);
    }
}
                

Lua示例代碼:

local function my_lua_callback(arg)
    print('my_lua_callback: ' .. arg)
end

local myObj = CS.MyClass()

-- delegate使用點文法,否則調用委托時參數會變成nil
-- Lua中沒有+=操作符,方法的添加順序會影響調用順序
myObj.MyDelegate = myObj.MyDelegate + my_lua_callback
myObj.MyDelegate('delegate callback')

-- event使用冒号文法,不能直接使用MyEvent來觸發事件
myObj:MyEvent('+', my_lua_callback)
myObj:TriggerEvent('event callback 1')
myObj:MyEvent('-', my_lua_callback)
myObj:TriggerEvent('event callback 2')
           

4.6 擴充方法

在C#中定義了擴充方法後,為該擴充方法所在的類添加

[LuaCallCSharp]

特性标簽,就可以在Lua中直接使用這個擴充方法,

4.7 泛型方法

xLua不支援泛型方法,但可以使用擴充方法為泛型方法添加針對特定類型的轉換方法,實作一個假的泛型。例如,下面的示例為GenericTest()方法實作了針對string類型的轉換方法:

C#代碼:

[LuaCallCSharp]
public class MyClass
{
    public void GenericTest<T>(T t)
    {
        Debug.Log("GenericTest: " + typeof(T) + "-" + t.ToString());
    }
}

[LuaCallCSharp]
public static class MyExtensions
{
    public static void GenericTestOfString(this MyClass obj, string arg)
    {
        obj.GenericTest<string>(arg);
    }
}
                

Lua代碼:

local myObj = CS.MyClass()
myObj:GenericTestOfString('fake')
           

4.8 類型轉換

Lua沒有類型轉換功能,但xLua提供了

cast()

方法實作了類似的功能,該方法讓xLua使用指定的生成代碼去調用一個對象。有些時候,第三方庫對外暴露的接口是一個interface或者抽象類,其實作類是隐藏的,這時就沒辦法對實作類進行代碼生成,xLua會通過反射來通路這個實作類。如果這種通路很頻繁,會很影響性能。這時就可以把第三方庫暴露出來的interface或抽象類添加到生成代碼清單,然後指定用這個interface或抽象類的生成代碼來通路對象,類似于将對象轉換成了interface或抽象類的類型。例如,下面的Lua示例代碼指定了使用CS.MyInterface的生成代碼來通路myObj對象:

4.9 完整示例代碼

建議在Lua種使用局部變量緩存需要經常通路的類,這樣不僅能夠提高開發效率,還能提高性能。例如:

local GameObject = CS.UnityEngine.GameObject
GameObject.Find('obj_name')
           

C#示例代碼:

namespace MyNamespace
{
    [LuaCallCSharp]
    public class MyClass
    {
        public string id;
        // delegate需要有預設值,否則Lua中會報錯
        public Action<string> MyDelegate = (arg) => { };
        public event Action<string> MyEvent;

        public MyClass() { id = "id_default"; }

        public MyClass(string id) { this.id = id; }

        // 帶有預設參數的方法
        public void DefaultParamsTest(int a, int b = 1)
        {
            Debug.Log("DefaultParamsTest: " + (a + b));
        }
        // 帶有可變參數的方法
        public void VariableParamsTest(int a, params int[] args)
        {
            int sum = a;
            foreach (var arg in args) sum += arg;
            Debug.Log("VariableParamsTest: " + sum);
        }
        // 帶有ref、out參數的方法
        public int RefOutTest(int a, out int b, ref int c)
        {
            b = 32;
            return a + b + c;
        }
        // 帶有複雜類型(非基本類型)參數的方法
        public void ComplexStructTest(Vector3 v3)
        {
            Debug.Log("ComplexStructTest: " + v3);
        }
        // 枚舉
        public void EnumTest(MyEnum e)
        {
            Debug.Log("EnumTest: " + e.ToString());
        }
        // 觸發事件,不能再Lua中直接使用MyEvent觸發事件,添加一層轉接
        public void TriggerEvent(string arg)
        {
            if (MyEvent != null)
            {
                MyEvent(arg);
            }
        }
        // 操作符重載
        public static MyClass operator +(MyClass a, MyClass b)
        {
            MyClass sum = new MyClass(a.id + "&" + b.id);
            return sum;
        }
        // 泛型方法
        public void GenericTest<T>(T t)
        {
            Debug.Log("GenericTest: " + typeof(T) + "-" + t.ToString());
        }
    }

    [LuaCallCSharp]
    public enum MyEnum
    {
        A, B, C
    }

    [LuaCallCSharp]
    public static class MyExtensions
    {
        // 擴充方法
        public static void MyExtensionMethod(this MyClass obj, string msg)
        {
            Debug.Log("MyExtensionMethod - " + msg);
        }
        // xLua不支援泛型方法假裝支援string泛型
        public static void GenericTestOfString(this MyClass obj, string arg)
        {
            obj.GenericTest<string>(arg);
        }
    }
}

// 這裡請參考第5.2節(靜态清單)
[LuaCallCSharp]
public static class CsLuaCaster
{
    // 靜态清單
    public static List<Type> LuaCallCsCastList = new List<Type>()
    {
        typeof(Action),
        typeof(Action<string>)
    };
}
                

Lua示例代碼:

-- 緩存經常通路的類
local Time = CS.UnityEngine.Time
-- 讀靜态屬性
Time.deltaTime
-- 寫靜态屬性
Time.timeScale = 0.5
-- 調用靜态方法
local obj = CS.UnityEngine.GameObject.Find('obj_name')

-- 讀(父類)成員屬性
obj.name
-- 寫(父類)成員屬性
obj.name = 'new_name'
-- 調用成員方法,注意冒号文法和點文法的參數差別
obj:SetActive(false)
obj.SetActive(obj, true)

-- 用于測試delegate和event
function my_lua_callback(arg)
	print('my_lua_callback: ' .. arg)
end

-- 通路MyClass類
function lua_call_cs()
	local MyClass = CS.MyNamespace.MyClass

	-- 執行個體化C#對象,方法重載
	local myObj0 = MyClass()
	local myObj1 = MyClass('id_1')
	-- 操作符重載
	local myObj2 = myObj0 + myObj1
	print('Operator Overload: ' .. myObj2.id)
	-- 預設參數
	myObj0:DefaultParamsTest(1)
	-- 可變參數
	myObj0:VariableParamsTest(1, 2, 3)
	-- ref、out參數
	local ret = myObj0:RefOutTest(1, 2)
	print('RefOutTest: ' .. ret)
	
	-- C#複雜類型與Lua table的自動轉換
	myObj0:ComplexStructTest({x=1.0, y=2.0, z=3.0})
	
	-- 枚舉,像使用靜态屬性一樣使用枚舉
	local MyEnum = CS.MyNamespace.MyEnum
	myObj0:EnumTest(MyEnum.A)
	-- 枚舉的__CastFrom()方法可以将一個整數或字元串轉換到枚舉值
	myObj0:EnumTest(MyEnum.__CastFrom(1))
	myObj0:EnumTest(MyEnum.__CastFrom('C'))


	-- delegate使用點文法,否則調用委托時參數會變成nil
	-- Lua中沒有+=操作符,方法的添加順序會影響調用順序
	myObj0.MyDelegate = myObj0.MyDelegate + my_lua_callback
	myObj0.MyDelegate('delegate callback')
	-- event使用冒号文法,不能直接使用MyEvent來觸發事件
	myObj0:MyEvent('+', my_lua_callback)
	myObj0:TriggerEvent('event callback 1')
	myObj0:MyEvent('-', my_lua_callback)
	myObj0:TriggerEvent('event callback 2')

	-- 擴充方法
	myObj0:MyExtensionMethod('hello')

	-- xLua不支援泛型方法,這裡是假的泛型方法
	myObj0:GenericTestOfString('fake')

	-- Lua沒有類型轉換功能,但xLua提供了cast方法實作了類似的功能
	-- 指定使用MyClass類的生成代碼通路myObj0,類似于把myObj0轉換成MyInterface類型
	-- cast(myObj0, typeof(CS.MyNamespace.MyInterface))
end

lua_call_cs()

           

5. 代碼生成配置

xLua的所有配置都支援3種方式:特性标簽、靜态清單和動态清單。

對于xLua的配置,有兩個必須和兩個建議:

  • 清單方式都必須在靜态類中進行配置
  • 清單方式都必須使用靜态字段/屬性
  • 建議不要使用特性标簽,這種方式在IL2CPP模式下會增加不少的代碼量
  • 建議将清單方式的配置放到Editor目錄(如果是

    Hotfix

    配置,而且類位于

    Assembly-CSharp.dll

    之外的其它dll中,必須放Editor目錄)

5.1 特性标簽

xLua通過白名單來指明要為哪些類生成代碼,而白名單通過特性标簽(Attribute)來配置。為類添加

[CSharpCallLua]

[LuaCallCSharp]

特性标簽後,通過Unity編輯器菜單欄的 XLua - Generate Code 按鈕即可為該類生成适配代碼。

如果一個C#類型添加了

[LuaCallCSharp]

特性标簽,那麼xLua會生成這個類的适配代碼,包括構造方法、成員屬性和方法、靜态屬性和方法;如果沒有為類型添加這個特性标簽,那麼xLua會嘗試使用性能較差的反射方式通路C#類。xLua隻會為添加了該特性标簽的類型生成代碼,不會自動為該類型的父類生成代碼,當子類對象通路父類方法時,如果父類也添加了特性标簽,則執行父類的适配代碼,否則将嘗試使用反射來通路父類。反射方式除了性能不佳外,在IL2CPP模式下還有可能因為代碼裁剪而導緻無法通路。建議所有要在Lua中通路的C#代碼,要麼加上

[LuaCallCSharp]

特性标簽,要麼加上

[ReflectionUse]

特性标簽,這樣才能夠保證程式在各平台都能正常運作。

如果需要把一個Lua function綁定到C# delegate,或者需要把一個Lua table映射到C# interface,那麼需要為delegate或者interface添加

[CSharpCallLua]

特性标簽。

特性标簽友善使用,但在IL2CPP模式下會增加不少的代碼量,不建議使用。

5.2 靜态清單

有時候無法直接給一個類型添加特性标簽,例如系統API、沒有源碼的DLL等,這時可以在一個靜态類中聲明一個靜态字段,這一字段隻要實作了

IEnumerable<Type>

并且沒有使用

BlackList

AdditionalProperties

特性标簽即可,例如

List<Type>

,然後為這個靜态類或靜态字段添加

[LuaCallCSharp]

特性标簽即可。建議将靜态清單放到Editor目錄中。示例代碼:

[LuaCallCSharp]
public static class StaticListClass
{
    // 靜态清單
    public static List<Type> LuaCallCsStaticList = new List<Type>()
    {
        typeof(GameObject),
        typeof(Action<string>),
        typeof(Dictionary<string, GameObject>),
    };
}
                

5.3 動态清單

與靜态清單類似,動态清單需要在一個靜态類中聲明一個靜态屬性,并為其添加相應的特性标簽。在靜态屬性的Getter代碼塊中,可以實作很多效果,例如按命名空間配置、按程式及配置等。建議将動态清單放到Editor目錄中。示例代碼:

public static class DynamicListClass
{
    [Hotfix]
    public static List<Type> LuaCallCsDynamicList
    {
        get
        {
            return (
                from type in Assembly.Load("Assembly-CSharp").GetTypes()
                where type.Namespace == "Xxx"
                select type
            ).ToList();
        }
    }
}
                

5.4 xLua特性标簽清單

xLua特性标簽的詳細介紹請檢視xLua配置文檔。

特性标簽 用途簡述
XLua.LuaCallCSharp 生成C#類型的适配代碼
XLua.CSharpCallLua 生成C# delegate或interface的适配代碼
XLua.ReflectionUse 阻止IL2CPP進行代碼裁剪
XLua.DoNotGen 不生成某個方法、字段或屬性的适配代碼,通過反射通路
XLua.GCOptimize 優化C#純值類型的轉換性能
XLua.AdditionalProperties 通過屬性通路私有字段
XLua.BlackList 不生成某些類成員的适配代碼