【Unity入门】热更新框架之xLua

目录

一、xLua概述

1.1xLua简介

xLua是由腾讯维护的一个开源项目,xLua为Unity、 .Net、 Mono等C#环境增加Lua脚本编程的能力,借助xLua,这些Lua代码可以方便的和C#相互调用。自2016年初推广以来,已经应用于十多款腾讯自研游戏,因其良好性能、易用性、扩展性而广受好评。现在,腾讯已经将xLua开源到GitHub。

git下载地址:https://github.com/Tencent/xLua

xLua教程地址:https://github.com/Tencent/xLua/blob/master/Assets/XLua/Doc/XLua教程.md

xLua在功能、性能、易用性都有不少突破,这几方面分别最具代表性的是:

  1. 可以运行时把C#实现(方法,操作符,属性,事件等等)替换成lua实现;
  2. 出色的GC优化,自定义struct,枚举在Lua和C#间传递无C# gc alloc;
  3. 编辑器下无需生成代码,开发更轻量;

除此之外,xLua另一特色功能就是代码热补丁。非常适合前期没有规划使用Lua进行逻辑开发,后期又需要在iOS这种平台获得代码热更新能力的项目。

1.2xLua安装

  1. 首先下载最新版xLua ,然后解压到你想解压到的位置;

  2. xLua-master工程打开后,你将会看到一个Assets目录,点击到Assets目录会看到以下几个文件

    将 xLua-master 工程中Assets目录下的文件复制到你的Unity工程的Assets目录下即可;

  3. 加载完后,菜单栏有 xlua 菜单表示,导入成功

  4. 在菜单栏中 xlua 菜单添加 Hotfix Inject In Editor 选项

    菜单栏中 xlua 菜单会显示 Hotfix Inject In Editor 选项

  5. 如果希望安装到其它目录,可参看:https://github.com/Tencent/xLua/blob/master/Assets/XLua/Doc/faq.md

  6. xLua API可参见链接:https://github.com/Tencent/xLua/blob/master/Assets/XLua/Doc/XLua_API.md

示例代码:

csharp 复制代码
using UnityEngine;
//引入 Xlua 命名空间
using XLua;

public class HotFixScript : MonoBehaviour
{
    //lua 环境变量
    private LuaEnv luaEnv;

    private void Start()
    {
        //创建lua运行环境
        luaEnv = new LuaEnv();
        luaEnv.DoString("print('Hellow World 1')");
    }

    private void OnDestroy()
    {
        //释放lua环境
        luaEnv.Dispose();
    }
}

编写完脚本,挂载到游戏物体上,先按 Generate Code 选项生成代码,再按 Hotfix Inject In Editor 选项将补丁注入编辑器,运行结果:

二、Lua文件加载

2.1执行字符串

最基本是直接用LuaEnv.DoString执行一个字符串,当然,字符串得符合Lua语法

比如:luaenv.DoString("print('hello world')")

完整代码见XLua\Tutorial\LoadLuaScript\ByString目录。

但这种方式并不建议,更建议下面介绍这种方法加载Lua文件

2.2加载Lua文件

用lua的require函数即可,比如:DoString("require 'byfile'")

require实际上是调一个个的loader去加载,有一个成功就不再往下尝试,全失败则报文件找不到。

完整代码见XLua\Tutorial\LoadLuaScript\ByFile目录。

2.3自定义loader

在xLua加自定义loader是很简单的,只涉及到一个接口:

public delegate byte[] CustomLoader(ref string filepath);

public void LuaEnv.AddLoader(CustomLoader loader);

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

有了这个就简单了,用IIPS的IFS?没问题。写个loader调用IIPS的接口读文件内容即可。文件已经加密?没问题,自己写loader读取文件解密后返回即可。

示例代码:

csharp 复制代码
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
using XLua;

public class LuaStudy : MonoBehaviour
{
    private LuaEnv luaEnv;

    private void Start()
    {
        //创建lua运行环境
        luaEnv = new LuaEnv();
        LuaLoaderFun();
        VisitLua_Table();
        VisitLua_Function();
    }

