如何在Lua中调用Unity Job System
解读
国内项目普遍用 xLua/toLua/SLua 把热更逻辑放在Lua层,而Unity Job System(含Entities 0.5/1.0)属于 C#高性能线程栈,两者内存模型、线程模型完全不同。面试官真正想问的是:
- 你是否知道 Lua虚拟机(主线程) 与 Unity Job线程池 的隔离性;
- 能否给出 零GC、零反射、线程安全 的可落地方案;
- 是否理解 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 都会直接崩溃。
- 国内主流封装套路:
- C#层声明
[StructLayout(LayoutKind.Sequential)]的 Plain Old Data 结构体; - NativeArray<T> 或 NativeHashMap 做数据容器,Unity.Collections.Allocator.Persistent 分配;
- Lua层只把 float/int/bool 数组通过 xlua.pushstruct 压到C#,C#拷贝到NativeArray后 Schedule+Complete;
- 结果通过 主线程Complete后回调到Lua,避免 *跨线程访问lua_State。
- C#层声明
- 性能细节:
- NativeArray 在Editor下会额外做安全检查,真机 #UNITY_DOTS_DEBUG 关闭后无开销;
- IJobParallelForBatch 比 IJobParallelFor 更适合Lua一次性提交大批量数据(国内MMO常见3000+怪)。
- 热更限制:Job代码必须放在 Assembly-CSharp 或 Assembly-CSharp-firstpass,不能下放到热更DLL,因此 Job算法固定、参数可配 是国内共识。
答案
分四步落地,代码可直接写进简历项目经验。
- 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");
}
}
- 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
-
生命周期管理
MonoBehaviour.OnDestroy 里调用s_Buf.Dispose(),防止 NativeLeak 在iOS审核时被拒。 -
真机验证
用 Unity Profiler 查看 “Job.Worker” 线程利用率 >90%,主线程 ScriptBehaviourUpdate 耗时 <2 ms,即可在面试时给出 “3000颗子弹+60 FPS” 量化战绩。
拓展思考
-
Lua能不能用Entities 1.0?
目前 Entities 1.0 的 EntityManager 在主线程,SystemBase 跑在 World.Update(),Lua只能 C#层写好System,通过 EntityQuery 暴露 RO/RW Component 的 NativeArray 句柄给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破坏;面试时提到这一点,能体现 团队级架构思维。