第4章 XLua实战:环境搭建、脚本加载与交互调用
好,咱们进入XLua实战环节。
说实话,XLua是我在项目中用得最多的热更新方案。为什么?因为它轻量、灵活,而且跟Unity的集成做得相当好。我记得第一次在项目里引入XLua时,团队里还有人担心性能问题——后来跑了一轮压测,大家就都闭嘴了。
4.1 XLua环境搭建
先说说环境搭建。这一步其实很简单,但有几个坑我得提前告诉你。
4.1.1 下载与导入
去GitHub上搜XLua,找到官方仓库,下载最新的release包。解压后你会看到几个文件夹:
- Assets:核心代码,直接拖到Unity项目里
- Examples:示例工程,建议先跑一遍
- Tools:一些辅助工具
我个人习惯是把整个Assets文件夹复制到项目的Assets目录下。嗯,这里要注意:XLua依赖的是C#的反射机制,所以不需要额外安装什么运行时环境。
4.1.2 配置与验证
导入完成后,怎么验证环境搭好了?很简单,写个测试脚本:
using XLua;
using UnityEngine;
public class XLuaTest : MonoBehaviour
{
private LuaEnv luaEnv;
void Start()
{
luaEnv = new LuaEnv();
luaEnv.DoString("print('Hello XLua!')");
}
void OnDestroy()
{
luaEnv.Dispose();
}
}
挂到任意GameObject上,运行。控制台输出"Hello XLua!",说明环境OK。
这里有个细节:LuaEnv是XLua的核心对象,一个进程里通常只创建一个。我见过有人每加载一个脚本就new一个LuaEnv,结果内存直接爆了。别这么干。
4.2 加载Lua脚本
环境搭好了,接下来就是怎么加载脚本。XLua提供了几种方式,我挨个说说。
4.2.1 字符串加载
最直接的方式,用DoString:
luaEnv.DoString("local a = 1 + 2; print(a)");
这种方式适合测试,但生产环境不推荐。为什么?字符串写死在代码里,改起来麻烦,而且没法做热更新。
4.2.2 文件加载
实际项目中,我们通常把Lua脚本放在Resources或StreamingAssets目录下:
TextAsset luaScript = Resources.Load<TextAsset>("MyScript");
luaEnv.DoString(luaScript.text);
或者用自定义的加载器:
luaEnv.AddLoader((ref string filepath) =>
{
string path = Application.dataPath + "/Lua/" + filepath + ".lua";
return File.ReadAllBytes(path);
});
我个人偏爱自定义加载器。它让你能控制脚本从哪里加载——本地、网络、甚至加密后的资源。我在一个项目里就遇到过,脚本明文放在包里被玩家扒出来改数据,后来加了AES加密,用加载器解密后再执行。
4.3 C#调用Lua函数
这是热更新的核心能力。C#怎么调用Lua里定义的函数?XLua提供了几种映射方式。
4.3.1 通过LuaFunction调用
LuaFunction luaFunc = luaEnv.Global.Get<LuaFunction>("myFunc");
luaFunc.Call(1, 2);
这种方式简单,但性能一般。每次Call都有装箱拆箱的开销。如果你在Update里频繁调用,建议用下面的方式。
4.3.2 通过委托调用
// 先定义委托
[XLua.CSharpCallLua]
public delegate int MyDelegate(int a, int b);
// 获取
MyDelegate myFunc = luaEnv.Global.Get<MyDelegate>("myFunc");
int result = myFunc(1, 2);
这种方式性能好很多,因为XLua会生成胶水代码,直接调用。记得加上[CSharpCallLua]特性,否则XLua不知道要生成绑定代码。
嗯,这里有个坑:如果你改了Lua函数的签名,记得重新生成胶水代码。我有一回忘了,结果传参对不上,查了半天才发现是缓存的问题。
4.3.3 通过类映射调用
如果Lua里定义了一个模块,可以用类来映射:
// Lua代码
myModule = {
add = function(a, b) return a + b end,
sub = function(a, b) return a - b end
}
// C#代码
public class MyModule
{
public int add(int a, int b) { return 0; } // 占位
public int sub(int a, int b) { return 0; }
}
MyModule module = luaEnv.Global.Get<MyModule>("myModule");
int sum = module.add(3, 4);
这种方式适合管理一组相关函数。不过要注意,类里的方法只是占位,实际执行的是Lua里的逻辑。
4.4 Lua调用C#方法
反过来,Lua里怎么调用C#?这也是热更新的关键——你可以在Lua脚本里复用C#的代码,比如Unity的API。
4.4.1 直接调用静态方法
-- Lua代码
local go = CS.UnityEngine.GameObject.Find("Player")
CS.UnityEngine.Debug.Log("找到玩家对象")
XLua默认把C#的命名空间映射到CS表下。所以CS.UnityEngine.GameObject就是C#里的UnityEngine.GameObject。
4.4.2 调用实例方法
local go = CS.UnityEngine.GameObject("MyObj")
local transform = go.transform
transform:SetPosition(0, 1, 0)
注意这里用的是冒号:,不是点.。Lua里冒号调用会自动把调用者作为第一个参数传进去。说白了,transform:SetPosition(0,1,0)等价于transform.SetPosition(transform, 0,1,0)。
4.4.3 注册C#回调给Lua
这个场景很常见:Lua里需要监听C#的事件。比如:
// C#代码
public class GameEvent
{
public static event System.Action OnGameStart;
}
// 注册到Lua
luaEnv.Global.Set("onGameStart", (System.Action)OnGameStart);
-- Lua代码
onGameStart(function()
print("游戏开始了!")
end)
这样Lua就能响应C#的事件了。我在项目里用这个模式做UI逻辑——C#负责事件分发,Lua负责具体表现,分工明确。
- C#调用Lua:用委托性能最好,记得加[CSharpCallLua]
- Lua调用C#:用CS前缀,实例方法用冒号调用
- 事件回调:通过闭包或委托传递,注意生命周期管理
4.5 避坑指南
最后,分享几个我实战中踩过的坑:
- LuaEnv生命周期:记得在场景切换或退出时Dispose。我见过有人忘了,结果内存泄漏,游戏越跑越卡。
- GC问题:频繁的Lua-C#交互会产生GC Alloc。用对象池或者减少跨语言调用频率来优化。
- 错误处理:Lua脚本报错时,C#这边会抛异常。建议用
try-catch包一下,别让脚本错误搞崩整个游戏。 - 热更新文件:脚本文件更新后,记得清理LuaEnv的缓存。否则加载的还是旧脚本。
好了,XLua的基础实战就这些。下一章咱们聊聊更高级的话题——怎么用XLua做真正的热更新流程,包括资源管理和版本控制。到时候我会分享一个我在线上项目里用的完整方案。