XLua使用教程

XLua的Github仓库中提供了一个详细的使用教程、感兴趣的同学可以做进一步的了解。本文档只会讲解日常开发中使用频率最高的用法。

在C#操作Lua

1. 初始化LuaEnv

XLua.LuaEnv是xlua提供的一个Lua环境类,一个LuaEnv实例代表一个Lua虚拟机环境。在游戏开始运行时初始化完成。
初始化代码:

XLua.LuaEnv luaenv = new XLua.LuaEnv();
luaenv.DoString("CS.UnityEngine.Debug.Log('hello world')");
luaenv.Dispose();

开发的同学不需要初始化Lua环境,在项目运行时已经由AgentSystem负责初始化完成,可以通过AgentSystem的单例获取到LuaEnv实例,通过以下代码获取Lua实例:

XLua.LuaEnv luaEnv = AgentSystem.AgentSystem.singleton._xlua;

关于AgentSystem更多的内容,会在后面做进一步的讲解,目前先关注XLua的使用。

2. XLua.LuaTable的使用

LuaTable是XLua提供的Lua 中的table类型的C#实现,其实例对应一个lua中的表。LuaEnv中的Global字段就是一个特殊的表,这张表是映射到lua中的全局表的。它提供了一系列访问和操作lua表的函数。主要分为两类函数:Get类用于获取表中的字段,Set类用于设置表中的字段。

Get方法
  • API介绍
方法签名 说明
T Get<T>(object key) key参数是object类型,会存在装箱,不建议使用
void Get<T, V>(T key, out V value) 泛型版本,传递值类型无装箱,返回值以out参数返回
V Get<T, V>(T key) 泛型版本,传递值类型无装箱
V Get<V>(string key) 当table的key是字符串时可以使用该接口
T GetInPath<T>(string path) 当要获取的值在嵌套表中时使用,减少调用次数,路径形如 power.action.value

例子:

  1. Get<T, V>(T key) 方法
    XLua.LuaEnv env = AgentSystem.AgentSystem.singleton._xlua;
    env.DoString(@"testInt = 5");
    XLua.LuaTable _G = env.Global;
    int testInt = _G.Get<string, int>("testInt");
    UnityEngine.Debug.Log("获取Lua全局表中的testInt字段,值为:" + testInt);
    
  2. GetInPath<T>(string path)方法
    XLua.LuaEnv env = AgentSystem.AgentSystem.singleton._xlua;
    env.DoString(@"testTable = {a=true,b='HelloWorld'}");
    XLua.LuaTable _G = env.Global;
    bool a = _G.GetInPath<bool>("testTable.a");
    string b = _G.GetInPath<string>("testTable.b");
    UnityEngine.Debug.Log("Lua全局表中的testTable中的a值为:" + a.ToString());
    UnityEngine.Debug.Log("Lua全局表中的testTable中的b值为:" + b);
    
  • table映射C#复杂类型
    当获取lua的表时,可以通过上面的泛型接口指定table映射到C#中的哪个类型,下面是官方给出的几种映射方式的介绍:

    1. 映射到普通class或struct
      定义一个class,有对应于table的字段的public属性,而且有无参数构造函数即可,比如对于{f1 = 100, f2 = 100}可以定义一个包含public int f1;public int f2;的class。 这种方式下xLua会帮你new一个实例,并把对应的字段赋值过去。

      table的属性可以多于或者少于class的属性。可以嵌套其它复杂类型。 要注意的是,这个过程是值拷贝,如果class比较复杂代价会比较大。而且修改class的字段值不会同步到table,反过来也不会。

      这个功能可以通过把类型加到GCOptimize生成降低开销。 那有没有引用方式的映射呢?有,下面这个就是:

    2. 映射到一个interface
      这种方式依赖于生成代码(如果没生成代码会抛InvalidCastException异常),代码生成器会生成这个interface的实例,如果get一个属性,生成代码会get对应的table字段,如果set属性也会设置对应的字段。甚至可以通过interface的方法访问lua的函数。

    3. 更轻量级的by value方式:映射到Dictionary<>,List<>
      不想定义class或者interface的话,可以考虑用这个,前提table下key和value的类型都是一致的。

    4. 另外一种by ref方式:映射到LuaTable类
      这种方式好处是不需要生成代码,但也有一些问题,比如慢,比方式2要慢一个数量级,比如没有类型检查。

      使用建议:

      1. 对于一次性的获取table数据的操作,建议使用映射到class、struct 或者是更轻量的Dictionary<>,List<>目前我们项目中遇到的大部分需求也都是这种的。
      2. 对于映射到interface的方式,由于需要生成代码,在日常开发中使用较为复杂,不是对性能要求很高的情况下,不建议使用该方式。
      3. 在需要对某个lua表中的具体字段进行读取,而不需要表中的所有数据时可以使用映射LuaTable的方式进行获取。相较于上面两种方式的全量数据获取,虽然LuaTable在读取上性能较差,但在获取复杂结构的表中数据时,该方式是较优的选择。
