Lua - xLua逻辑热更新
前言
Lua基础语法中系统介绍了 Lua 的语法体系,本文将进一步介绍 Unity3D 中基于 xLua 实现逻辑热更新。
逻辑热更新是指:在保持程序正常运行的情况下,在后台修改代码逻辑,修改完成并推送到运行主机上,主机无缝接入更新后的代码逻辑。Unity3D 中,基于 Lua 的逻辑热更新方案主要有 ToLua、xLua、uLua、sLua,本文将介绍 xLua 逻辑热更新方案。
热更新的好处
- 不用浪费流量重新下载
- 不用通过商店审核,版本迭代更加快捷
- 不用重新安装,用户可以更快体验更新的内容
xLua 插件下载
xLua 是腾讯研发的 Unity3D 逻辑热更新方案,目前已开源,资源见:
xLua 插件导入
将插件的 Assets 目录下的所有文件拷贝到项目的 Assets 目录下,如下:
生成 Wrap 文件
导入插件后,菜单栏会多一个 XLua 窗口,点击 Generate Code 会生成一些 Wrap 文件,生成路径见【Assets\XLua\Gen】,这些 Wrap 文件是 C# 与 Lua 沟通的桥梁。每次生成文件时,建议先点击下 Clear Generate Code,再点击 Generate Code。
官方教程文档
在【Assets\XLua\Doc\XLua教程.doc】中可以查阅官方教程文档,在线教程文档见:
- github:https://github.com/Tencent/xLua/tree/master/Assets/XLua/Doc/XLua教程.md
- gitcode:https://gitcode.net/mirrors/Tencent/xLua/tree/master/Assets/XLua/Doc/XLua教程.md
官方Demo
xLua 应用
C# 中执行 Lua 代码串
HelloWorld.cs
using UnityEngine;
using XLua;
public class HelloWorld : MonoBehaviour {
private void Start() {
LuaEnv luaEnv = new LuaEnv();
string luaStr = @"print('Hello World')
CS.UnityEngine.Debug.Log('Hello World')";
luaEnv.DoString(luaStr);
luaEnv.Dispose();
}
}
运行如下:
说明:第一个日志是 lua 打印的,所以有 "LUA: " 标识,第二个日志是 Lua 调用 C# 的 Debug 方法,所以没有 "LUA: " 标识。
C# 中调用 Lua 文件
通过 Resources.Load 加载 lua 文件
ScriptFromFile.cs
using UnityEngine;
using XLua;
public class ScriptFromFile : MonoBehaviour {
private void Start() {
LuaEnv luaEnv = new LuaEnv();
TextAsset textAsset = Resources.Load<TextAsset>("02/LuaScript.lua");
luaEnv.DoString(textAsset.text);
luaEnv.Dispose();
}
}
LuaScript.lua.txt
print("Load lua script")
说明:LuaScript.lua.txt 文件放在 【Assets\Resources\02】目录下。因为 Resource 只支持有限的后缀,放 Resources 下的 lua 文件得加上 txt 后缀。
通过内置 loader 加载 lua 文件
ScriptFromFile.cs
using UnityEngine;
using XLua;
public class ScriptFromFile : MonoBehaviour {
private void Start() {
LuaEnv luaEnv = new LuaEnv();
luaEnv.DoString("require '02/LuaScript'");
luaEnv.Dispose();
}
}
说明:require 实际上是调一个个的 loader 去加载,有一个成功就不再往下尝试,全失败则报文件找不到。 目前 xLua 除了原生的 loader 外,还添加了从 Resource 加载的 loader。因为 Resource 只支持有限的后缀,放 Resources 下的 lua 文件得加上 txt 后缀。
通过自定义 loader 加载 lua 文件
ScriptFromFile.cs
using UnityEngine;
using XLua;
using System.IO;
using System.Text;
public class ScriptFromFile : MonoBehaviour {
private void Start() {
LuaEnv luaEnv = new LuaEnv();
luaEnv.AddLoader(MyLoader);
luaEnv.DoString("require '02/LuaScript'");
luaEnv.Dispose();
}
private byte[] MyLoader(ref string filePath) {
string path = Application.dataPath + "/Resources/" + filePath + ".lua.txt";
string txt = File.ReadAllText(path);
return Encoding.UTF8.GetBytes(txt);
}
}
C# 中调用 Lua 变量
AccessVar.cs
using UnityEngine;
using XLua;
public class AccessVar : MonoBehaviour {
private LuaEnv luaEnv;
private void Start() {
luaEnv = new LuaEnv();
luaEnv.DoString("require '03/LuaScript'");
TestAccessVar();
}
private void TestAccessVar() {
bool a = luaEnv.Global.Get<bool>("a");
int b = luaEnv.Global.Get<int>("b");
float c = luaEnv.Global.Get<float>("c");
string d = luaEnv.Global.Get<string>("d");
Debug.Log("a=" + a + ", b=" + b + ", c=" + c + ", d=" + d); // a=True, b=10, c=7.8, d=xxx
}
private void OnApplicationQuit() {
luaEnv.Dispose();
luaEnv = null;
}
}
LuaScript.lua.txt
a = true
b = 10
c = 7.8
d = "xxx"
C# 中调用 Lua table
通过自定义类映射 table
AccessTable.cs
using UnityEngine;
using XLua;
public class AccessTable : MonoBehaviour {
private LuaEnv luaEnv;
private void Start() {
luaEnv = new LuaEnv();
luaEnv.DoString("require '04/LuaScript'");
TestAccessTable();
}
private void TestAccessTable() {
Student stu = luaEnv.Global.Get<Student>("stu");
Debug.Log("name=" + stu.name + ", age=" + stu.age); // name=zhangsan, age=23
stu.name = "lisi";
luaEnv.DoString("print(stu.name)"); // LUA: zhangsan
}
private void OnApplicationQuit() {
luaEnv.Dispose();
luaEnv = null;
}
}
class Student {
public string name;
public int age;
}
LuaScript.lua.txt
stu = {name = "zhangsan", age = 23, sex = 0, 1, 2, 3}
说明:允许 table 中元素个数与自定义类中属性个数不一致,允许自定义类中属性顺序与 table 中元素顺序不一致;类中需要映射的属性名必须与 table 中相应元素名保持一致(大小写也必须一致);修改映射类的属性值,不影响 table 中相应元素的值。
通过自定义接口映射 table
AccessTable.cs
using UnityEngine;
using XLua;
public class AccessTable : MonoBehaviour {
private LuaEnv luaEnv;
private void Start() {
luaEnv = new LuaEnv();
luaEnv.DoString("require '04/LuaScript'");
TestAccessTable();
}
private void TestAccessTable() {
IStudent stu = luaEnv.Global.Get<IStudent>("stu");
Debug.Log("name=" + stu.name + ", age=" + stu.age); // name=zhangsan, age=23
stu.name = "lisi";
luaEnv.DoString("print(stu.name)"); // LUA: lisi
stu.study("program"); // LUA: subject=program
stu.raiseHand("right"); // LUA: hand=right
}
private void OnApplicationQuit() {
luaEnv.Dispose();
luaEnv = null;
}
}
[CSharpCallLua]
public interface IStudent {
public string name {get; set;}
public int age {get; set;}
public void study(string subject);
public void raiseHand(string hand);
}
说明:在运行脚本之前,需要先点击下 Clear Generate Code,再点击 Generate Code;允许 table 中元素个数与自定义接口中属性个数不一致,允许自定义接口中属性顺序与 table 中元素顺序不一致;接口中需要映射的属性名和方法名必须与 table 中相应元素名和函数名保持一致(大小写也必须一致);修改映射接口的属性值,会影响 table 中相应元素的值。
LuaScript.lua.txt
stu = {
name = "zhangsan",
age = 23,
study = function(self, subject)
print("subject="..subject)
end
}
--function stu.raiseHand(self, hand)
function stu:raiseHand(hand)
print("hand="..hand)
end
通过 Dictionary 映射 table
AccessTable.cs
using System.Collections.Generic;
using UnityEngine;
using XLua;
public class AccessTable : MonoBehaviour {
private LuaEnv luaEnv;
private void Start() {
luaEnv = new LuaEnv();
luaEnv.DoString("require '04/LuaScript'");
TestAccessTable();
}
private void TestAccessTable() {
Dictionary<string, object> stu = luaEnv.Global.Get<Dictionary<string, object>>("stu");
Debug.Log("name=" + stu["name"] + ", age=" + stu["age"]); // name=zhangsan, age=23
stu["name"] = "lisi";
luaEnv.DoString("print(stu.name)"); // LUA: zhangsan
}
private void OnApplicationQuit() {
luaEnv.Dispose();
luaEnv = null;
}
}
说明:修改映射 Dictionary 的元素值,不影响 table 中相应元素的值。
LuaScript.lua.txt
stu = {name = "zhangsan", age = 23, "math", 2, true}
通过 List 映射 table
AccessTable.cs
using System.Collections.Generic;
using UnityEngine;
using XLua;
public class AccessTable : MonoBehaviour {
private LuaEnv luaEnv;
private void Start() {
luaEnv = new LuaEnv();
luaEnv.DoString("require '04/LuaScript'");
TestAccessTable();
}
private void TestAccessTable() {
List<object> list = luaEnv.Global.Get<List<object>>("stu");
string str = "";
foreach(var item in list) {
str += item + ", ";
}
Debug.Log(str); // math, 2, True,
}
private void OnApplicationQuit() {
luaEnv.Dispose();
luaEnv = null;
}
}
LuaScript.lua.txt
stu = {name = "zhangsan", age = 23, "math", 2, true}
通过 LuaTable 映射 table
AccessTable.cs
using UnityEngine;
using XLua;
public class AccessTable : MonoBehaviour {
private LuaEnv luaEnv;
private void Start() {
luaEnv = new LuaEnv();
luaEnv.DoString("require '04/LuaScript'");
TestAccessTable();
}
private void TestAccessTable() {
LuaTable table = luaEnv.Global.Get<LuaTable>("stu");
Debug.Log("name=" + table.Get<string>("name") + ", age=" + table.Get<int>("age")); // name=zhangsan, age=23
table.Set<string, string>("name", "lisi");
luaEnv.DoString("print(stu.name)"); // LUA: lisi
table.Dispose();
}
private void OnApplicationQuit() {
luaEnv.Dispose();
luaEnv = null;
}
}
说明:修改映射 LuaTable 的属性值,会影响 table 中相应元素的值
LuaScript.lua.txt
stu = {name = "zhangsan", age = 23, "math", 2, true}
C# 中调用 Lua 全局函数
通过 delegate 映射 function
AccessFunc.cs
using System;
using UnityEngine;
using XLua;
public class AccessFunc : MonoBehaviour {
private LuaEnv luaEnv;
[CSharpCallLua] // 需要设置 public, 并且点击 Generate Code
public delegate int MyFunc1(int arg1, int arg2);
[CSharpCallLua] // 需要设置 public, 并且点击 Generate Code
public delegate int MyFunc2(int arg1, int arg2, out int resOut);
[CSharpCallLua] // 需要设置 public, 并且点击 Generate Code
public delegate int MyFunc3(int arg1, int arg2, ref int resRef);
private void Start() {
luaEnv = new LuaEnv();
luaEnv.DoString("require '05/LuaScript'");
TestAccessFunc1();
TestAccessFunc2();
TestAccessFunc3();
TestAccessFunc4();
}
private void TestAccessFunc1() { // 测试无参函数
Action func1 = luaEnv.Global.Get<Action>("func1");
func1(); // LUA: func1
}
private void TestAccessFunc2() { // 测试有参函数
Action<string> func2 = luaEnv.Global.Get<Action<string>>("func2");
func2("xxx"); // LUA: func2, arg=xxx
}
private void TestAccessFunc3() { // 测试有返回值函数
MyFunc1 func3 = luaEnv.Global.Get<MyFunc1>("func3");
Debug.Log(func3(2, 3)); // 6
}
private void TestAccessFunc4() { // 测试有多返回值函数
MyFunc1 func41 = luaEnv.Global.Get<MyFunc1>("func4");
Debug.Log(func41(2, 3)); // 5
int res, resOut;
MyFunc2 func42 = luaEnv.Global.Get<MyFunc2>("func4");
res = func42(2, 3, out resOut);
Debug.Log("res=" + res + ", resOut=" + resOut); // res=5, resOut=-1
int ans, resRef = 0;
MyFunc3 func43 = luaEnv.Global.Get<MyFunc3>("func4");
ans = func43(2, 3, ref resRef);
Debug.Log("ans=" + ans + ", resRef=" + resRef); // res=5, resRef=-1
}
private void OnApplicationQuit() {
luaEnv.Dispose();
luaEnv = null;
}
}
说明:Lua 函数支持多返回值,但 C# 函数不支持多返回值,要想让 C# 接收 Lua 函数的多个返回值,需要通过 out 或 ref 参数接收第 2 个及之后的返回值。
LuaScript.lua.txt
--无参函数
function func1()
print("func1")
end
--有参函数
function func2(arg)
print("func2, arg="..arg)
end
--有返回值函数
function func3(a, b)
return a * b
end
--有多返回值函数
function func4(a, b)
return a + b, a - b
end
通过 LuaFunction 映射 function
AccessFunc.cs
using UnityEngine;
using XLua;
public class AccessFunc : MonoBehaviour {
private LuaEnv luaEnv;
private void Start() {
luaEnv = new LuaEnv();
luaEnv.DoString("require '05/LuaScript'");
TestAccessFunc1();
TestAccessFunc2();
TestAccessFunc3();
TestAccessFunc4();
}
private void TestAccessFunc1() { // 测试无参函数
LuaFunction func1 = luaEnv.Global.Get<LuaFunction>("func1");
func1.Call(); // LUA: func1
}
private void TestAccessFunc2() { // 测试有参函数
LuaFunction func2 = luaEnv.Global.Get<LuaFunction>("func2");
func2.Call("xxx"); // LUA: func2, arg=xxx
}
private void TestAccessFunc3() { // 测试有返回值函数
LuaFunction func3 = luaEnv.Global.Get<LuaFunction>("func3");
object[] res = func3.Call(2, 3);
Debug.Log(res[0]); // 6
}
private void TestAccessFunc4() { // 测试有多返回值函数
LuaFunction func4 = luaEnv.Global.Get<LuaFunction>("func4");
object[] res = func4.Call(2, 3);
Debug.Log("res1=" + res[0] + ", res2=" + res[1]); // res1=5, res2=-1
}
private void OnApplicationQuit() {
luaEnv.Dispose();
luaEnv = null;
}
}
说明:LuaFunction 映射方式相较 delegate 方式,性能消耗较大。
LuaScript.lua.txt
--无参函数
function func1()
print("func1")
end
--有参函数
function func2(arg)
print("func2, arg="..arg)
end
--有返回值函数
function func3(a, b)
return a * b
end
--有多返回值函数
function func4(a, b)
return a + b, a - b
end
Lua 中创建 GameObject 并获取和添加组件
TestGameObject.cs
using UnityEngine;
using XLua;
public class TestGameObject : MonoBehaviour {
private LuaEnv luaEnv;
private void Start() {
luaEnv = new LuaEnv();
luaEnv.DoString("require '06/LuaScript'");
}
private void OnApplicationQuit() {
luaEnv.Dispose();
luaEnv = null;
}
}
LuaScript.lua.txt
local GameObject = CS.UnityEngine.GameObject
local PrimitiveType = CS.UnityEngine.PrimitiveType
local Color = CS.UnityEngine.Color
local Rigidbody = CS.UnityEngine.Rigidbody
GameObject("xxx") --创建空对象
go = GameObject.CreatePrimitive(PrimitiveType.Cube)
go:GetComponent("MeshRenderer").sharedMaterial.color = Color.red
rigidbody = go:AddComponent(typeof(Rigidbody))
rigidbody.mass = 1000
Lua 中访问 C# 自定义类
TestSelfClass.cs
using UnityEngine;
using XLua;
public class TestSelfClass : MonoBehaviour {
private LuaEnv luaEnv;
private void Awake() {
luaEnv = new LuaEnv();
luaEnv.DoString("require '07/LuaScript'");
}
private void OnApplicationQuit() {
luaEnv.Dispose();
luaEnv = null;
}
}
[LuaCallCSharp] // 需要点击 Generate Code
class Person {
public string name;
public int age;
public Person(string name, int age) {
this.name = name;
this.age = age;
}
public void Run() {
Debug.Log("run");
}
public void Eat(string fruit) {
Debug.Log("eat " + fruit);
}
public override string ToString() {
return "name=" + name + ", age=" + age;
}
}
LuaScript.lua.txt
local Person = CS.Person
person = Person("zhangsan", 23)
print("name="..person.name..", age="..person.age) -- LUA: name=zhangsan, age=23
print(person:ToString()) -- LUA: name=zhangsan, age=23
person:Run() -- run
person:Eat("aple") -- eat aple
Lua Hook MonoBehaviour 生命周期方法
TestLife.cs
using System;
using System.Collections.Generic;
using UnityEngine;
using XLua;
public class TestLife : MonoBehaviour {
private LuaEnv luaEnv;
private Dictionary<string, Action> func;
private void Awake() {
luaEnv = new LuaEnv();
luaEnv.DoString("require '08/LuaScript'");
GetFunc();
CallFunc("awake");
}
private void OnEnable() {
CallFunc("onEnable");
}
private void Start() {
CallFunc("start");
}
private void Update() {
CallFunc("update");
}
private void OnDisable() {
CallFunc("onDisable");
}
private void OnDestroy() {
CallFunc("onDestroy");
}
private void GetFunc() {
func = new Dictionary<string, Action>();
AddFunc("awake");
AddFunc("onEnable");
AddFunc("start");
AddFunc("update");
AddFunc("onDisable");
AddFunc("onDestroy");
}
private void AddFunc(string funcName) {
Action fun = luaEnv.Global.Get<Action>(funcName);
if (fun != null) {
func.Add(funcName, fun);
}
}
private void CallFunc(string funcName) {
if (func.ContainsKey(funcName)) {
Action fun = func[funcName];
fun();
}
}
private void OnApplicationQuit() {
func.Clear();
func = null;
luaEnv.Dispose();
luaEnv = null;
}
}
LuaScript.lua.txt
function awake()
print("awake")
end
function onEnable()
print("onEnable")
end
function start()
print("start")
end
function update()
print("update")
end
function onDisable()
print("onDisable")
end
function onDestroy()
print("onDestroy")
end
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!