【Unity】xLua及热更新

1 前言

        本文主要讲xLua的基本使用。看完有个基本认识还是可以了,简单的热更操作还是可以做到的,但更多细节内容依旧需要从官方文档中了解、学习。

2 xLua

2.1 什么是xLua

        xLua是由腾讯维护的一个开源项目,我们也可以将其看做一个插件。xLua为Unity、 .Net、 Mono等C#环境增加Lua脚本编程的能力,借助xLua,这些Lua代码可以方便的和C#相互调用。通常作为Unity的一种热更新解决方案。

2.2 xLua安装

        需要先在github上下载,地址:https://github.com/Tencent/xLua。进入网址后,按如下操作下载(别问,问就是下载最新的)。

下载完成后,打开下载的压缩包,将Assets文件夹中的内容复制到Unity的Assets中即可。压缩包Assets中内容:

复制到Unity Assets下的目录(只要在Assets下就行,这里我就套了两层文件夹):

PS:下载的压缩包里也有教程文档、案例之类的,可以跟着那个文档学习,本章基本也是基于那个文档。xLua-master.zip\xLua-master\Assets\XLua\Doc。

2.3 简单案例

        在C#脚本中,我们可以通过一个lua虚拟机执行lua代码,也可以执行C#代码。

代码:

using UnityEngine;
using XLua;//xLua头文件

public class xLuaTest01 : MonoBehaviour
{
    //一个LuaEnv实例对应Lua虚拟机,出于开销的考虑,建议全局唯一
    LuaEnv luaenv;

    private void Start()
    {
        //创建LuaEnv对象
        luaenv = new LuaEnv();

        //调用Lua代码
        luaenv.DoString("print('lua代码内容。')");
        //调用C#代码(注意C#代码需要前加CS.)
        luaenv.DoString("CS.UnityEngine.Debug.Log('C#代码内容。')");

        //销毁LuaEnv对象
        luaenv.Dispose();
    }
}

结果:

2.4 读取外部lua文件

2.4.1 第一种方法

        将lua文件存储为文本文件,然后读取内容执行。首先在Resources文件夹中创建一个.lua文件,写入代码,但最后要把后缀改为.txt,代码、文件如下图:

print("I belong to lua script.");

改为.txt是为了后续读取文本内容。读取并执行代码如下:

using UnityEngine;
using XLua;

public class xLuaTest02 : MonoBehaviour
{
    //一个LuaEnv实例对应Lua虚拟机,出于开销的考虑,建议全局唯一
    LuaEnv luaenv;

    private void Start()
    {
        //创建LuaEnv对象
        luaenv = new LuaEnv();
        
        //读取lua文本文件
        TextAsset ta = Resources.Load<TextAsset>("luaContent01.lua");
        //调用Lua代码
        luaenv.DoString(ta.text);
        
        //销毁LuaEnv对象
        luaenv.Dispose();
    }
}

结果:

2.4.2 第二种方法

        使用LuaEnv自带的require函数。比如:DoString("require 'byfile'")。

代码:

using UnityEngine;
using XLua;

public class xLuaTest02 : MonoBehaviour
{
    //一个LuaEnv实例对应Lua虚拟机,出于开销的考虑,建议全局唯一
    LuaEnv luaenv;

    private void Start()
    {
        //创建LuaEnv对象
        luaenv = new LuaEnv();

        //调用Lua代码
        luaenv.DoString("require 'luaContent01'");//使用Loader加载(通常有多个Loader,加载时一个不成功会换下一个)

        //销毁LuaEnv对象
        luaenv.Dispose();
    }
}

结果一样:

        require实际上是调一个个的loader去加载,有一个成功就不再往下尝试,全失败则报文件找不到。
        目前xLua除了原生的loader外,还添加了从Resource加载的loader,需要注意的是因为Resource只支持有限的后缀,放Resources下的lua文件得加上txt后缀(如2.4.1所说)。
        建议的加载Lua脚本方式是:整个程序就一个DoString("require 'main'"),然后在main.lua加载其它脚本(类似lua脚本的命令行执行:lua main.lua)。换言之就是我们只调用一个.lua文件的内容,后续所有内容都已经安排在了这个.lua文件当中。
        但如果Lua文件是需要下载回来的(需下载),或者某个自定义的文件格式里头解压出来(需解压),或者需要解密(需解密)等等,怎么办?还可不可以使用require?答案是:可以,直接调用require,这些工作交给Loader即可。这时候就需要了解下xLua的自定义Loader了。
        (这些处理工作肯定是跑不掉的,问题是在哪处理。不使用require的话,我们也可以写个万金油方法读取文件,获取文本内容,然后DoString。)

2.5 自定义Loader

        自定义loader的本质就是添加委托。涉及到的委托与接口如下:

public delegate byte[] CustomLoader(ref string filepath);//委托
public void LuaEnv.AddLoader(CustomLoader loader)//添加委托的方法

        通过AddLoader可以注册个回调函数,该回调函数有一个是字符串参数,lua代码里头调用require时,参数将会透传给回调,回调中就可以根据这个参数去加载指定文件,如果需要支持调试,需要把filepath修改为真实路径传出。该回调返回值是一个byte数组,如果为空表示该loader找不到,否则则为lua文件的内容,即要执行的内容。