Set方法
  • API介绍
方法签名 说明
void Set<T, V>(T key, V value) 设置字段的泛型版本,指定对应类型不会产生装箱消耗
void SetInPath<T>(string path, T val) 当要设置的值在嵌套表中时使用,减少调用次数,路径形如 power.action.value
  • 例子
  1. Set<T, V>(T key, V value) 方法
    XLua.LuaEnv env = AgentSystem.AgentSystem.singleton._xlua;
    env.DoString(@"testInt = 5");
    XLua.LuaTable _G = env.Global;
    _G.Set<string, int>("testInt",233);
    UnityEngine.Debug.Log("设置Lua全局表中的testInt字段,值为:" + _G.Get<int>("testInt"));
    
  2. GetInPath<T>(string path)方法
    XLua.LuaEnv env = AgentSystem.AgentSystem.singleton._xlua;
    env.DoString(@"testTable = {a=true,b='HelloWorld'}");
    XLua.LuaTable _G = env.Global;
    _G.SetInPath<bool>("testTable.a",false);
    _G.SetInPath<string>("testTable.b","HelloWorld Again");
    UnityEngine.Debug.Log("设置Lua全局表中的testTable中的a值为:" + _G.GetInPath<bool>("testTable.a").ToString());
    UnityEngine.Debug.Log("设置Lua全局表中的testTable中的b值为:" + _G.GetInPath<string>("testTable.b"));
    

3. XLua.LuaFunction的使用

LuaFunction是XLua提供的Lua中的function函数类型的C#实现,其实例对应一个lua中的函数。
同LuaTable中的大多数API一样,LuaFunction中主要是提供了一系列泛型方法用于lua函数的调用。
这里提供两个接口的调用实例,更多方法调用方式大家由此举一反三。

