Unity热更新 xLua

摘要:
xLua是Unity3D下Lua编程解决方案,自2016年初推广以来,已经应用于十多款腾讯自研游戏,因其良好性能、易用性、扩展性而广受好评。现在,腾讯已经将xLua开源到GitHub。首先我们创建好一个Lua文件,然后在C#中加载后使用usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;usingXLua;publicclassMyHello:MonoBehaviour{voidStart(){TextAssett=Resources.Load;LuaEnvluaenv=newLuaEnv();luaenv.DoString;luaenv.Dispose();}}注意:在加载的时候,我们使用的是TextAsset文本格式,它默认识别的后缀为.txt,所以我们上面创建的lua文件后缀不是.lua,但是为了让我们方便的看出它是一个lua文件,所以取名的时候使用.lua.txt。

xLua是Unity3D下Lua编程解决方案,自2016年初推广以来,已经应用于十多款腾讯自研游戏,因其良好性能、易用性、扩展性而广受好评。现在,腾讯已经将xLua开源到GitHub。

2016年12月末,xLua刚刚实现新的突破:全平台支持用Lua修复C#代码bug。

目前Unity下的Lua热更新方案大多都是要求要热更新的部分一开始就要用Lua语言实现,不足之处在于:

  1. 接入成本高,有的项目已经用C#写完了,这时要接入需要把需要热更的地方用Lua重新实现;
  2. 即使一开始就接入了,也存在同时用两种语言开发难度较大的问题;
  3. Lua性能不如C#;

xLua热补丁技术支持在运行时把一个C#实现(函数,操作符,属性,事件,或者整个类)替换成Lua实现,意味着你可以:

  1. 平时用C#开发;
  2. 运行也是C#,性能秒杀Lua;
  3. 有bug的地方下发个Lua脚本fix了,下次整体更新时可以把Lua的实现换回正确的C#实现,更新时甚至可以做到不重启游戏; 这个新特性iOS,Android,Window,Mac都测试通过了,目前在做一些易用性优化。

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

xLua的使用

创建工程并导入xLua插件

Unity热更新 xLua第1张

通过xLua插件运行lua程序

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

public class MyHelloWorld : MonoBehaviour {

	void Start () {
        // 创建lua环境
        LuaEnv luaenv = new LuaEnv();
        // 运行Lua代码
        luaenv.DoString("print('Hello World')");
        // 关闭Lua环境
        luaenv.Dispose();
	}
}

可以看到,输出了打印,前缀有Lua的标识表示这是由Lua中的方法执行的

Unity热更新 xLua第2张

反过来,也可以使用lua调用C#中的程序

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

public class MyHelloWorld : MonoBehaviour {

	void Start () {
        // 创建lua环境
        LuaEnv luaenv = new LuaEnv();
        // 运行Lua代码
        //luaenv.DoString("print('Hello World')");
        luaenv.DoString("CS.UnityEngine.Debug.Log('Hello World')");
        // 关闭Lua环境
        luaenv.Dispose();
	}
}

这个时候,打印前就没有Lua标识符了,表示这是由C#中代码执行的

Unity热更新 xLua第3张

上面是C#和Lua之间的简单调用,但是在实际工作中,我们不可能这么写。我们的做法是写好Lua文件后,在C#中加载这个文件,然后使用其中的函数功能。

首先我们创建好一个Lua文件,然后在C#中加载后使用

Unity热更新 xLua第4张

Unity热更新 xLua第5张

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

public class MyHello : MonoBehaviour {

	void Start () {
        TextAsset t = Resources.Load<TextAsset>("helloworld.lua");

        LuaEnv luaenv = new LuaEnv();
        luaenv.DoString(t.ToString());
        luaenv.Dispose();
	}
}

注意:在加载的时候,我们使用的是TextAsset文本格式,它默认识别的后缀为.txt,所以我们上面创建的lua文件后缀不是.lua,但是为了让我们方便的看出它是一个lua文件,所以取名的时候使用.lua.txt。

除了上面的加载方法外,更常用的方法是使用require加载

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

public class MyHello : MonoBehaviour {

	void Start () {
        LuaEnv luaenv = new LuaEnv();
        luaenv.DoString("require 'helloworld'");
        luaenv.Dispose();
	}
}

require实际上是调一个个的loader去加载,有一个成功则不再往下尝试,全部失败则报文件找不到。目前Lua除了原生的loader外,还添加了从Resources加载的loader,需要注意的是Resources只支持有限的后缀,放在Resources下的lua文件需要加上.txt后缀。

自定义loader

我们发现上面的lua文件都是放在Resources文件夹下,因为原生的loader会在这个下面去加载。在我们的项目中,可能我们的lua文件放在自定义的文件夹下,这个时候就需要我们自定义loader,在xLua加自定义loader是很简单的,只涉及到一个接口:

publicdelegatebyte[] CustomLoader(refstringfilepath);

publicvoidLuaEnv.AddLoader(CustomLoaderloader)

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

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

