Xlua学习笔记(C#调用Lua)

XLua为Unity、 .Net、 Mono等C#环境增加Lua脚本编程的能力,借助xLua,这些Lua代码可以方便的和C#相互调用。 其实除了XLua实现了相互调用的能力,另外通过XLua很容易实现一种热更新方案,动态替换Lua脚本内容,可以看出XLua还是很强的,XLua的原理可以参考另一篇博客《XLua实现原理》。

导入XLua

项目导入Xlua,项目地址 https://github.com/Tencent/xLua

Helloworld

 1using System;
 2using System.Collections;
 3using System.Collections.Generic;
 4using UnityEngine;
 5
 6// XLua namespace
 7using XLua;
 8
 9
10public class HelloWord01 : MonoBehaviour
11{
12    private LuaEnv env;
13    // Start is called before the first frame update
14    void Start()
15    {
16        // 创建Lua环境
17        env = new LuaEnv();
18        env.DoString("print('Hello XLua')");
19
20        // Lua内部调用C#
21        env.DoString("CS.UnityEngine.Debug.Log('Hello XLua')");
22
23        // // 释放Env 推荐在MonoBehaviour的生命周期里释放
24        // env.Dispose();
25    }
26
27    // Update is called once per frame
28    void Update()
29    {
30
31    }
32
33    private void OnDestroy()
34    {
35        env.Dispose();
36    }
37}

上面是一个Helloword的示例程序,展示了C#如何直接执行Lua语句,同时也展示了Lua调用C#!

一个LuaEnv实例对应一个Lua虚拟机,处于开销的考虑,建议全局唯一

TextAsset绑定Lua

运行加载Lua源文件:

 1using System;
 2using System.Collections;
 3using System.Collections.Generic;
 4using UnityEngine;
 5using XLua;
 6public class HelloWorld02 : MonoBehaviour
 7{
 8    private LuaEnv luaEnv;
 9
10    // private TextAsset textScript;
11
12    // Start is called before the first frame update
13    void Start()
14    {
15        luaEnv = new LuaEnv();
16        TextAsset textAsset = Resources.Load<TextAsset>("helloworld.lua");
17        print("Lua文件内容是" + textAsset.text);
18
19        // 执行Lua
20        luaEnv.DoString(textAsset.text);
21    }
22
23    // Update is called once per frame
24    void Update()
25    {
26
27    }
28
29    private void OnDestroy()
30    {
31        luaEnv.Dispose();
32    }
33}

运行结果如下,其原理为通过Resources.Load进行Lua文件的读取,并将读取的代码放入luaEnv.DoString()执行

通过内置loader加载lua源文件,其实就是通过require的方式引入helloword模块,然后执行:

1luaEnv.DoString("require 'helloworld'"); // 这是XLua的内置loader

这种方式依旧需要保证lua脚本的后缀名是 .lua.txt

自定义Loader —— 热更新方案

使用自定义Loader的好处:可以将lua文件放在自定义目录下,也可以放在服务器端进行下载动态取得。那么如何自定义loader?

 1void Start(){
 2    LuaEnv env = new LuaEnv();
 3    env.AddLoader(MyLoader);    // 这边手动添加了一个自定义loader的回调
 4    env.DoString("require 'xxx'");    
 5    // 不仅会去Resource中找,也会通过自定义loader的回调去查找。
 6    // 注意:找到一个loader后就不会继续查找了
 7    env.Dispose();
 8}
 9
10// 这里即为自定义的loader,当上面执行env.DoString("require")时,
11// 会将该自定义loader也加入查找。
12// 返回值不为空时,即表示找到lua代码并返回,为空时会继续遍历查找其他loader
13private byte[] MyLoad(ref string path){
14    if(path=="xxx")
15    return System.Text.Encoding.UTF8.GetBytes("print 1");
16}

这个其实类似反向的双亲委派模型,如果定义里LoaderA、LoaderB、LoaderC

会按照顺序查找,找不到就交给下一个Loader,如果全部的自定义Loader都找不到,则会找系统的Loader

使用自定义loader进行加载的实例:

1// 将lua文件放入streamingAssetsPath
2private byte[] MyLoader(ref string filePath){
3    string path = Application.streamingAssetsPath + "/" + filePath + ".lua.txt";
4    return System.Text.Encoding.UTF8.GetBytes(File.ReadAllText(path));
5}

C#访问Lua全局变量

Lua中定义了:

1print("Hello CShapCallLua")
2
3str = "C# 获得Lua全局变量"
4
5isDie = false
6
7num = 200

C#中如何获取?需要写好泛型,否则无法正确接收:

 1void Start()
 2{
 3    luaEnv = new LuaEnv();
 4    luaEnv.DoString("require 'cshapcalllua'");
 5
 6    int num = luaEnv.Global.Get<int>("num");
 7    string str = luaEnv.Global.Get<string>("str");
 8    bool isDie = luaEnv.Global.Get<bool>("isDie");
 9
10    print(num + ", " + str + ", " + isDie);
11}

运行结果:

C#访问Lua的table

访问Lua中的table(映射到class)

方式1——映射到class

1Person = {
2    name = "Tim",
3    age = 10
4}

C#中如何获得?

1Person person = luaEnv.Global.Get<Person>("Person");
2print("person.name " + person.name);
3print("person.age " + person.age);

这是一个值拷贝的过程 – 因此获得以后,lua里和c#里的两者就没有关系了 

xLua会先new一个对象实例,并把对应的字段复制过去。

如果class比较复杂,则代价会比较大(可通过GC优化),修改后的字段也不会同步到table,反之也不会

Table中多余的字段也不影响映射,不论是多出的字段或者是函数,都不会影响。

方式2——映射到interface

有点兼容性问题(降低.Net版本),参考:

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

 1[CSharpCallLua]
 2interface IPerson {   // 接口不能定义字段
 3    string name{get;set;}
 4    int age{get;set;}
 5    void eat(int a, int b)
 6}
 7
 8IPerson p = luaEnv.Global.Get<IPerson>("person");
 9
10// 注意:这是引用 -- 在c#中改变person的属性,也会改变lua的中的元组
11p.age = 15;
12luaEnv.DoString("print(persion.age)");
13// 此时输出的为Lua: 15
14
15// 映射为接口可进行函数映射
16p.eat(1,2)
17
18// 对应lua的person元组的定义一个对应的匿名函数
19// 这样定义lua函数时,有三个参数,第一个为函数本身,对应c#里的this,这个必须写。可以通过self进行元组的访问,比如self.age
20person = {
21    ...
22    eat = function (self, a, b)
23        print(a+b)
24    end
25}
26// 输出3
27
28// 不声明第一个self参数的写法:这么写会默认带一个self参数
29function person:eat(a, b)
30end
31// 上面的写法等价于:
32function person.eat(self, a, b)
33end

interface应该是最常用的方式,因为可以访问变量、设置变量、调用函数,几乎是100%映射Lua

方式3 —— 映射到 Dictionary/List

不需要定义class或interface,直接映射成Dictionary或List集合中,映射成Dictionary或List中,各有缺点

 1// Dictionary
 2// 局限性:元组中没有对应key的元素,不会进行映射
 3Dictionary<string, object> dict = luaEnv.Global.Get<Dictionary<string, object>>("person");
 4foreach(string key in dict.keys) {
 5    // 遍历
 6}
 7
 8// List
 9// 局限性:为键值对会被忽略,只会映射单纯的值
10List<object> list = luaEnv.Global.Get<List<object>>("person");
11foreach(object item in list) {
12    // 遍历
13}
14
15luaEnv.Global.Get<List<int>>("person")
16// 只会映射元组中为number的值,若为小数则映射为0

方法4 —— 映射到LuaTable类(不推荐)

xLua提供的一个c#类,优势:无需生成代码;缺点:比较慢,比方法2慢了一个数量级,没有类型检查

1LuaTable tab = luaEnv.Global.Get<LuaTable>("person");
2print(tab.Get<string>("name"));    // 输出key为name对应的值
3print(tab.Get<int>("age"));
4
5// 还有其他一些方法可以尝试,比如Length/ Get/ Set等

C#访问Lua全局function

方式1 —— 映射到C#的delegate

将lua中的全局function映射到C#的delegate委托 – 推荐使用;

优点:性能好;类型安全;缺点:需要生成代码

1function add()
2    print("call add ...")
3end
4
5function add2(a, b)
6    print("call add ..." .. (a+b))
7end

C#中调用:

1// Action add = luaEnv.Global.Get<Action>>("add");
2// 带参数
3Action<int, int> add = luaEnv.Global.Get<Action<int, int>>("add2");
4// 调用
5add(10, 20);
6add = null; // 类似释放函数指针

旧版本需要通过自定义一个委托,新版本则不需要:

 1// 定义委托 -- 需要加上CSharpCallLua特性
 2[CSharpCallLua]
 3delegate void Func(int a, string b);
 4
 5// 映射
 6Func func = luaEnv.Global.Get<Func>("funcName");
 7// 调用
 8func(1,"aa");
 9// 释放
10func = null;
11// 如果没释放就进行luaEnv.Dispose(),会报错

多个返回值如何处理?

 1// 单返回值
 2[CSharpCallLua]
 3delegate int Add(int a, int b);
 4
 5Add add = luaEnv.Global.Get<Add>("add");
 6int res = add(1,2);
 7add = null;
 8
 9// 多返回值 -- 通过out/ ref的方式
10[CSharpCallLua]
11delegate int Add(int a, int b, out int res2, ref int res3);
12
13// 可用out或ref都行
14Add add = luaEnv.Global.Get<Add>("add")
15int res1 = add(1, 2, out int res2, ref int res3);
16add = null;

方式2 —— 映射到LuaFunction(不推荐)

优缺点与方法1相反 – 速度慢,类型不安全

1LuaFunction func = luaEnv.Global.Get<LuaFunction>("add");
2// 返回类型为object[]
3object[] results = func.Call(1, 2); 

总结如下:在C#中访问Lua的全局数据时:推荐通过delegate访问函数、通过interface访问元组,这样做可以使c#使用方和xLua解耦,由一个专门的模块负责xlua的初始化以及delegate/ interface的映射。

更多关于XLua的文档可以在这里看到:

https://github.com/Tencent/xLua/tree/master/Assets/XLua/Doc