XLua在Unity中的用法摘要

版权声明:若有错误之处,欢迎指正!如有侵权,请联系删除! https://blog.csdn.net/qq_21397217/article/details/82983462

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的返回值和使用 outref 修饰的参数从左到右依次对应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#方法的 outref 参数会按照从左到右的顺序依次映射到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、12分别映射到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对象:

cast(myObj, typeof(CS.MyInterface))

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> 并且没有使用 BlackListAdditionalProperties 特性标签即可,例如 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 不生成某些类成员的适配代码

猜你喜欢

转载自blog.csdn.net/qq_21397217/article/details/82983462