之前写过一篇Xlua 插件学习的笔记,现在看有些简陋,只有基本使用和热补丁的内容,这篇笔记来补充下以前没有的内容,主要详细介绍了如何在API中在C#中调用Lua,如何在Lua中调用C#,如果想要了解这背后做了什么,可以看看我之前的另一篇文章,简单介绍了Lua与C#的互相调用。

LuaEnv虚拟机

之前的笔记也有,这里就不详细说了,为了文章完整性,还是简单介绍下:

using UnityEngine;
using XLua;

public class LuaEnv : MonoBehaviour
{
    void Start()
    {
        LuaEnv luaEnv = new LuaEnv();//实例化lua虚拟机,能够在unity中执行lua代码,一般要保证其唯一性
        luaEnv.DoString("print('helloworld')");//执行lua代码
        luaEnv.DoString("require('Main')");//执行在resource文件夹中的lua文件,只支持txt bytes等,不能识别.lua文件,因此脚本需要.lua.txt
        luaEnv.Tick();//类似GC
        luaEnv.Dispose();//销毁
    }
}

文件加载重定向

我们DoString默认加载的是resource文件夹的内容,但是实际上,我们要加载的是ab包中的内容,所以需要使用文件加载重定向的功能来自定义加载Lua脚本的位置。

using System.IO;
using UnityEngine;
using XLua;

public class Loader : MonoBehaviour
{
    void Start()
    {
        LuaEnv luaEnv = new LuaEnv();
        //允许我们自定义加载Lua文件的规则,每次require的时候就会执行我们下面自定义的函数
        //找不到对应路径才会在默认路径resource下面找
        luaEnv.AddLoader(MyLoader);
        luaEnv.DoString("require('Main')");
    }

    private byte[] MyLoader(ref string filePath)
    {
        string path = Application.dataPath + "/Script/Lua/" + filePath + ".lua";
        if (File.Exists(filePath))
        {
            return File.ReadAllBytes(path);
        }
        else
        {
            Debug.Log("重定向失败");
            return null;
        }
    }
}

Lua管理器

为了后续更加方便的使用Lua,Lua管理器封装了一些luaEnv方法和属性供后面其他地方使用,主要有,初始化、访问大G表,重定向,GC,销毁luaEnv等功能。

using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using XLua;

public class LuaMgr : Singleton<LuaMgr>
{
    private LuaEnv luaEnv;
    /// <summary>
    /// luaEnv中的大G表,保存了所有的lua对象,提供给C#调用
    /// </summary>
    public LuaTable Global
    {
        get
        {
            return luaEnv.Global;
        }
    }

    public void Init()
    {
        if (luaEnv != null)
        {
            return;
        }
        luaEnv = new LuaEnv();

        luaEnv.AddLoader(MyLoader);
        luaEnv.AddLoader(MyABLoader);
    }

    private byte[] MyLoader(ref string filePath)
    {
        string path = Application.dataPath + "/Scripts/Lua/" + filePath + ".lua";
        if (File.Exists(path))
        {
            return File.ReadAllBytes(path);
        }
        else
        {
            Debug.Log("普通重定向失败,文件名为:" + filePath);
            return null;
        }
    }

    //使用热更新时,我们会通过加载AB包中的Lua脚本资源
    //AB包中.lua还是无法识别
    //所以打包时需要将lua脚本后缀加入.txt
    private byte[] MyABLoader(ref string filePath)
    {
        TextAsset tx = ABMgr.Instance.LoadRes<TextAsset>("lua", filePath + ".lua");
        if (tx == null)
        {
            Debug.Log("AB重定向失败,文件名为:" + filePath);
            return null;
        }
        return tx.bytes;
    }

    public void DoLuaFile(string fileName)
    {
        string str = string.Format("require'{0}'", fileName);
        DoString(str);
    }

    public void DoString(string str)
    {
        if (luaEnv == null)
        {
            return;
        }
        luaEnv.DoString(str);
    }

    public void Tick()
    {
        if (luaEnv == null)
        {
            return;
        }
        luaEnv.Tick();
    }

    public void Dispose()
    {
        luaEnv.Dispose();
        luaEnv = null;
    }
}

获取、修改Lua脚本中的变量

这些之前的笔记中也有,这里就不详细说了,为了文章完整性,还是简单介绍下:

//lua脚本中:testNumber = 2
//获取
int a = luaEnv.Global.Get<int>("testNumber");
//修改
luaEnv.Global.Set("testNumber", 20);
//再次获取变量后,a的值变为20,其他数据类型如string、float、bool等同理
a = luaEnv.Global.Get<int>("testNumber");

注意,unity中利用XLUA的Global.Get无法获取lua中local修饰的局部变量。