public class CreateNewLoader : MonoBehaviour {

	void Start () {
        LuaEnv luaenv = new LuaEnv();
        // 自定义loader
        luaenv.AddLoader(MyLoader);
        luaenv.DoString("require 'newloaderText'");
        luaenv.Dispose();
	}
	
    private byte[] MyLoader(ref string filePath)
    {
        string absPath = Application.streamingAssetsPath + "/" + filePath + ".lua.txt";
        return System.Text.Encoding.UTF8.GetBytes(File.ReadAllText(absPath));
    }
}

上面代码中我们定义的lua文件为“newloaderText.lua”,该文件位于“StreamingAssets”文件夹下,该文件夹与Assets文件夹同级,所以在后面设置路径的时候使用系统自带的函数“Application.streamingAssetsPath”可以找到该文件夹。当然,我们也可以自定义文件夹的位置,后面的路径改一下就行。

上面的执行过程,注册回调后,调用require的时候,将“newloaderText”传递给回调函数"MyLoader",在此回调函数中我们加载到指定文件然后传回来使用。

C#访问Lua 获取全局变量

Unity热更新 xLua第6张

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

public class CSharpCallLua : MonoBehaviour {

	void Start () {
        LuaEnv luaenv = new LuaEnv();
        luaenv.DoString("require 'CSharpCallLua'");
        // 获取lua中的全局变量
        int num = luaenv.Global.Get<int>("num");
        string name = luaenv.Global.Get<string>("name");
        bool isPause = luaenv.Global.Get<bool>("isPause");
        Debug.Log("num:" + num);
        Debug.Log("name:" + name);
        Debug.Log("isPause:" + isPause);
        luaenv.Dispose();
	}
}

使用函数LuaEnv.Global就能访问,其中,luaenv.Global.Get<int>("num")中,<int>指的是要转换成的类型,"num"是在lua中定义的变量名

C#访问Lua 获取全局table

  • 映射到普通class或struct:定义一个class或者struct,有对应于table的字段的public属性,而且有无参数构造函数即可,比如对于{f1 = 100, f2 = 100}可以定义一个包含public int f1;public int f2;的class。这种方式下xLua会帮你new一个实例,并把对应的字段赋值过去。table的属性可以多于或者少于class的属性。可以嵌套其它复杂类型。

注意:lua的table中的字段名和C#的class中的字段名要一一对应(名字也要相同),否则取不到值。此种方式为值拷贝,修改class的字段值不会同步到table,反过来也不会。使用此种方式,不能访问lua的函数。

Unity热更新 xLua第7张

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

public class CSharpCallLua : MonoBehaviour {

	void Start () {
        LuaEnv luaenv = new LuaEnv();
        luaenv.DoString("require 'CSharpCallLua'");
        // 获取lua中的全局table
        Person p = luaenv.Global.Get<Person>("Person");
        Debug.Log("name:" + p.name);
        Debug.Log("age:" + p.age);
        luaenv.Dispose();
	}

    class Person
    {
        public string name;
        public int age;
    }
}
  • 映射到interface:这种方式依赖于生成代码(如果没生成代码会抛InvalidCastException异常),代码生成器会生成这个interface的实例,如果get一个属性,生成代码会get对应的table字段,如果set属性也会设置对应的字段。甚至可以通过interface的方法访问lua的函数

Unity热更新 xLua第8张

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

public class CSharpCallLua : MonoBehaviour {

	void Start () {
        LuaEnv luaenv = new LuaEnv();
        luaenv.DoString("require 'CSharpCallLua'");

        // 获取lua中的全局table(映射到interface)
        Person_1 p1 = luaenv.Global.Get<Person_1>("Person");
        Debug.Log("name:" + p1.name);
        Debug.Log("age:" + p1.age);
        p1.eat("apple");
        luaenv.Dispose();
	}

    [CSharpCallLua]
    interface Person_1
    {
        string name { get;set;}
        int age { get; set; }
        void eat(string str);
    }
}

注意:在lua中定义函数的时候,第一个参数是arg,需要写上,名字随意取都行,这里写的self。在C#中定义接口的时候,要加上标签[CSharpCallLua]

  • 映射到Dictionary<>,List<>

Unity热更新 xLua第9张

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

public class CSharpCallLua : MonoBehaviour {

	void Start () {
        LuaEnv luaenv = new LuaEnv();
        luaenv.DoString("require 'CSharpCallLua'");

        // 获取lua中的全局table(通过Dictionary)
        Dictionary<string, object> dict = luaenv.Global.Get<Dictionary<string, object>>("Person");
        foreach(string key in dict.Keys)
        {
            print("key:" + key + "   value:" + dict[key]);
        }
        luaenv.Dispose();
	}
}

Unity热更新 xLua第10张

注意:映射到Dictionary<>的时候,只映射了Lua中键值对的形式,普通的值没有映射过来

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

public class CSharpCallLua : MonoBehaviour {