Action<T1,T2>(T1 a1,T2 a2)方法
XLua.LuaEnv env = AgentSystem.AgentSystem.singleton._xlua;
env.DoString(@"
TestFunc = function(arg1,arg2)
    CS.UnityEngine.Debug.Log('C#调用Lua-----arg1: '..tostring(arg1) .. ‘ arg2: ’..tostring(arg2))
end");
XLua.LuaTable _G = env.Global;
XLua.LuaFunction testFunc = _G.Get<XLua.LuaFunction>("TestFunc");
testFunc.Action<int, bool>(233, true);
T2 Func<T1,T2>(T1 a1)方法
XLua.LuaEnv env = AgentSystem.AgentSystem.singleton._xlua;
env.DoString(@"
    TestFunc = function(arg1)
    CS.UnityEngine.Debug.Log('C#调用Lua-----arg1: '..tostring(arg1)')
    return 'HelloWorld'
end");
XLua.LuaTable _G = env.Global;
XLua.LuaFunction testFunc = _G.Get<XLua.LuaFunction>("TestFunc");
string res = testFunc.Func<int, string>(233);
UnityEngine.Debug.Log("C#调用Lua-----return: " + res);

在Lua对C#进行操作

new C#对象

你在C#这样new一个对象:

var newGameObj = new UnityEngine.GameObject();

对应到Lua是这样:

local newGameObj = CS.UnityEngine.GameObject()

基本类似,除了:

1. lua里头没有new关键字;
2. 所有C#相关的都放到CS下,包括构造函数,静态成员属性、方法;

如果有多个构造函数呢?放心,xlua支持重载,比如你要调用GameObject的带一个string参数的构造函数,这么写:

local newGameObj2 = CS.UnityEngine.GameObject('helloworld')

访问C#静态属性,方法

读静态属性
CS.UnityEngine.Time.deltaTime
写静态属性
CS.UnityEngine.Time.timeScale = 0.5
调用静态方法
CS.UnityEngine.GameObject.Find('helloworld')

小技巧:如果需要经常访问的类,可以先用局部变量引用后访问,除了减少敲代码的时间,还能提高性能:

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

访问C#成员属性,方法

读成员属性
testobj.DMF
写成员属性
testobj.DMF = 1024
调用成员方法

注意:调用成员方法,第一个参数需要传该对象,建议用冒号语法糖,如下

testobj:DMFunc()
父类属性,方法

xlua支持(通过派生类)访问基类的静态属性,静态方法,(通过派生类实例)访问基类的成员属性,成员方法

参数的输入输出属性(out,ref)

Lua调用侧的参数处理规则:C#的普通参数算一个输入形参,ref修饰的算一个输入形参,out不算,然后从左往右对应lua 调用侧的实参列表;

Lua调用侧的返回值处理规则:C#函数的返回值(如果有的话)算一个返回值,out算一个返回值,ref算一个返回值,然后从左往右对应lua的多返回值。

重载方法

直接通过不同的参数类型进行重载函数的访问,例如:

testobj:TestFunc(100)
testobj:TestFunc('hello')

将分别访问整数参数的TestFunc和字符串参数的TestFunc。

注意:xlua只一定程度上支持重载函数的调用,因为lua的类型远远不如C#丰富,存在一对多的情况,比如C#的int,float,double都对应于lua的number,上面的例子中TestFunc如果有这些重载参数,第一行将无法区分开来,只能调用到其中一个(生成代码中排前面的那个)

操作符

支持的操作符有:+,-,*,/,==,一元-,<,<=, %,[]

参数带默认值的方法

和C#调用有默认值参数的函数一样,如果所给的实参少于形参,则会用默认值补上。

可变参数方法

对于C#的如下方法:

void VariableParamsFunc(int a, params string[] strs)

可以在lua里头这样调用:

testobj:VariableParamsFunc(5, 'hello', 'john')
使用Extension methods

在C#里定义了,lua里就能直接使用。

泛化(模版)方法

不直接支持,可以通过Extension methods功能进行封装后调用。

枚举类型

枚举值就像枚举类型下的静态属性一样。

testobj:EnumTestFunc(CS.Tutorial.TestEnum.E1)

上面的EnumTestFunc函数参数是Tutorial.TestEnum类型的。

枚举类支持__CastFrom方法,可以实现从一个整数或者字符串到枚举值的转换,例如:

CS.Tutorial.TestEnum.__CastFrom(1)
CS.Tutorial.TestEnum.__CastFrom('E1')
delegate使用(调用,+,-)

C#的delegate调用:和调用普通lua函数一样

+操作符:对应C#的+操作符,把两个调用串成一个调用链,右操作数可以是同类型的C# delegate或者是lua函数。

-操作符:和+相反,把一个delegate从调用链中移除。

Ps:delegate属性可以用一个luafunction来赋值。

event

比如testobj里头有个事件定义是这样:public event Action TestEvent;

增加事件回调

testobj:TestEvent('+', lua_event_callback)

移除事件回调

testobj:TestEvent('-', lua_event_callback)
C#复杂类型和table的自动转换

对于一个有无参构造函数的C#复杂类型,在lua侧可以直接用一个table来代替,该table对应复杂类型的public字段有相应字段即可,支持函数参数传递,属性赋值等,例如:
C#下B结构体(class也支持)定义如下:

public struct A
{
    public int a;
}

public struct B
{
    public A b;
    public double c;
}

某个类有成员函数如下:

void Foo(B b)

在lua可以这么调用

obj:Foo({b = {a = 100}, c = 200})
获取类型(相当于C#的typeof)

比如要获取UnityEngine.ParticleSystem类的Type信息,可以这样

typeof(CS.UnityEngine.ParticleSystem)
“强”转

lua没类型,所以不会有强类型语言的“强转”,但有个有点像的东西:告诉xlua要用指定的生成代码去调用一个对象,这在什么情况下能用到呢?有的时候第三方库对外暴露的是一个interface或者抽象类,实现类是隐藏的,这样我们无法对实现类进行代码生成。该实现类将会被xlua识别为未生成代码而用反射来访问,如果这个调用是很频繁的话还是很影响性能的,这时我们就可以把这个interface或者抽象类加到生成代码,然后指定用该生成代码来访问:

cast(calc, typeof(CS.Tutorial.Calc))

上面就是指定用CS.Tutorial.Calc的生成代码来访问calc对象。