找回密码
 立即注册
查看: 670|回复: 0

Unity xLua 热更新(三)

[复制链接]
发表于 2024-1-20 13:58 | 显示全部楼层 |阅读模式
前言

这一篇主要讲xLua的安装、使用xLua实现C#与Lua之间的交互。
关于Lua的语法和元表,可以参考我的前两篇文章。
xLua

介绍

前面两篇文章说过,xLua就是目前热更新的Lua解决方案中主要流派之一。这里照搬一下xLua的官方说明,同时该页面也是xLua的下载地址。
xLua为Unity、 .Net、 Mono等C#环境增加Lua脚本编程的能力,借助xLua,这些Lua代码可以便利的和C#彼此调用。xLua在功能、性能、易用性都有不少打破,这几方面分袂最具代表性的是:

  • 可以运行时把C#实现(方式,操作符,属性,事件等等)替换成lua实现;
  • 超卓的GC优化,自定义struct,枚举在Lua和C#间传递无C# gc alloc;
  • 编纂器下无需生成代码,开发更轻量;
安装

点开GitHub页面,直接下载源码安装包,通过Release安装的源码包也可。
解压压缩包后将Assets文件夹中的内容一股脑拖进项目的Assets目录就行了。
xLua官方是有一篇官方教程的。可以自行斟酌看官方教程还是这篇文章(毕竟这一篇还不会讲到热更新)。
hello world