	void Start () {
        LuaEnv luaenv = new LuaEnv();
        luaenv.DoString("require 'CSharpCallLua'");

        // 获取lua中的全局table(通过List)
        List<object> list = luaenv.Global.Get<List<object>>("Person");
        foreach(object o in list)
        {
            print(o);
        }
        luaenv.Dispose();
	}
}

Unity热更新 xLua第11张

注意:映射到List<>的时候,只映射了Lua中值的形式,键值对的形式没有映射过来

映射到LuaTable类:这种方式不常用,也不建议使用

C#访问Lua 获取全局函数

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

Unity热更新 xLua第12张

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

public class CSharpCallLua : MonoBehaviour {

	void Start () {
        LuaEnv luaenv = new LuaEnv();
        luaenv.DoString("require 'CSharpCallLua'");

        // 访问lua中的全局函数(映射到delegate)
        Add add = luaenv.Global.Get<Add>("add");
        int res1 = 0; int res2 = 0;
        int res = add(3, 4, out res1, out res2);
        print("res:" + res);
        print("res1:" + res1);
        print("res2:" + res2);
        add = null;
        luaenv.Dispose();
	}

    [CSharpCallLua]
    delegate int Add(int a, int b, out int res1, out int res2);
}

注意:使用delegate需要添加特性[CSharpCallLua],如果lua中函数返回多值,在C#中只能接收一个值,其它值从左往右映射到c#的输出参数,输出参数包括返回值,out参数,ref参数。

  • 映射到LuaFunction:这个性能不好,不建议使用

Lua访问C#

在C#这样new一个对象:

var newGameObj =new UnityEngine.GameObject();

对应到Lua是这样:

local newGameObj = CS.UnityEngine.GameObject()

Unity热更新 xLua第13张

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

public class LuaCallCSharp : MonoBehaviour {

	void Start () {
        LuaEnv luaenv = new LuaEnv();
        luaenv.DoString("require 'LuaCallCS'");
        luaenv.Dispose();
	}
}

Unity热更新 xLua第14张

Lua访问C#静态属性和方法

Unity热更新 xLua第15张

Unity热更新 xLua第16张

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

Lua访问C#成员属性和方法

读成员属性

testobj.DMF

写成员属性

testobj.DMF = 1024

Unity热更新 xLua第17张

免责声明:文章转载自《Unity热更新 xLua》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇第七部分(三) 动态渲染页面爬取(用Selenium获取淘宝商品,不涉及验证登录)记录用户登陆信息,你用PHP是如何来实现的下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

CentOS搭建python开发环境

装了个CentOS 5.5,想在上面搭个python的开发环境,可是还是遇到了很多问题,记录一下过程: 1、python升级 查看python版本 python -V Python 2.4.3 因为python3的变化很大,还是希望用新的版本,goole了一把,看到有一个指导贴: cd /usr/local/src wget http://www.py...

Halcon 17与 c# 混合编程

这篇主要是C#和Halcon的混合编程,在此基础上对按键不同功能的划分,以及图片适应窗口和从本地打开图片。 halcon源程序:   dev_open_window(0, 0, 512, 512, 'black', WindowHandle) read_image (Image, 'C:/Users/Administrator/Desktop/猫.jpg'...

C# AS与Is

在C#中,所有的东西都是对象。因此任何常数也是一个整型对象。这里用到了as,as是C#语言里面的一个关键字。as运算符类似于类型转换,所不同的是,当转换失败时,as运算符将产生空,而不是引发异常。在形式上,这种形式的表达式:expression as type as 运算符只执行引用转换和装箱转换。as 运算符无法执行其他转换,如用户定义的转换,这类转换应...

openresty性能优化 -- table相关优化

最近一直在做openresty相关开发,使用lua优化,优化了几次,发现最大的优化是table的优化。table优化的大原则是尽量少创建表,表创建多了毕竟耗性能。这里的创建,指新创建和扩表引起的创建。在往table插入数据的过程中,如果table不够用,会扩大两倍,所以,一个1030项数据,会经过十次扩表,非常消耗性能。 方法一,代码层面重用表 原始代码:...

使用Docker构建redis集群

1集群结构说明 集群中有三个主节点,三个从节点,一共六个结点。因此要构建六个redis的docker容器。在宿主机中将这六个独立的redis结点关联成一个redis集群。需要用到官方提供的ruby脚本。 2构建redis基础镜像 本文选择版本为redis-3.0.7,如果需要其他版本,直接修改wget后面地址中的版本号即可。 代码清单2-1 下载&...

GO 解决使用bee工具,报 bash: bee: command not found

我最近使用beego时,遇到以下问题:command not found使用vscode时,运行bee run,报以下错  我查到一篇文章csdn,说用拷贝bee.exe方法,我觉得纯扯淡 如何解决? 通常这种情况常在windows出现,苹果还没遇到这个问题,会出现这个问题的环境,往往修改过GOPATH。例如Go 的msi安装是默认会把环境变量配置好,但...