如何在Lua中调用Unity Job System

解读

国内项目普遍用 xLua/toLua/SLua 把热更逻辑放在Lua层,而Unity Job System(含Entities 0.5/1.0)属于 C#高性能线程栈,两者内存模型、线程模型完全不同。面试官真正想问的是:

  1. 你是否知道 Lua虚拟机(主线程)Unity Job线程池 的隔离性;
  2. 能否给出 零GC、零反射、线程安全 的可落地方案;
  3. 是否理解 Hybrid+ECS 在国内商业项目中的真实分工(C#写Job,Lua写流程)。
    一句话:Lua不能直接new Job、不能写lambda Job,只能通过“C#桥梁”把数据喂给Job,再把结果回调给Lua。

知识点

  • Unity Job System三规则:值类型、无托管引用、线程安全。
  • Lua与C#交互成本:xlua的 CSharpCallLua/LuaCallCSharp 生成代码、 ObjectTranslator 装箱、 enum/struct 数组 的pin/copy。
  • 线程安全红线:LuaState 只能在主线程触摸,Job线程里任何 lua_gettop/lua_pushxxx 都会直接崩溃。
  • 国内主流封装套路
    1. C#层声明 [StructLayout(LayoutKind.Sequential)]Plain Old Data 结构体;
    2. NativeArray<T>NativeHashMap 做数据容器,Unity.Collections.Allocator.Persistent 分配;
    3. Lua层只把 float/int/bool 数组通过 xlua.pushstruct 压到C#,C#拷贝到NativeArray后 Schedule+Complete
    4. 结果通过 主线程Complete后回调到Lua,避免 *跨线程访问lua_State
  • 性能细节
    • NativeArray 在Editor下会额外做安全检查,真机 #UNITY_DOTS_DEBUG 关闭后无开销;
    • IJobParallelForBatchIJobParallelFor 更适合Lua一次性提交大批量数据(国内MMO常见3000+怪)。
  • 热更限制:Job代码必须放在 Assembly-CSharpAssembly-CSharp-firstpass,不能下放到热更DLL,因此 Job算法固定、参数可配 是国内共识。

答案

分四步落地,代码可直接写进简历项目经验。

  1. C#层定义数据结构(保证blittable,无托管字段)
// Assets/Scripts/Jobs/LuaBridge.cs
[StructLayout(LayoutKind.Sequential)]
public struct BulletData
{
    public float3 pos;
    public float3 vel;
    public int id;
}

[BurstCompile]
public struct BulletMoveJob : IJobParallelFor
{
    [NativeDisableParallelForRestriction] public NativeArray<BulletData> bullets;
    public float dt;
    public void Execute(int i)
    {
        var b = bullets[i];
        b.pos += b.vel * dt;
        bullets[i] = b;
    }
}

public static class LuaJobBridge
{
    // 池子复用,避免每次new
    static NativeArray<BulletData> s_Buf;
    static bool s_Inited = false;

    [MonoPInvokeCallback(typeof(Func<IntPtr, int, bool>))]
    public static bool UpdateBullets(IntPtr ptr, int count)
    {
        if (!s_Inited) { s_Buf = new NativeArray<BulletData>(4096, Allocator.Persistent); s_Inited = true; }

        // 把Lua数组拷贝到NativeArray
        unsafe
        {
            var size = sizeof(BulletData) * count;
            UnsafeUtility.MemCpy(s_Buf.GetUnsafePtr(), (void*)ptr, size);
        }

        new BulletMoveJob { bullets = s_Buf, dt = Time.deltaTime }
            .Schedule(count, 64).Complete();

        // 结果写回同一内存,Lua端直接复用
        return true;
    }

    [LuaCallCSharp]
    public static void Register()
    {
        LuaDLL.lua_pushstdcallcfunction(LuaEnv.L, UpdateBullets);
        LuaDLL.lua_setglobal(LuaEnv.L, "cs_UpdateBullets");
    }
}
  1. Lua层只负责组装数据(无gc)
local bulletBuf = CS.UnityEngine.Vector3Array(4096 * 2) -- 预分配
local function tick()
    local n = 0
    for _, b in ipairs(bullets) do
        bulletBuf[n]   = b.pos.x
        bulletBuf[n+1] = b.pos.y
        bulletBuf[n+2] = b.pos.z
        bulletBuf[n+3] = b.vel.x
        bulletBuf[n+4] = b.vel.y
        bulletBuf[n+5] = b.vel.z
        n = n + 6
    end
    -- 直接走unsafe memory,无boxing
    cs_UpdateBullets(bulletBuf, n/6)
    -- 此时bulletBuf里已经是Job计算后的新坐标,直接读回表现层
end
  1. 生命周期管理
    MonoBehaviour.OnDestroy 里调用 s_Buf.Dispose(),防止 NativeLeak 在iOS审核时被拒。

  2. 真机验证
    Unity Profiler 查看 “Job.Worker” 线程利用率 >90%,主线程 ScriptBehaviourUpdate 耗时 <2 ms,即可在面试时给出 “3000颗子弹+60 FPS” 量化战绩。

拓展思考

  • Lua能不能用Entities 1.0?
    目前 Entities 1.0EntityManager 在主线程,SystemBase 跑在 World.Update(),Lua只能 C#层写好System,通过 EntityQuery 暴露 RO/RW ComponentNativeArray 句柄给Lua,逻辑与Job一致,但 System代码不能热更,所以国内项目把 System当引擎模块 看待,Lua只调 参数配置+事件触发

  • Burst编译后Lua还能调吗?
    Burst生成的是 native code,只要 入口函数 加了 [BurstCompile][MonoPInvokeCallback],Lua就可以 delegate ptr 方式调用,但 Burst函数内部不能有任何lua_API,否则直接编译失败。

  • 多人协作规范
    主程层面会出一份 《Job白名单》,列出允许Lua调用的 struct+Job签名,并写 T4模板 自动生成 xlua的gc优化导出,防止策划乱加字段导致 blittable破坏;面试时提到这一点,能体现 团队级架构思维