Unity热更新 xLua

时间:2024-11-19 10:33:43

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

通过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

反过来,也可以使用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

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

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

Unity热更新 xLua

Unity热更新 xLua

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是很简单的,只涉及到一个接口:

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

public void LuaEnv.AddLoader(CustomLoader loader)

通过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

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

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

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

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

注意:映射到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

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

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

C#访问Lua   获取全局函数

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

Unity热更新 xLua

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

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

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

Unity热更新 xLua

Unity热更新 xLua

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

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

读成员属性

testobj.DMF

写成员属性

testobj.DMF = 1024

Unity热更新 xLua