    private void OnDestroy()
    {
        //释放lua环境
        luaEnv.Dispose();
    }

    /// <summary>
    /// 加载及调用lua脚本
    /// </summary>
    void LuaLoaderFun()
    {
        //执行lua的输出语句
        luaEnv.DoString("print('Hellow World 1')");

        //在lua中调用C#方法
        luaEnv.DoString("CS.UnityEngine.Debug.Log('Hellow World 2')");

        //加载运行Resources文件中的lua脚本
        TextAsset ta = Resources.Load<TextAsset>("HellowWorld.lua");
        luaEnv.DoString(ta.text);
        //加载运行helloworld.lua.txt  默认从Resources文件中进行加载
        luaEnv.DoString("require 'HellowWorld'");
       
        // 添加自定义的Loader方法
        //添加了一个自定义的Loader返回null并且DoString里面添加的是一个不存在的lua文件。它会返回错误。
        //luaEnv.AddLoader(MyLoader_1);
        //luaEnv.DoString("require 'xxx'");

        // 添加自定义的Loader方法
        //添加了一个自定义的Loader返回lua语句的二进制并且DoString里面添加的是一个不存在的lua文件。它会执行自定义的Loader的输出。
        //luaEnv.AddLoader(MyLoader_2);
        //luaEnv.DoString("require 'xxx'");

        //通过自定义Loader加载指定目录的Lua脚本
        luaEnv.AddLoader(MyLoader_3);
        luaEnv.DoString("require 'Lua_StreamingAssets'");
    }

    /// <summary>
    /// 访问获取lua属性和表
    /// </summary>
    void VisitLua_Table() {

        //调用lua脚本才能访问属性
        luaEnv.DoString("require 'LuaTest'");

        //获取lua脚本中的全局变量
        int num = luaEnv.Global.Get<int>("num");//获取lua里面的全局变量num
        string name = luaEnv.Global.Get<string>("name");//获取lua里面的全局变量name
        Debug.Log("num:" + num + "  name:" + name);

        //访问lua中的table(映射到class)
        Person personTemp = luaEnv.Global.Get<Person>("person");
        Debug.Log(personTemp.name + "    " + personTemp.age);
        //这种方式的访问,修改age的值不会影响到lua里面的表的属性
        personTemp.name = "wang";
        personTemp.age = 50;
        //修改后试一下
        Person personTemp2 = luaEnv.Global.Get<Person>("person");
        Debug.Log(personTemp2.name + "    " + personTemp2.age);

        //访问lua中的table(映射到interface)
        //映射到interface修改p中的属性,lua中的原table也会发生变化
        IPerson personTemp3 = luaEnv.Global.Get<IPerson>("person");
        Debug.Log(personTemp2.name + "    " + personTemp2.age);

        //通过LuaTable访问table
        List<object> list = luaEnv.Global.Get<List<object>>("person");
        foreach (object o in list)
        {
            print(o);
        }

        //通过LuaTable类 比较慢
        LuaTable luaTable = luaEnv.Global.Get<LuaTable>("person");
        print(luaTable.Get<string>("name"));
        print(luaTable.Get<int>("age"));
    }

    /// <summary>
    /// 访问lua中的全局方法
    /// </summary>
    void VisitLua_Function()
    {
        //调用lua脚本才能访问属性
        luaEnv.DoString("require 'LuaTest'");

        //使用委托来获取Lua中的全局函数
        Add add = luaEnv.Global.Get<Add>("add");
        add(2, 4);

        //通过LuaFunction访问Lua中的全局函数
        LuaFunction luaFunction = luaEnv.Global.Get<LuaFunction>("add");
        luaFunction.Call(2, 4);
    }

    private byte[] MyLoader_1(ref string filePath)
    {
        print(filePath);
        return null;
    }

    private byte[] MyLoader_2(ref string filePath) {
        string s = "print(123)";
        return System.Text.Encoding.UTF8.GetBytes(s);
    }

    private byte[] MyLoader_3(ref string filePath) {
        string path = Application.streamingAssetsPath + "/" + filePath + ".lua.txt";
        return System.Text.Encoding.UTF8.GetBytes(File.ReadAllText(path));
    }