虽然Lua中Number是唯一的数值类型,但是我们可以根据他具体的值,用对应的C#办理类型类存储。

通过委托和LuaFunction映射Lua脚本中的函数

测试Lua脚本中的四种函数,分别是无参无返回值函数,有参有返回值函数,Lua特有的有参多返回值函数,需要通过ref或者out来接收多返回值,还有变长参数的函数,具体代码如下:

testFun = function()
    print("无参无返回值")
end

testFun2 = function(a)
    print("有参有返回值")
    retrun a;
end

testFun3 = function(a)
    print("有参多返回值")
    return a,false,"123"
end

testFun5 = function(a, ...)
    print("变长参数")
    arg = {...}
    for k,v in pairs(arg) do
        print(k,v)
    end
end

在C#中获取并执行Lua脚本中的函数API:

委托是函数的容器,所以C#通过委托来映射Lua中的函数,同时Lua也提供了LuaFunction来作为Lua函数的容器。

using System;
using UnityEngine;
using UnityEngine.Events;
using XLua;
public class CallFunc : MonoBehaviour
{
    //无参无返回值的委托
    public delegate void CustomCall();
    
    //[CSharpCallLua]特性是在XLua命名空间中的,注意加了[CSharpCallLua]后要在编辑器里生成Lua代码
    //有参有返回的委托
    [CSharpCallLua]
    public delegate int CustomCall2(int a);

    [CSharpCallLua]
    public delegate int CustomCall3(int a, out bool b, out string s);

    [CSharpCallLua]
    public delegate int CustomCall4(int a, ref bool b, ref string s);

    [CSharpCallLua]
    public delegate void CustomCall5(string a, params int[] args);//变长参数的类型是根据实际情况来定的

    void Start()
    {
        LuaMgr.Instance.Init();
        LuaMgr.Instance.DoLuaFile("main");
        //无参无返回函数的获取
        //委托
        CustomCall call = LuaMgr.Instance.Global.Get<CustomCall>("testFun");
        call();
        //Unity自带委托
        UnityAction ua = LuaMgr.Instance.Global.Get<UnityAction>("testFun");
        ua();
        //C#提供的委托
        Action ac = LuaMgr.Instance.Global.Get<Action>("testFun");
        ac();
        //Xlua提供的一种 获取函数的方式,会产生GC所以少用
        LuaFunction lf = LuaMgr.Instance.Global.Get<LuaFunction>("testFun");
        lf.Call();

        //有参有返回函数的获取
        CustomCall2 call2 = LuaMgr.Instance.Global.Get<CustomCall2>("testFun2");
        Debug.Log("有参有返回:" + call2(10));
        //C#自带的泛型委托 方便我们使用
        Func<int, int> sFun = LuaMgr.Instance.Global.Get<Func<int, int>>("testFun2");
        Debug.Log("有参有返回:" + sFun(20));
        //Xlua提供的
        LuaFunction lf2 = LuaMgr.Instance.Global.Get<LuaFunction>("testFun2");
        Debug.Log("有参有返回:" + lf2.Call(30)[0]);

        //多返回值函数的获取
        //使用out或ref来接收第1个之后的返回值
        CustomCall3 call3 = LuaMgr.Instance.Global.Get<CustomCall3>("testFun3");
        bool b;
        string s;
        Debug.Log("第一个返回值:" + call3(100, out b, out s));
        Debug.Log(b + "_" + s);

        CustomCall4 call4 = LuaMgr.Instance.Global.Get<CustomCall4>("testFun3");
        bool b1 = false;
        string s1 = "";
        Debug.Log("第一个返回值:" + call4(200, ref b1, ref s1));
        Debug.Log(b1 + "_" + s1);

        //Xlua提供的
        LuaFunction lf3 = LuaMgr.Instance.Global.Get<LuaFunction>("testFun3");
        object[] objs = lf3.Call(1000);
        for (int i = 0; i < objs.Length; ++i)
        {
            Debug.Log("第" + i + "个返回值是:" + objs[i]);
        }

        //变长参数
        CustomCall5 call5 = LuaMgr.Instance.Global.Get<CustomCall5>("testFun4");
        call5("123", 1, 2, 3, 4, 5, 566, 7, 7, 8, 9, 99);

        LuaFunction lf4 = LuaMgr.Instance.Global.Get<LuaFunction>("testFun4");
        lf4.Call("456", 6, 7, 8, 99, 1);
    }
}

List和Dictionary映射Table

lua部分的Table测试代码如下,可以通过下面C#中List和Dictionary来映射。

list一般用来映射没有自定义索引的表,dictionary一般用于映射有自定义索引的表,如果确定list/dictionary的类型,在泛型指定类型即可,如果不确实的类型则用object这所有类型之父来制定。