首先需要实例化一个 LuaEnv 类,这个类可是说是 xLua 的万物发源。这里通过 LuaEnv 的DoString方式简单实现Hello World(调用C#输出),然后在使用结束后释放 LuaEnv (必然要释放,很多模块的Lua用完就可以释放了,如果不调用这个方式可能占必然内存),代码如下:
void Start()
{
    LuaEnv luaenv = new LuaEnv();
    luaenv.DoString(”CS.UnityEngine.Debug.Log('hello world')”);
    luaenv.Dispose();
}
这段代码直接运行就能正常运作了,不需要从头生成。但是编写完一段注入式C#代码时,需要通过上方菜单的XLua从头生成代码并注入。
Lua文件加载

虽然DoString理论上也能运行大段的Lua代码,但是正常都是用外部加载的方式来执行Lua的。
按照Lua的特性,通过require函数可以加载文件。xLua的require实际上是通过内置的Loader来加载文件的,如果Loader中有一个成功就不再往下测验考试,全掉败则报错(找不到文件)。xLua目前内置(默认)的Loader还会加载 Resource 文件夹,但是由于打包限制,Resource 文件夹下的Lua脚本要以.txt结尾,否则不会被打包进项目。
一般来说,我们会选择自定义Loader,通过调用 AddLoader 方式而且传入 CustomLoader 的委托,这个委托传入单个 string 的引用参数并返回 byte 数组。主要感化就是返回整个文件夹的 lua 脚本的字节流读取,定义方式很简单,如下代码:
    private void Awake()
    {
        luaEnv = new LuaEnv();
        luaEnv.AddLoader(MyLoader);
        luaEnv.DoString(”require 'Test'”);
    }

    private byte[] MyLoader(ref string filePath)
    {
        string absPath = ”E:\\Lua\\” + filePath + ”.lua”;
        return System.Text.Encoding.UTF8.GetBytes(File.ReadAllText(absPath));
    }
这里我在E盘设置了Lua文件夹,里面都是以 .lua结尾的 Lua 脚本,然后通过System直接读取字节流,实际上,这个传入的委托可以做的事情非常多,包罗解密、网络传输、多项合并等,只不外学习代码没必要整这么复杂。此中就有一个名为Test.lua的脚本。如果运行后无报错,其实就代表可以开始下一步编写了。
Lua调用C#

通过Lua调用C#是实现热更的重要一步,但是这两种语言分歧太大了,为了实现Unity的C#代码,xLua实现了面向对象而且封装了一些反射C#的方式。
比如,正常我们在C#实例化一个物体,我们会通过下面这种写法:
var go = Instantiate(_prefab);
如果在Lua调用,我们需要这样写:
local go = CS.UnityEngine.GameObject.Instantiate(_prefab);xLua通过CS表中的UnityEngine等表,模拟了Unity的C#环境,当我们调用这样的Lua脚本时,内部会通过一系列反射方式对应到C#语法并执行。
在写法上,Lua保持了原有的风格(没有new实例),而且需要完整地写出定名空间。当然,我们可以通过lua的成员声明,来实现类似using的方式,比如下段代码:
local GameObject = CS.UnityEngine.GameObject
GameObject.Instantiate(_prefab);
local go = GameObject.Find(”name”)而在使用非静态方式与非静态类时,往往我们需要一个实例才能操作,这时候需要LuaCallCSharp这一特性,这一特性可以声明在近乎任何处所。一旦某个类被声明这一特性了,就能在Lua进行访谒,同时__call元方式会被赋值(也就是可以模拟构造函数)。需要注意的是,我们通过Lua的类名(表)调用函数时,默认需要传入的第一个参数是self,也就是建议使用 : 调用方式
下面是一段测试的C#代码:
//省略了MonoBehavior包裹的类,然后挂载到物体上
private void Awake()
{
    luaEnv = new LuaEnv();
    luaEnv.AddLoader(MyLoader);
    luaEnv.DoString(”require 'Test'”);
}

[LuaCallCSharp]
public class Data
{
    public int x = 1;
    public void Method()
    {
        Debug.Log($”方式生效,目前为{x}”);
    }
}
可以看到我们的Data被设为该特性,此时我们对 Test.lua 里面编写Lua代码进行调用:
--没有定名空间,则默认在根表(CS)后面
local dataClass = CS.Data
--进行实例化(__call元方式模拟)
local data = dataClass()
--调用方式,注意使用的是 ':'
data:Method()
--输出 方式生效,目前为1
--直接更改
data.x = 100
data:Method()
--输出 方式生效,目前为100此时我们就已经完成了基础的Lua调用C#的流程。同时,由于xLua实现了面向对象的多态(包罗重载和覆写),所以我们可以做到在 Lua 调用重载方式(重名分歧参数)。同时xLua是lua 5.1版本(没有64位int),所以本身编写了一个64位运算库,而且撑持和默认的 number 斗劲与运算。
C#访谒Lua

C#访谒的是Lua的全局成员(通过_G表),而且可以进行读取与操作。由于Lua是弱类型语言,在映射到C#时很容易呈现类型转换的问题,这也是需要小心报错的处所。
Lua的全局成员代码如下(这里代码也能在XLua\Tutorial\CSharpCallLua下找到,本文颠末必然删减):
a = 1

d = {
   f1 = 12, f2 = 34,
   1, 2, 3,
   add = function(self, a, b)
      print('d.add called')
   end
}

function e()
    print('i am e')
end我们通过C#对a进行读写,代码如下(luaEnv提前实例化过了,也提前加载脚本了):
Debug.Log($”Lua的变量a为{luaEnv.Global.Get<int>(”a”)}”);
//输出 Lua的变量a为1
luaEnv.Global.Set(”a”, 100);
Debug.Log($”Lua的变量a为{luaEnv.Global.Get<int>(”a”)}”);
//输出 Lua的变量a为100
而如果访谒诸如d这样复杂的table,我们可以通过布局、类、容器(也是类)等方式读取,而理论上是可以进行更改的但是不建议,毕竟会导致原数值发生意料外的更改甚至是损坏,而且正常的主逻辑应该在C#而不是Lua,更改Lua的table值也很少用。
由于lua的表能存储的关系实在是太复杂了,我们声明的class/struct的字段可以多于或者少于table的成员。这样读取会Lua风格般地补缺省值或者舍弃多余值,而Lua则会实例化一个table出来作为赋值方,整个过程由于是值拷贝,其实开销是斗劲大的,而且二者不会同步更改数值(赋值完了就互不影响了)。
同时,如果使用容器读取时,除了会舍弃无法匹配泛型的值,List只会读取table中从1开始的数组值、Dictionary只会读取table中的字典值,斗劲类似于Lua自身的pairs与ipairs,区别是Dictionary实际上不会存入数组值(去掉pairs部门)。这部门斗劲抽象,但是实际上本身写一下而且打印就能理解了,代码如下:
public class DClass
{
    public int f1;
    public int f2;
}
private void Start()
{
    //映射到有对应字段的class,返回实例
    DClass d = luaEnv.Global.Get<DClass>(”d”);
    Debug.Log($”d的f1与f2分袂为{d.f1}和{d.f2}”);
    //输出 d的f1与f2分袂为12和34

    //映射到Dictionary<string, double>,注意会舍弃无法映射的值,返回实例
    Dictionary<string, double> dic = luaEnv.Global.Get<Dictionary<string, double>>(”d”);
    Debug.Log($”d的f1与f2分袂为{dic[”f1”]}和{dic[”f2”]},键值对存在{dic.Count}对”);
    //输出 d的f1与f2分袂为12和34,键值对存在2对

    //映射到List<double>,注意会舍弃无法映射的值,返回实例
    List<double> list = luaEnv.Global.Get<List<double>>(”d”); //映射到List<double>,by value
    Debug.Log($”键值对存在{list.Count}对”);
    //输出 键值对存在3对
}
当然,xLua也封装了一个非常强大的类 LuaTable 来实现完全映射,他可以通过Get传入字符串方式获取键值对,也能通过传入下标并out返回值来获取数组值(仍然是从1开始数),代码如下:
//映射到LuaTable
LuaTable t = luaEnv.Global.Get<LuaTable>(”d”);
//获取下标为1(就是Lua的第一个数)的值,并赋值给d
t.Get(1, out double d);
Debug.Log($”f1的值为:{t.Get<double>(”f1”)}、第一个数为:{d}”);
//输出 f1的值为:12、第一个数为:1
LuaTable类也是能Set来操作Lua中的全局成员的,但是不建议的理由同上。
至于类型为函数的Lua成员,则需要通过委托类来读取,而且可以正常传参、调用、获得返回值。而委托需要插手到生成列表,Unity自带的Action与Func是默认插手的,所以我们用Action来捕捉(如果需要捕捉返回值,则使用Func),代码如下:
//映射到一个delgate,要求delegate加到生成列表,否则返回null
Action e = luaEnv.Global.Get<Action>(”e”);
e();
//Lua输出 i am e
其他

实际上,xLua在每次生成的时候城市动态生成一系列C#脚本,出格是标注了 CSharpCallLua 特性的,有时候删掉这个特性下面的整个类或者方式就会呈现大量报错,从头生成代码页不管用。解决方式也很简单,能通过上方下拉菜单->XLua->Clear Generated Code清理代码;也能在Xlua/Gen文件夹下面存在所有的生成脚本,只需要删除已经不存在的类的对应生成脚本(包罗用于注册的代码行),就不报错了。
其他

估计下篇文章就讲完这个系列了,实际上xLua的热更新能非常深入,哪怕是这篇文章涉及的模块也只是提及了皮毛,想更深入学习的还是得大量使用加自学案例。
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-4-27 19:44 , Processed in 0.124296 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表