    class Person
    {
        public string name;
        public int age;
    }

	//打标签
    [CSharpCallLua]
    public interface IPerson
    {
        string name { get; set; }
        int age { get; set; }
    }
	//打标签
    [CSharpCallLua]
    delegate void Add(int a, int b);

}

完整示例见XLua\Tutorial\LoadLuaScript\Loader。

三、xLua文件配置

xLua所有的配置都支持三种方式:打标签;静态列表;动态列表。

配置有两必须两建议:

  • 列表方式均必须是静态的字段/属性
  • 列表方式均必须放置一个静态类
  • 建议使用标签方式
  • 建议列表方式配置放Editor目录(如果是Hotfix配置, - - 并且类位于Assembly-CSharp.dll之外的其他dll,必须放Editor目录)

3.1打标签

xLua用白名单来指明生成哪些代码,而白名单通过attribute来配置,比如你想从lua

调用c#的某个类,希望生成适配代码,你可以为这个类型打一个LuaCallCSharp标签:

csharp 复制代码
[LuaCallCSharp]
publicclassA
{

}

该方式方便,但在il2cpp下会增加不少的代码量,不建议使用。

常用标签如下所示:

  1. [XLua.LuaCallCSharp]:为一个C#类型加了这个配置,xLua会生成这个类型的适配代码(否则将会尝试用性能较低的反射方式来访问),提供给Lua调用。

  2. [XLua.CSharpCallLua]:如果希望Lua文件中的逻辑能够被C#调用,可为Lua重的函数等打上该标签。

  3. [XLua.Hotfix]:用于标记需要进行热更新的文件。

除此之外,xLua还提供了其他标签,具体可参见链接:https://github.com/Tencent/xLua/blob/master/Assets/XLua/Doc/configure.md

3.2静态列表

有时我们无法直接给一个类型打标签,比如系统api,没有源码的库,或者实例化的泛化类型,那么你可以在一个静态类里声明一个静态字段,该字段的类型除BlackList和AdditionalProperties之外只要实现了IEnumerable就可以了(这两个例外后面具体会说),然后为这字段加上标签:

csharp 复制代码
[LuaCallCSharp]
public static List<Type> mymodule_lua_call_cs_list = new List<Type>()
{
    typeof(GameObject),
    typeof(Dictionary<string, int>),
};

该字段需要放置一个静态类里头,建议放置Editor目录。

3.3动态列表

声明一个静态属性,打上相应的标签即可。

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

Getter是代码,你可以实现很多效果,比如按名称空间配置,按程序集配置等等。

该属性需要放置一个静态类里头,建议放置Editor目录。

四、Lua与C#交互

4.1 C#访问Lua

4.1.1 获取一个全局基本数据类型

访问LuaEnv.Global就可以了,上面有个模版Get方法,可指定返回的类型。

csharp 复制代码
luaenv.Global.Get<int>("a")
luaenv.Global.Get<string>("b")
luaenv.Global.Get<bool>("c")

4.1.2 访问一个全局的table

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要慢一个数量级,比如没有类型检查。

4.1.3 访问一个全局的function

1、映射到delegate

这种是建议的方式,性能好很多,而且类型安全。缺点是要生成代码(如果没生成代码会抛InvalidCastException异常)。

delegate要怎样声明呢?

对于function的每个参数就声明一个输入类型的参数。

多返回值要怎么处理?从左往右映射到c#的输出参数,输出参数包括返回值,out参数,ref参数。

参数、返回值类型支持哪些呢?都支持,各种复杂类型,out,ref修饰的,甚至可以返回另外一个delegate。

delegate的使用就更简单了,直接像个函数那样用就可以了。

2、映射到LuaFunction

这种方式的优缺点刚好和第一种相反。

使用也简单,LuaFunction上有个变参的Call函数,可以传任意类型,任意个数的参数,返回值是object的数组,对应于lua的多返回值。

4.2 Lua调用C#

4.2.1 new C#对象

在C#这样new一个对象:

csharp 复制代码
var newGameObj = new UnityEngine.GameObject();