值得注意的是,如果在C#中修改list中的值,是浅拷贝值传递,不会修改Lua中的内容

testList = {1,2,3,4,5,6}
testList2 = {1,"test",3,true,5,6}

testDic = {
    ["1"] = 1,
    ["2"] = 2,
    ["3"] = 3,
    ["4"] = 4,
    ["5"] = 5,
}
testDic2 = {
    ["1"] = 1,
    [true] = 2,
    [false] = 3,
    ["4"] = 4,
    ["5"] = 5,
}
List<int> list = LuaMgr.Instance.Global.Get<List<int>>("testList");
List<object> list2 = LuaMgr.Instance.Global.Get<List<object>>("testList2");
Dictionary<int, string> dic = LuaMgr.Instance.Global.Get<Dictionary<int, string>>("testDic");
Dictionary<object, object> dic2 = LuaMgr.Instance.Global.Get<Dictionary<object, object>>("testDic2");

用Class来映射Table

lua部分,使用Table来表示C#中的类:

testClass = {
    testInt = 2,
    testBool = true,
    testFlaot = 2.2,
    testString = "test",
    testFun = function()
        print("testfun")
    end,
    testInClass = {
        testInt = 5
    }
}

C#部分,使用CallLuaClass类来映射Lua中的Table,注意这个类中申明的成员变量要和Lua那边一致,否则无法匹配赋值,但是变量多了少了只是会被忽略,不会被匹配而已,编写好映射类后,还是通过Gloabal.Get的方式获取到Lua中对应的类。

同样,类的映射是浅拷贝值传递,修改C#中的变量不会改变Lua中的内容。

public class CallLuaClass
{
    public int testInt;
    public bool testBool;
    public float testString;
    public float testFloat;
    public UnityAction testFun;
    public CallLuaInClasss testInClass;
}
public class CallLuaInClasss
{
    public int testInt;
}
public class CallClass : MonoBehaviour
{
    void Start()
    {
        LuaMgr.Instance.Init();
        LuaMgr.Instance.DoLuaFile("main");
        CallLuaClass obj = LuaMgr.Instance.Global.Get<CallLuaClass>("testClass");
        Debug.Log(obj.testBool);
        Debug.Log(obj.testInt);
        Debug.Log(obj.testFloat);
        Debug.Log(obj.testString);
        obj.testFun();
        Debug.Log(obj.testInClass.testInt);
    }
}

用Interface来映射Table

lua中的Table还是和上一节的Class用一样的代码。

接口中不允许有成员变量,因此用属性来接受,注意接口和类的规则是一样的,其中的属性多了少了不影响结果但是会被忽略。

值得注意的是,接口的映射是深拷贝引用传递,修改C#中的变量会改变Lua中的内容。

[CSharpCallLua]
public interface ICallInterface
{
    int testInt{get;set;}
    bool testBool{get;set;}
    float testFloat{get;set;}
    string testString{get;set;}
    UnityAction testFun{get;set;}
    ICallInInterface testInClass{get;set;}
}
[CSharpCallLua]
public interface ICallInInterface
{
    int testInt{get;set;}
}

public class CallInterface : MonoBehaviour
{
    void Start()
    {
        LuaMgr.Instance.Init();
        LuaMgr.Instance.DoLuaFile("main");
        ICallInterface obj = LuaMgr.Instance.Global.Get<ICallInterface>("testClass");
        Debug.Log(obj.testBool);
        Debug.Log(obj.testInt);
        Debug.Log(obj.testFloat);
        Debug.Log(obj.testString);
        Debug.Log(obj.testInClass.testInt);
        obj.testBool = false;
        obj.testFun();
        ICallInterface obj2 = LuaMgr.Instance.Global.Get<ICallInterface>("testClass");
        Debug.Log(obj.testBool);//引用拷贝
    }
}

用LuaTable映射Table

lua中的Table还是和上一节的接口用一样的代码,因此这里就不重复展示了。

除非必要,不建议使用LuaTable和LuaFunction,效率低,且会产生额外GC。

注意,和接口一样,LuaTable的映射是深拷贝引用传递,修改C#中的变量会改变Lua中的内容。

LuaTable table = LuaMgr.Instance.Global.Get<LuaTable>("testClass");
Debug.Log(table.Get<int>("testInt"));
Debug.Log(table.Get<bool>("testBool"));
Debug.Log(table.Get<float>("testFloat"));
Debug.Log(table.Get<string>("testString"));
table.Get<LuaFunction>("testFun").Call();
table.Set<string, int>("testInt", 999);
LuaTable table2 = LuaMgr.Instance.Global.Get<LuaTable>("testClass");
Debug.Log(table2.Get<int>("testInt"));//引用传递
//不用了记得dispose
table.Dispose();
table2.Dispose();