案例代码:

using UnityEngine;
using XLua;

public class xLuaTest02 : MonoBehaviour
{
    //一个LuaEnv实例对应Lua虚拟机,出于开销的考虑,建议全局唯一
    LuaEnv luaenv;

    private void Start()
    {
        //创建LuaEnv对象
        luaenv = new LuaEnv();

        //添加自定义loader
        luaenv.AddLoader(MyLoader);

        //调用Lua代码
        luaenv.DoString("require 'luaContent01'");//使用Loader加载(通常有多个Loader,加载时一个不成功会换下一个)

        //销毁LuaEnv对象
        luaenv.Dispose();
    }

    //自定义loader
    private byte[] MyLoader(ref string filePath)
    {
        //定义一段Lua代码(也可以根据上面的filePath读取)
        string content = "print('myLoader')";
        //返回此代码
        return System.Text.Encoding.UTF8.GetBytes(content);
    }
}

结果:

可以看到,我们最终没有执行luaContent01.lua.txt内的lua代码,而是执行了我们定义的lua代码。因为在我们注册自定义的loader后,require时先使用了我们自定义的loader,成功获得返回内容后就不会再执行其他loader了,所以最终执行了是我们定义的lua代码。当然,若我们在自定义loader中返回null,获取失败,那么后续就会执行自带的loader读取luaContent01.lua.txt内的lua代码,并最终执行,这里我就不尝试了。

2.6 C#访问Lua的数据结构

        主要涉及获取全局变量、table、全局function。三者的获取通过访问LuaEnv.Global就可以了,里面有个泛型Get方法,可指定返回的类型。

2.6.1 获取全局变量

        首先在Resources文件夹中创建一个CSharpCallLua.lua.txt文件,文件内容如下:

a = 149
b = "shqz"
c = true

然后C#代码:

using UnityEngine;
using XLua;

public class xLuaTest02 : MonoBehaviour
{
    //一个LuaEnv实例对应Lua虚拟机,出于开销的考虑,建议全局唯一
    LuaEnv luaenv;

    private void Start()
    {
        //创建LuaEnv对象
        luaenv = new LuaEnv();

        //调用Lua代码
        luaenv.DoString("require 'CSharpCallLua'");//使用Loader加载(通常有多个Loader,加载时一个不成功会换下一个)

        //“要先执行上面的DoString执行Lua代码,后续才能获取lua代码中的数据结构”

        //获取全局变量
        int aa = luaenv.Global.Get<int>("a");
        string bb = luaenv.Global.Get<string>("b");
        bool cc = luaenv.Global.Get<bool>("c");
        Debug.Log("aa:" + aa + "   bb:" + bb + "   cc:" + cc);

        //销毁LuaEnv对象
        luaenv.Dispose();
    }
}

结果:

2.6.2 获取Table

        获取table也分多方式,主要有一下四种:

  1. 映射到普通class或struct(by value:值拷贝映射)
  2. 映射到interface(by ref:引用映射)
  3. 映射到Dictionary<>,List<>(更轻量级的by value方式)
  4. 映射到LuaTable类(另外一种by ref方式)

        首先创建一个.lua的文件,别忘了加.txt后缀,这里创建的文件全面为CSharpCallLua.lua.txt,文件内容如下:

--全局变量
a = 149
b = "shqz"
c = true

--Table
Map = {
--table的变量
areaName = "mdsl",
areaNumber = 2,
"test1",
"test2",
123,
231,
--table的方法
ff = function(self,a,b)--这里需要加第一个参数,指表本身,用.的形式也要加,用:的形式可以省略,这是lua语法的内容。例子在下面。
print(a+b)
end
}

--其他的“table方法定义方式”
--[[
function Map:ff(a,b)--默认自带一个self参数,表示当前表
print(a+b)
end
--]]

--[[
function Map.ff(self,a,b)
print(a+b)
end
--]]