对应到Lua是这样:

csharp 复制代码
local newGameObj = CS.UnityEngine.GameObject()

基本类似,除了:

lua里头没有new关键字;

所有C#相关的都放到CS下,包括构造函数,静态成员属性、方法;

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

csharp 复制代码
local newGameObj2 = CS.UnityEngine.GameObject('helloworld')

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

读静态属性

csharp 复制代码
CS.UnityEngine.Time.deltaTime

写静态属性

csharp 复制代码
CS.UnityEngine.Time.timeScale = 0.5

调用静态方法

csharp 复制代码
CS.UnityEngine.GameObject.Find('helloworld')

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

csharp 复制代码
local GameObject = CS.UnityEngine.GameObject
GameObject.Find('helloworld')

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

读成员属性

csharp 复制代码
testobj.DMF

写成员属性

csharp 复制代码
testobj.DMF = 1024

调用成员方法

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

csharp 复制代码
testobj:DMFunc()

4.2.4 访问父类属性,方法

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

类的成员属性,成员方法。

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

Lua调用测的参数处理规则:C#的普通参数算一个输入形参,ref修饰的算一个输入形参

out修饰的不算,然后从左往右对应lua 调用测的实参列表;

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

4.2.6 重载方法

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

testobj:TestFunc(100)

testobj:TestFunc('hello')

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

注意:xlua只一定程度上支持重载函数的调用,因为lua的类型远远不如C#丰富,存在一

对多的情况,比如C#的int,float,double都对应于lua的number,上面的例子中

TestFunc如果有这些重载参数,第一行将无法区分开来,只能调用到其中一个(生成代码

中排前面的那个)。

4.2.7 操作符

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

4.2.8 参数带默认值的方法

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

4.2.9 可变参数方法

对于C#的如下方法:

csharp 复制代码
void VariableParamsFunc(int a, params string[] strs)

可以在lua里头这样调用:

csharp 复制代码
testobj:VariableParamsFunc(5, 'hello', 'john')

4.2.10 使用Extension methods

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

4.2.11 泛化(模版)方法

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

4.2.12 枚举类型

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

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

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

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

CS.Tutorial.TestEnum.__CastFrom(1)

CS.Tutorial.TestEnum.__CastFrom('E1')

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

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

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

C#delegate或者是lua函数。

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

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

4.2.14 event

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

增加事件回调

testobj:TestEvent('+', lua_event_callback)

移除事件回调

testobj:TestEvent('-', lua_event_callback)

4.2.15 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)。

4.2.16 "强"转

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

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

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

4.2.17 C#与Lua交互原理

1、C#调用Lua:C#先调用Lua的dll文件(C语言写的库),dll文件执行Lua代码

2、Lua调用C#:

1、Wrap方式:非反射机制,需要为源文件生成相应的wrap文件,当启动

Lua虚拟机时,Wrap文件将会被自动注册到Lua虚拟机中,之后,Lua文件

将能够识别和调用C#源文件。

总结:Lua调用Wrap文件, Wrap文件调用C#源文件

2、反射机制

五、xLua热更新

5.1 使用方式

  1. 添加HOTFIX_ENABLE宏打开该特性(在Unity3D的File->Build Setting->Scripting Define Symbols下添加)。编辑器、各手机平台这个宏要分别设置!如果是自动化打包,要注意在代码里头用API设置的宏是不生效的,需要在编辑器设置。(建议平时开发业务代码不打开HOTFIX_ENABLE,只在build手机版本或者要在编译器下开发补丁时打开HOTFIX_ENABLE)

  2. 执行XLua/Generate Code菜单。

  3. 注入,构建手机包这个步骤会在构建时自动进行,编辑器下开发补丁需要手动执行"XLua/Hotfix Inject In Editor"菜单。注入成功会打印"hotfix inject finish!"或者"had injected!"。

5.2 约束

不支持静态构造函数,目前只支持Assets下代码的热补丁,不支持引擎,c#系统库的热补丁。

