第4章 XLua实战:环境搭建、脚本加载与交互调用

好,咱们进入XLua实战环节。

说实话,XLua是我在项目中用得最多的热更新方案。为什么?因为它轻量、灵活,而且跟Unity的集成做得相当好。我记得第一次在项目里引入XLua时,团队里还有人担心性能问题——后来跑了一轮压测,大家就都闭嘴了。

4.1 XLua环境搭建

先说说环境搭建。这一步其实很简单,但有几个坑我得提前告诉你。

4.1.1 下载与导入

去GitHub上搜XLua,找到官方仓库,下载最新的release包。解压后你会看到几个文件夹:

  • Assets:核心代码,直接拖到Unity项目里
  • Examples:示例工程,建议先跑一遍
  • Tools:一些辅助工具

我个人习惯是把整个Assets文件夹复制到项目的Assets目录下。嗯,这里要注意:XLua依赖的是C#的反射机制,所以不需要额外安装什么运行时环境。

我曾经踩过的坑:导入后如果报错说找不到某个DLL,检查一下你的Unity版本。XLua对2018.4以上的版本支持最好。太老的版本可能需要手动编译。

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加密,用加载器解密后再执行。

小技巧:加载器返回的是byte[],不是string。这样你可以做预处理,比如压缩、加密。返回null表示文件不存在,XLua会继续找下一个加载器。

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 避坑指南

最后,分享几个我实战中踩过的坑:

  1. LuaEnv生命周期:记得在场景切换或退出时Dispose。我见过有人忘了,结果内存泄漏,游戏越跑越卡。
  2. GC问题:频繁的Lua-C#交互会产生GC Alloc。用对象池或者减少跨语言调用频率来优化。
  3. 错误处理:Lua脚本报错时,C#这边会抛异常。建议用try-catch包一下,别让脚本错误搞崩整个游戏。
  4. 热更新文件:脚本文件更新后,记得清理LuaEnv的缓存。否则加载的还是旧脚本。

好了,XLua的基础实战就这些。下一章咱们聊聊更高级的话题——怎么用XLua做真正的热更新流程,包括资源管理和版本控制。到时候我会分享一个我在线上项目里用的完整方案。