--既然说的第一参数问题,那也说说在lua中调用函数时也要面临的这个问题
--在调用table中方法时:
--若是 = 创建,则需要以table.形式调用,且需加第一个参数
--若是 . 创建,则需要以table.形式调用,且需加第一个参数
--若是 : 创建,则需要以table:形式调用,且可不加第一个参数
--在调用C#中的成员方法时:
--以 . 形式,需加第一个参数。以 : 形式,可不加第一个参数。(这个后面讲到lua调用C#也会再提)
2.6.2.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生成降低开销,详细可参见配置介绍文档(与教程文档在一起)。

代码:

using UnityEngine;
using XLua;

public class xLuaTest02 : MonoBehaviour
{
    //一个LuaEnv实例对应Lua虚拟机,出于开销的考虑,建议全局唯一
    LuaEnv luaenv;

    private void Start()
    {
        //创建LuaEnv对象
        luaenv = new LuaEnv();

        //调用Lua代码
        luaenv.DoString("require 'CSharpCallLua'");//使用Loader加载(通常有多个Loader,加载时一个不成功会换下一个)

        //“要先执行上面的DoString执行Lua代码,后续才能获取lua代码中的数据结构”

        //获取表格,映射为class
        Map map = luaenv.Global.Get<Map>("Map");
        Debug.Log("class:   " + "name = " + map.areaName + "   number = " + map.areaNumber);

        //销毁LuaEnv对象
        luaenv.Dispose();
    }

    //映射用类
    class Map
    {
        public string areaName;
        public int areaNumber;
    }
}

结果:

2.6.2.2 映射为interfiace

        这种方式依赖于生成代码(如果没生成代码会抛InvalidCastException异常)(接口加[CSharpCallLua]标识,后面代码案例里会有),代码生成器会生成这个interface的实例,如果get一个属性,生成代码会get对应的table字段,如果set属性也会设置对应的字段,即interface与table的数据是引用关系,修改会相互影响。另外,可以通过interface的方法访问lua table中的函数,即函数间的映射。

代码:

using UnityEngine;
using XLua;

public class xLuaTest02 : MonoBehaviour
{
    //一个LuaEnv实例对应Lua虚拟机,出于开销的考虑,建议全局唯一
    LuaEnv luaenv;

    private void Start()
    {
        //创建LuaEnv对象
        luaenv = new LuaEnv();

        //调用Lua代码
        luaenv.DoString("require 'CSharpCallLua'");//使用Loader加载(通常有多个Loader,加载时一个不成功会换下一个)

        //“要先执行上面的DoString执行Lua代码,后续才能获取lua代码中的数据结构”

        //获取表格,inerface
        IMap imap = luaenv.Global.Get<IMap>("Map");
        Debug.Log("inerface:   " + "name = " + imap.areaName + "   number = " + imap.areaNumber);
        imap.ff(1, 2);//方法调用

        //销毁LuaEnv对象
        luaenv.Dispose();
    }

    //映射用接口(我这里接口还需要加public)
    [CSharpCallLua]//需加此标签
    public interface IMap
    {
        string areaName { get; set; }
        int areaNumber { get; set; }
        void ff(int a, int b);
    }
}

结果:

2.6.2.3 映射到Dictionary<>,List<>

        不想定义class或者interface的话,可以考虑用这个,前提table下key类型一致,value类型一致。
        注意,Dictionary<>,List<>是泛型的,所传的类型参数将决定dic和list中存储那些数据,类型不匹配的内容将不会被获取到。

代码:

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

public class xLuaTest02 : MonoBehaviour
{
    //一个LuaEnv实例对应Lua虚拟机,出于开销的考虑,建议全局唯一
    LuaEnv luaenv;

    private void Start()
    {
        //创建LuaEnv对象
        luaenv = new LuaEnv();

        //调用Lua代码
        luaenv.DoString("require 'CSharpCallLua'");//使用Loader加载(通常有多个Loader,加载时一个不成功会换下一个)

        //“要先执行上面的DoString执行Lua代码,后续才能获取lua代码中的数据结构”

        //获取表格,Dictionary<>,List<>
        Dictionary<string, int> dic = luaenv.Global.Get<Dictionary<string, int>>("Map");
        foreach (var key in dic.Keys)
        {
            Debug.Log("key: " + key + ", value: " + dic[key]);//只输出可key为string类型,value为int类型的内容
        }
        List<int> list = luaenv.Global.Get<List<int>>("Map");
        foreach (var data in list)
        {
            Debug.Log("list value: " + data);//只输出了无key的int类型的内容
        }

        //销毁LuaEnv对象
        luaenv.Dispose();
    }
}

结果:

2.6.2.4 映射到LuaTable类

        这种方式好处是不需要生成代码,但也有一些问题,比如慢,比“映射为interfiace”要慢一个数量级,比如没有类型检查。

代码:

using UnityEngine;
using XLua;

public class xLuaTest02 : MonoBehaviour
{
    //一个LuaEnv实例对应Lua虚拟机,出于开销的考虑,建议全局唯一
    LuaEnv luaenv;

    private void Start()
    {
        //创建LuaEnv对象
        luaenv = new LuaEnv();

        //调用Lua代码
        luaenv.DoString("require 'CSharpCallLua'");//使用Loader加载(通常有多个Loader,加载时一个不成功会换下一个)

        //“要先执行上面的DoString执行Lua代码,后续才能获取lua代码中的数据结构”

        //获取表格,LuaTable
        LuaTable tab = luaenv.Global.Get<LuaTable>("Map");
        Debug.Log("LuaTable.areaName = " + tab.Get<string>("areaName"));

        //销毁LuaEnv对象
        luaenv.Dispose();
    }
}

结果:

2.6.3 获取全局function

        获取方式分为两种:

  1. 映射到delegate
  2. 映射到LuaFunction

        修改CSharpCallLua.lua.txt文件的内容,加入一个全局函数,这里我直接把之前的删除,只留一个全局函数:

--全局函数
function FGlobal(p)
print("global function!")
return p,p*p
end
2.6.3.1 映射到delegate

        这种是建议的方式,性能好很多,而且类型安全。缺点是要生成代码(如果没生成代码会抛InvalidCastException异常)(委托加[CSharpCallLua]标识)。
         对于function的每个参数就声明一个输入类型的参数。
        多返回值要怎么处理?从左往右映射到c#的输出参数,输出参数包括返回值,out参数,ref参数(代码中有示例)。
        参数、返回值类型支持哪些呢?都支持,各种复杂类型,out,ref修饰的,甚至可以返回另外一个delegate。
        delegate的使用就更简单了,直接像个函数那样用就可以了。
        代码注释中还讲了一些其他需要注意的内容。

代码:

using UnityEngine;
using XLua;

public class xLuaTest02 : MonoBehaviour
{
    //一个LuaEnv实例对应Lua虚拟机,出于开销的考虑,建议全局唯一
    private LuaEnv luaenv;

    //创建委托类型
    //注意此委托类型参数,有一个out参数,这是为了接受Lua函数的多值返回。
    //若Lua函数返回多个值,比如3个,函数本身返回一个,那另外两个呢?就需要在委托中定义额外的两个out参数来接收。
    //接收顺序:函数返回、out参数顺序。
    //实际除了out,也可以使用ref。
    //具体的使用例子看下面代码。
    [CSharpCallLua]//加标识!
    private delegate int myAction(int p,out int extraReturnParam);
    //创建委托对象
    myAction act;

    private void Start()
    {
        //创建LuaEnv对象
        luaenv = new LuaEnv();

        //调用Lua代码
        luaenv.DoString("require 'CSharpCallLua'");//使用Loader加载(通常有多个Loader,加载时一个不成功会换下一个)

        //“要先执行上面的DoString执行Lua代码,后续才能获取lua代码中的数据结构”

        //获取全局函数,使用委托类型
        act = luaenv.Global.Get<myAction>("FGlobal");
        //执行函数
        int extraReturnP;
        int num = act(10, out extraReturnP);
        Debug.Log("获取的返回值是:" + num + ",额外返回值:" + extraReturnP);

        //act是全局变量,所以这类需要置为null,释放对函数的索引,这样luaenv虚拟机才能在OnDestroy中正确销毁(Dispose)
        //若act是局部变量则不用置为null,因为离开此“代码块”自己就会被销毁了。
        act = null;
    }

    //这里销毁虚拟机的代码移动到这里是因为不能与调用全局函数的索引在一个“代码块”内,否则会销毁失败,报出异常。
    //这里实际上就是报对函数的引用没释放的异常,即是我们将对函数的引用变量置为null,但只要在一个“代码块”内,就还会报此错误。
    private void OnDestroy()
    {
        //销毁LuaEnv对象
        luaenv.Dispose();
    }
}

结果:

2.6.3.2 映射到LuaFunction

        这种方式的优缺点刚好和第一种相反。
        使用也简单,LuaFunction上有个变参的Call函数,可以传任意类型,任意个数的参数,返回值是object的数组,对应于lua的多返回值。

代码:

using UnityEngine;
using XLua;

public class xLuaTest02 : MonoBehaviour
{
    //一个LuaEnv实例对应Lua虚拟机,出于开销的考虑,建议全局唯一
    private LuaEnv luaenv;

    private void Start()
    {
        //创建LuaEnv对象
        luaenv = new LuaEnv();

        //调用Lua代码
        luaenv.DoString("require 'CSharpCallLua'");//使用Loader加载(通常有多个Loader,加载时一个不成功会换下一个)

        //“要先执行上面的DoString执行Lua代码,后续才能获取lua代码中的数据结构”

        //获取全局函数,LuaFunction
        LuaFunction act = luaenv.Global.Get<LuaFunction>("FGlobal");
        //执行函数
        object[] objs = act.Call(10);//数组接收多返回值
        foreach(object obj in objs)
        {
            Debug.Log("返回值:" + obj);
        }

        //销毁LuaEnv对象(没有索引烦恼,直接销毁!)
        luaenv.Dispose();
    }
}

结果:

2.6.4 使用建议

        访问lua全局数据,特别是table以及function,代价比较大,建议尽量少做,比如在初始化时把要调用的lua function获取一次(映射到delegate)后,保存下来,后续直接调用该delegate即可。table也类似。

        如果lua侧的实现的部分都以delegate和interface的方式提供,使用方可以完全和xLua解耦:由一个专门的模块负责xlua的初始化以及delegate、interface的映射,然后把这些delegate和interface设置到要用到它们的地方。(即Lua代码内容,完全靠委托、接口映射的思想来实现。然后C#这边专门一个模块来初始化、映射、维护这些委托、接口。我们再通过这个模块调用我们需要的内容。实现结构:使用--模块--xLua。完成解耦。)

2.7 Lua调用C#

        先创建一个LuaCallCSharp.lua.txt文件,后续将在里面写Lua代码。

2.7.1 new C# 对象

//在C#中new一个对象
var newGameObj = new UnityEngine.GameObject();
--在lua中new一个对象
local newGameObj = CS.UnityEngine.GameObject()

它们基本一致,但也有一些区别:

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

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

--调用重载
local newGameObj2 = CS.UnityEngine.GameObject('helloworld')

        在LuaCallCSharp.lua.txt写入:

--创建一个游戏对象,并命名为 GameObject, new in lua
CS.UnityEngine.GameObject("GameObject, new in lua")

C#代码(执行lua代码。就是前面的读取lua文件的代码,只是lua文件名修改了):

using UnityEngine;
using XLua;

public class xLuaTest02 : MonoBehaviour
{
    //一个LuaEnv实例对应Lua虚拟机,出于开销的考虑,建议全局唯一
    private LuaEnv luaenv;

    private void Start()
    {
        //创建LuaEnv对象
        luaenv = new LuaEnv();

        //调用Lua代码
        luaenv.DoString("require 'LuaCallCSharp'");//使用Loader加载(通常有多个Loader,加载时一个不成功会换下一个)

        //销毁LuaEnv对象
        luaenv.Dispose();
    }
}

结果:

2.7.2 访问C#静态属性,方法

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

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

--用一个变量GameObject引用到UnityEngine.GameObject类
local GameObject = CS.UnityEngine.GameObject
--用变量使用类
GameObject.Find('helloworld')

案例就不演示了。

2.7.3 访问C#成员属性,方法

--testobj是我们假设创建的一个类对象

--读成员属性
testobj.DMF
--写成员属性
testobj.DMF = 1024
--调用成员方法(注意:调用成员方法,第一个参数需要传当前对象,这里对象指testobj,建议用冒号语法糖,如下)
testobj:DMFunc()--冒号语法糖,不需要传当前对象

案例就不演示了。

2.7.4 访问C#父类属性,方法

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

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

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

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

        来一个简单案例说明。在LuaCallCSharp.lua.txt写入:

--lua中的函数
function LuaFunc(l1,l2,l3)
print(l1,l2,l3)
return l1*l1,l2*l2,l3*l3
end

C#代码:

using UnityEngine;
using XLua;

public class xLuaTest02 : MonoBehaviour
{
    //一个LuaEnv实例对应Lua虚拟机,出于开销的考虑,建议全局唯一
    private LuaEnv luaenv;

    //C#中定义一个委托来获取
    [CSharpCallLua]
    private delegate int myAction(int c1, ref int c2, out int c3, int c4);
    //创建委托对象
    myAction act;

    private void Start()
    {
        //创建LuaEnv对象
        luaenv = new LuaEnv();

        //调用Lua代码
        luaenv.DoString("require 'LuaCallCSharp'");//使用Loader加载(通常有多个Loader,加载时一个不成功会换下一个)

        //获取全局函数,使用委托类型
        act = luaenv.Global.Get<myAction>("LuaFunc");

        //调用方法
        int p = 2;
        int re1 = 0;
        int re2 = 0;
        re1 = act(1, ref p, out re2, 3);
        Debug.Log("返回值1:" + re1 + ",返回值2:" + p + ",返回值3:" + re2);

        //置空,为luaenv销毁做准备
        act = null;    
    }

    private void OnDestroy()
    {
        //销毁LuaEnv对象
        luaenv.Dispose();
    }
}

结果:

首先,“Lua调用侧的参数处理规则”所说的对应关系是什么?即代码中lua与C#函数的参数列表的对应关系,在案例中体现为:

  • l1 → c1
  • l2 → c2
  • l2 → c4

看到没,这里没有c3,因为c3是out修饰的,是不算参数列表对应关系的。因此我们传入的值应是1、p(2)、3,而不是1、p(2)、re2(0)。图中打印lua函数打印的参数值就是1、2、3。好,那么lua函数的返回值是什么,是每个参数的平方,即1、4、9。那么来开始讨论“Lua调用侧的返回值处理规则”,即返回值与参数的对应关系,案例中体现为:

  • l1*l1 → act return
  • l2*l2 → p
  • l3*l3 → re2

第一个是函数返回值,后续则是ref、out修饰的参数,修饰参数从左往右来一一对应返回值,这就是它们的对应关系。图中我们将这几个参数输出,结果就是1、4、9,印证了这种关系。实际,这个返回值关系在前面也都已经说过一次了。

2.7.6 重载方法

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

--TestFunc重载1调用
testobj:TestFunc(100)
--TestFunc重载2调用
testobj:TestFunc('hello')

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

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

2.7.7 操作符

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

2.7.8 参数带默认值的方法

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

2.7.9 可变参数方法

//对于C#的如下方法
void VariableParamsFunc(int a, params string[] strs)
--可以在lua里头这样调用
testobj:VariableParamsFunc(5, 'hello', 'john')

2.7.10 使用Extension methods

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

2.7.11 泛化(模版)方法

        不直接支持,可以通过Extension methods功能进行封装后调用。(就是根据需要的类型进行封装,一种类型组合一个封装函数,然后根据需要去调用。在封装后使用的这一层,我们已经失去了泛型的便利性。)(一般函数封装也行,不一定非要Extension methods。)

        GetComponent方法如何调用?

--没有泛型,改为这样:
GetComponent('组件名')

2.7.12 枚举类型

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

--调用EnumTestFunc函数,参数是Tutorial.TestEnum枚举类型
testobj:EnumTestFunc(CS.Tutorial.TestEnum.E1)

另外,如果枚举类加入到生成代码的话,枚举类将支持__CastFrom方法,可以实现从一个整数或者字符串到枚举值的转换,例如:

--将整型转换为枚举类型
CS.Tutorial.TestEnum.__CastFrom(1)
--将字符串转换为枚举类型
CS.Tutorial.TestEnum.__CastFrom('E1')

如何将枚举类型加入到生成代码?给枚举类型加上[LuaCallCSharp]标签即可。如:

    [LuaCallCSharp]
    enum MyEnum
    {
        one,
        two,
        three
    }

2.7.13 delegate使用(调用,+,-)

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

  • +操作符:对应C#的+操作符,把两个调用串成一个调用链,右操作数可以是同类型的C# delegate或者是lua函数。
  • -操作符:和+相反,把一个delegate从调用链中移除。

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

2.7.14 event

        比如testobj里头有个事件定义是这样:public event Action TestEvent; 那么lua里如何增加、移除回调?

--增加事件回调
testobj:TestEvent('+', lua_event_callback)--回调函数:lua_event_callback
--移除事件回调
testobj:TestEvent('-', lua_event_callback)

2.7.15 64位整数支持

        Lua53版本64位整数(long,ulong)映射到原生的64未整数,而luajit版本,相当于lua5.1的标准,本身不支持64位,xlua做了个64位支持的扩展库,C#的long和ulong都将映射到userdata:

  1. 支持在lua里头进行64位的运算,比较,打印
  2. 支持和lua number的运算,比较
  3. 要注意的是,在64扩展库中,实际上只有int64,ulong也会先强转成long再传递到lua,而对ulong的一些运算,比较,我们采取和java一样的支持方式,提供一组API,详情请看API文档。

2.7.16 C#复杂类型和table的自动转换

        对于一个有无参构造函数的C#复杂类型,在lua侧可以直接用一个table来代替,该table有对应复杂类型中public字段的相应字段即可。所用来代替的table支持作为函数参数传递,属性赋值等,例如:

//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})

可以看到,为复杂类型B的形参b直接被用一个table代替了,table里还有一个table,对应着B类型中的公共字段A类型的b。

2.7.17 获取类型(相当于C#的typeof)

--比如要获取UnityEngine.ParticleSystem类的Type信息,可以这样
typeof(CS.UnityEngine.ParticleSystem)

2.7.18 “强”转

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

--也可以简单理解为将calc转换为后面的类型
cast(calc, typeof(CS.Tutorial.Calc))

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

3 热更新

3.1 配置

3.1.1 xLua配置

        首先是xLua的配置,就是上面讲的xLua安装,拷贝几个文件夹。

        然后这里需要再拷贝一些新的东西,即工具包。在xLua压缩包里有一个Tools文件,如图:

我们需要将其拷贝到项目当中,放到与Assets同目录下。

3.1.2 宏配置

        配置Unity设置中的宏。如图:

配置好后便可在菜单栏看到新增的功能按钮(将热更新代码注入到编辑器里)。如下:

3.1.3 dll文件配置

        需要将Unity安装目录下的一些.dll文件拷贝到xLua的文件目录中。
        .dll文件所在目录:“ 2022.3.0f1c1\Editor\Data\Managed ”(2022.3.0f1c1是我所安装的Unity所在文件夹,安装时自动生成的)。文件如图:

将此三个文件拷贝到“ XLua\Src\Editor ”下即可。

3.2 注意的问题

3.2.1 生成代码

        运行前需要!我们需要先生成代码,再将代码注入到编辑器内,才可以正常执行。当C#代码有修改了,也不要忘了重新生成代码。操作如图:

PS:生成代码时我遇到了一些类型无法识别的问题,去github一看,“fix: 修复在 Unity 2022 下生成代码报 Span 无法作泛型参数的问题 2 days ago”,两天前刚修复的.......所以很多问题可能换个版本就解决了,要么就等大佬修复。不过官方也提供了很多问题解释,在github上都有,可以参照官方解释来修正错误。

PS:生成时,等待右下角进度条执行完毕再操作。

3.2.2 清除代码

        在生成代码选项下的那个选项。重新生成代码前,可以先清除(一般是不清除的,但有时候出问题报错了可以清除试一试)。

PS:清除时,等待右下角进度条执行完毕再操作。

3.2.3 中文路径

        不要中文路径,不管什么开发,都要养成不用中文路径的习惯。

3.2.4 打包项目

        打包项目时,先把xLua中的案例全删除,否则会报错,无法打包。

3.2.5 开发阶段关闭宏

        建议平时开发业务代码不打开HOTFIX_ENABLE,只在build手机版本或者要在编译器下开发补丁时打开HOTFIX_ENABLE。
        PS:我的理解是纯C#开发不打开,但牵扯到Lua补丁开发的话就需要打开,毕竟不打开的话,使用hotfix的Lua代码会报错。然后若没有Lua补丁,那么在build的时候也要打开,这应该是为后续Lua补丁的开发做准备,打开后,后续更新就可以使用Lua代码借助hotfix来调整代码逻辑了。

3.3 函数替换小案例

3.3.1 基本

        官方案例就是个替换函数的案例,这里算是重新敲了一遍。首先创建一个脚本,将其挂载到场景当中,代码如下:

using UnityEngine;
using XLua;

[Hotfix]//热更新标签
public class xluaTest_Hot : MonoBehaviour
{
    //一个LuaEnv实例对应Lua虚拟机,出于开销的考虑,建议全局唯一
    LuaEnv luaenv;

    private void Awake()
    {
        //创建LuaEnv对象
        luaenv = new LuaEnv();       
    }

    [LuaCallCSharp]//加此标签,因为lua代码调用了它
    private void Update()
    {
        Debug.Log("旧函数");
    }

    private void OnGUI()
    {
        //按钮
        if (GUI.Button(new Rect(10, 10, 300, 80), "Hotfix"))
        {
            //替换xluaTest_Hot脚本的Update方法替换为自定义方法,准确说是内容替换。
            luaenv.DoString(@"
                --xlua.hotfix来执行函数替换,参数:类名、被替换函数名、替换的新函数。self是类本身(这里指xluaTest_Hot),如this。如果函数有参数的话,就继续在self后面写就行。
                xlua.hotfix(CS.xluaTest_Hot,'Update',function(self)
                    local a = CS.UnityEngine.GameObject.Find('Main Camera')
                    CS.UnityEngine.Debug.Log(a.name)
                end)
            ");
        }
    }

    private void OnDestroy()
    {
        //销毁LuaEnv对象
        luaenv.Dispose();
    }
}

然后“生成代码”、“插入代码”,再运行即可。点击按钮就可以发现Update函数的执行内容发生了变化。

        核心思想就是,借助xlua代码的执行,去修改我们的代码逻辑,比如这里的函数替换,用新逻辑替换旧逻辑。但要注意xlua代码的执行位置,注意逻辑间的执行顺序。xlua代码在一个.txt文件内,所以我们可以通过更新文件的方法更新xlua文件,来更新文件内容,来更新代码,更新逻辑。

3.3.2 改进

        整理下代码,改为读取.txt文件中的lua代码形式:

myLua.lua.txt

--替换xluaTest_Hot脚本的Update方法替换为自定义方法,准确说是内容替换。
--xlua.hotfix来执行函数替换,参数:类名、被替换函数名、替换的新函数。self是类本身(这里指xluaTest_Hot),如this。如果函数有参数的话,就继续在self后面写就行。
xlua.hotfix(CS.xluaTest_Hot,'Update',function(self)
                    local a = CS.UnityEngine.GameObject.Find('Main Camera')
                    CS.UnityEngine.Debug.Log(a.name)
                end)

myLuaDestroy.lua.txt

--将函数替换为nil,解除引用。注意,这里函数只是还原为原函数了。
xlua.hotfix(CS.xluaTest_Hot,'Update',nil)

xluaTest_Hot.cs

using UnityEngine;
using XLua;
using System.IO;

[Hotfix]//热更新标签
public class xluaTest_Hot : MonoBehaviour
{
    //一个LuaEnv实例对应Lua虚拟机,出于开销的考虑,建议全局唯一
    LuaEnv luaenv;

    private void Awake()
    {
        //创建LuaEnv对象
        luaenv = new LuaEnv();

        //添加自定义loader
        luaenv.AddLoader(MyLoader);

        //读取myLua.lua.txt文件内容,并执行
        luaenv.DoString("require 'myLua'");
    }
    
    [LuaCallCSharp]//加此标签,因为lua代码调用了它
    private void Update()
    {
        Debug.Log("旧函数");
    }

    private void OnDisable()
    {
        //读取myLua.lua.txt文件内容,并执行。
        //执行的是清除引用操作,这样在OnDestroy里才能正常释放虚拟机,若不清除引用则会报错。至于是否放在OnDisable里还有待讨论,但肯定得在.Dispose前执行,且不能在一个函数体内。
        //替换函数实际是让C#委托指向Lua中的函数,委托上的函数就是我们要替换的新函数,委托不为空就会去执行委托函数,我们清除引用也只是将委托清空,还原为了未替换前的状态,即后续若再执行原函数,则会执行没替换前的内容。
        luaenv.DoString("require 'myLuaDestroy'");
    }

    private void OnDestroy()
    {
        //销毁LuaEnv对象
        luaenv.Dispose();
    }

    //自定义loader
    private byte[] MyLoader(ref string filePath)
    {
        //获取完整路径
        string path = @"E:\_U3D\Note_Projects\009_AssetBundle_xLua\009_AssetBundle_xLua\Assets\XluaScene\Resources\" + filePath + ".lua.txt";
        //读取文件内容,并转为字节数组返回(这里使用的是UTF8,要求.txt文件也是此编码格式,否则会报错,若是此问题则将.txt文件另存一下即可,在另存时设置UTF8格式)
        return System.Text.Encoding.UTF8.GetBytes(File.ReadAllText(path));
    }
}

3.4 热更新开发流程

  1. 首先是开发业务代码。
  2. 在所有可能出现问题的类上打上[Hotfix]标签,即标记我们想热更新的类。
  3. 在所有涉及被lua调用的C#方法上打上[LuaCallCSharp]。
  4. 在所有调用lua的C#方法上打上[CSharpCallLua]。
  5. 打包发布。
  6. 后续更新时,修改代码时(如BUG之类)只需要更新lua文件即可,修改资源时(声音、模型、贴图、图片、UI等)只需要更新AB包(AssetBundle)即可。用户只需要下载lua文件、AB包。

3.5 各种功能语法

3.5.1 替换函数

        看3.3小案例。

3.5.2 释放对lua函数的引用

        看3.3小案例。

3.5.3 访问私有成员

        在lua脚本里,我们一般只能访问C#类对象的公共成员,若想访问私有程序则需要一些处理。

--加上下面这句,即可访问xluaTest_Hot类的私有成员
xlua.private_accessible(CS.xluaTest_Hot)--私有访问!
xlua.hotfix(CS.xluaTest_Hot,'Update',function(self)
                    --处理内容,如self.私有成员
                end)

3.5.4 只替换函数部分

        说是只替换部分,实际是先把原函数执行一遍,再根据需要再在后面进行一些修改操作。

        首先需要一个.lua文件util.lua.txt,是官方提供的,需要将其复制到我们自己的.lua文件同目录下(放同目录下不是必须的,只是这样在引用时会比较方便,只需要引用文件名即可,也可以放到别的文件夹里,但这样在引用的时候,就需要额外加文件夹路径。像下面代码里,我在自己的.lua文件同目录下创建了一个lib文件夹,再将其复制进去,在引用的时候就需要加上lib)。util文件所在目录:“XLua\Resources\xlua”。

--获取util.lua.txt文件对象,这里lib是文件夹,与当前.lua文件同目录
local util = require 'lib/util'
--使用util的hotfix_ex函数来执行替换函数操作。原之前的区别在于,这个可以调用被替换函数本身
util.hotfix_ex(CS.xluaTest_Hot,'Update',function(self)
					--调用被替换函数本身
					self:Update()
					--执行其他操作
					local a = CS.UnityEngine.GameObject.Find('Main Camera')
					CS.UnityEngine.Debug.Log(a.name)
                end)

PS:那释放引用的时候怎么释放?用util.hotfix_ex来释放吗?不用,继续用xlua.hotfix。

3.5.5 随机数问题(lua中的类型转换)

--调用C#中的随机数函数
CS.UnityEngine.Random.Range(0,4)

--[[
存在问题:
因为lua中不区分int和float,但Range函数区分,这里会返回float类型。
若我们是使用一个C#中的int类型接收,或想要一个int类型,则需要执行强转操作,否则会给默认0值。
--]]

--使用C#中的向下取值来处理,转换为int类型
CS.UnityEngine.Mathf.Floor(CS.UnityEngine.Random.Range(0,4))

3.6 下载补丁

        AB包就不说了,之前的AB包文章里已经讲了如何下载新的AB包。

        然后就是Lua文件的下载。和AB包非常相似,就是在使用UnityWebRequest时有些不同,以及最后要将lua文件保存。

using System.Collections;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;


public class LoadFromWeb : MonoBehaviour
{
    void Start()
    {
        //开启协程,下载file.lua.txt文件
        StartCoroutine(DownloadLuaFile(@"http://ip地址/文件目录/file.lua.txt"));
    }

    IEnumerator DownloadLuaFile(string url)
    {
        //创建文件下载请求
        UnityWebRequest request = UnityWebRequest.Get(url);
        //开始请求下载并等待
        yield return request.SendWebRequest();
        //下载完成后,获取文件内容
        string str = request.downloadHandler.text;
        //保存文件。重写指定路径下file.lua.txt文件的内容(没有则创建),将内容存储进去。
        File.WriteAllText(@"xxx\x\x\xx\x\file.lua.txt", str);
    }
}

3.7 标识要热更新的类型(Hotfix标签)

        PS:这块直接粘贴的Xlua包中文档内的内容。主要说Hotfix标签怎么用,文档演示的直接放在类前,这种实际上不太合适了,官方还给出了另外一种方式,如下。

和其它配置一样,有两种方式

方式一:直接在类里头打Hotfix标签(不建议,示例只是为了方便演示采取这种方式);

!!注意,方式一在高版本unity不支持

方式二:在一个static类的static字段或者属性里头配置一个列表。属性可以用于实现的比较复杂的配置,比如根据Namespace做白名单。

!!注意,高版本Unity需要把配置文件放Editor目录下

//如果涉及到Assembly-CSharp.dll之外的其它dll,如下代码需要放到Editor目录
public static class HotfixCfg
{
    [Hotfix]
    public static List<Type> by_field = new List<Type>()
    {
        typeof(HotFixSubClass),
        typeof(GenericClass<>),
    };

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

4 结束语

        xLua的内容到这就差不多了,基础的内容基本上都说了下,更多细节问题得去看官方的文档,文档就在下载的Xlua包里,前面也有说过路径在哪,然后github上也有一些说明。想全部了解透彻的话肯定要把这些文档教程都看看才行,当然也可以摆烂直接用,哪里有问题了再去查。

猜你喜欢

转载自blog.csdn.net/Davidorzs/article/details/135167707