5.3 API

  1. xlua.hotfix(class, [method_name], fix)
    描述 : 注入lua补丁
  • class : C#类,两种表示方法,CS.Namespace.TypeName或者字符串方式"Namespace.TypeName",字符串格式和C#的Type.GetType要求一致,如果是内嵌类型(Nested Type)是非Public类型的话,只能用字符串方式表示"Namespace.TypeName+NestedTypeName";
  • method_name : 方法名,可选;
  • fix : 如果传了method_name,fix将会是一个function,否则通过table提供一组函数。table的组织按key是method_name,value是function的方式。

base(csobj)

描述 : 子类override函数通过base调用父类实现。

csobj : 对象

返回值 : 新对象,可以通过该对象base上的方法

  1. util.hotfix_ex(class, method_name, fix)
    描述 : xlua.hotfix的增强版本,可以在fix函数里头执行原来的函数,缺点是fix的执行会略慢。
  • method_name : 方法名;
  • fix : 用来替换C#方法的lua function。

5.4 Hotfix Flag

Hotfix标签可以设置一些标志位对生成代码及插桩定制化。

Stateless、Stateful

遗留设置,Stateful方式在新版本已经删除,因为这种方式可以用xlua.util.hotfix_state接口达到类似的效果。

ValueTypeBoxing

值类型的适配delegate会收敛到object,好处是代码量更少,不好的是值类型会产生boxing及gc,适用于对text段敏感的业务。

IgnoreProperty

不对属性注入及生成适配代码,一般而言,大多数属性的实现都很简单,出错几率比较小,建议不注入。

IgnoreNotPublic

不对非public的方法注入及生成适配代码。除了像MonoBehaviour那种会被反射调用的私有方法必须得注入,其它仅被本类调用的非public方法可以不注入,只不过修复时会工作量稍大,所有引用到这个函数的public方法都要重写。

Inline

不生成适配delegate,直接在函数体注入处理代码。

IntKey

不生成静态字段,而是把所有注入点放到一个数组集中管理。

5.5 使用建议

对所有较大可能变动的类型加上Hotfix标识;

建议用反射找出所有函数参数、字段、属性、事件涉及的delegate类型,标注CSharpCallLua;

业务代码、引擎API、系统API,需要在Lua补丁里头高性能访问的类型,加上

LuaCallCSharp

引擎API、系统API可能被代码剪裁调(C#无引用的地方都会被剪裁),如果觉得可能会

新增C#代码之外的API调用,这些API所在的类型要么加LuaCallCSharp,要么ReflectionUse;

六、热更新大致流程

  1. 游戏一开始启动时,检查远程服务器上是否有新的myHotfix.lua文件(例如:md5比对,自己加解密),有的话,下载下来,放在指定目录,没有的话,读取本地已有的myHotfix.lua文件,若文件不存在,则说明不需要热修复,一般这种情况是在项目刚发布的早期,没有进行打补丁;
  2. 项目发布版本前,需要在CustomGenConfig.cs中 加入需要添加[Hotfix]标签的类型,想要更灵活使用的话,可以自己写个配置表,读取配置中的类型,自动添加,***只有加入[Hotfix]标签的类型,才可以调用xlua.hotfix()。
相关推荐
那个村的李富贵5 分钟前
unity编辑器工具,输出使用的字体
unity·编辑器·游戏引擎
游乐码15 小时前
UnityGUI(五)GUI控件综合使用
开发语言·unity·c#
LF男男15 小时前
TshitBullect.cs
unity
游乐码1 天前
Unity(十六)切换场景及鼠标相关
unity·游戏引擎
FakeEnd1 天前
Unity开发笔记6
笔记·unity·游戏引擎
游乐码1 天前
Unity(十七)Unity随机数及Unity委托
unity·游戏引擎
ellis19701 天前
Unity性能优化之检测工具Profiler
unity·性能优化
RPGMZ1 天前
RPGMZ游戏引擎 一个窗口 文本居中显示
开发语言·javascript·游戏引擎·rpgmz
tohand1 天前
Unity 完美假阴影实现文档
unity·游戏引擎
@蓝莓果粒茶1 天前
【Unity笔记】保姆级AssetBundle详解(含代码+避坑指南)
笔记·游戏·unity