
PuerTS 3.0 在 C# 中调用 LuaDelegate 桥接、Eval 传参与 MonoBehaviour 生命周期实战指南【免费下载链接】puertsPUER(普洱) Typescript. Lets write your game in UE or Unity with TypeScript.项目地址: https://gitcode.com/GitHub_Trending/pu/puerts本文以 PuerTS 3.0 的 Lua 后端BackendLua为核心系统讲解如何把 Lua 函数转换为 C# delegate、通过Eval/EvalT在 C# 与 Lua 之间双向传参和取值、捕获 Lua 侧异常以及管理ScriptEnv与 delegate 的生命周期并最终组合出在 Lua 中实现 MonoBehaviour 生命周期回调的完整方案。读完本文你将掌握一套可直接落地到 Unity 项目的C# 主逻辑 Lua 业务逻辑的协作写法并理解其底层桥接机制。背景PuerTS 3.0 的多语言后端架构PuerTSPUER TypeScript是腾讯开源的 TypeScript 编程解决方案让开发者可以在 UE 或 Unity 中使用 TypeScript 编写游戏逻辑。PuerTS 3.0 在原有 JavaScript 后端之外引入了后端可插拔的架构除了 C# 调用 Javascript 外还支持 C# 调用 Python以及本文要讲的Lua 后端。从源码上看这一架构由两层组成ScriptEnv位于 unity/upms/core/Runtime/Src/PInvoke/ScriptEnv.cs是所有语言后端共用的宿主环境封装。它持有后端实例Backend、负责创建环境引用、注册类型、执行Eval、驱动Tick、并在Dispose()时清理资源。BackendLua位于 unity/upms/lua/Runtime/Src/Backends/BackendLua.cs是 Lua 后端的实现。它把 Lua 的require暴露为模块执行入口注入require(csharp)、require(puerts)两个内置模块并重写了 Lua 全局的print使其在 Unity 下输出到UnityEngine.Debug.Log、在纯 .NET 环境下输出到System.Console.WriteLine。因此本文中所有示例都统一通过new Puerts.ScriptEnv(new Puerts.BackendLua())创建 Lua 环境。每个语言后端在从 C# 侧调用时的语法细节各有不同三语言对比速查表 给出了横向对照本文聚焦 Lua 一侧的具体写法。通过 Delegate 调用 Lua 函数PuerTS 提供的一个关键能力是将 Lua 函数转换为 C# 的 delegate。依靠这个能力你可以在 C# 侧像调用普通委托一样调用 Lua 函数。假设 C# 侧有一个带 delegate 属性的类public delegate void TestCallback(string msg); public class TestClass { public TestCallback Callback; public void TriggerCallback() { if (Callback ! null) { Callback(hello_from_csharp); } } } void Start() { var env new Puerts.ScriptEnv(new Puerts.BackendLua()); env.Eval( local CS require(csharp) -- Create a C# object local obj CS.TestClass() -- Assign a Lua function to the C# delegate property obj.Callback function(msg) info msg end -- Trigger the callback from C# side obj:TriggerCallback() ); // info is now hello_from_csharp env.Dispose(); }这段代码演示了完整的双向调用闭环Lua 侧通过require(csharp)拿到 C# 命名空间入口用CS.TestClass()构造一个 C# 对象把一个 Lua 函数赋值给 C# 对象的Callbackdelegate 属性C# 侧的TriggerCallback()被调用后就会通过该 delegate 反向执行 Lua 函数。⚠️ 注意在 Lua 中给 C# 对象的 delegate 属性赋值时使用点号语法obj.Callback function(...) end。调用实例方法时使用冒号语法obj:TriggerCallback()。冒号语法本质是obj:Method(args)等价于obj.Method(obj, args)它会自动把self作为第一个参数传入。也可以在 Lua 侧主动触发 delegate 的Invoke方法-- Directly invoke the delegate from Lua obj.Callback:Invoke(hello_from_lua)这与测试用例 unity/test/Src/Cases/Lua/CrossLang/DelegateTest.cs 中DelegateBase的做法完全一致先通过deleteobj.Callback function(msg) info msg end赋值再调用deleteobj:CSMessage()触发 C# 侧执行最后用deleteobj.Callback:Invoke(js_msg)在 Lua 侧直接唤起 delegate并用luaEnv.Evalstring(return info)校验结果。这个用例同时验证了Lua 函数赋值到 C# delegate与从 Lua 直接Invokedelegate两条路径都真实可用。从 C# 往 Lua 传参把 Lua 函数转换成 delegate 时可以将其转换成带参数的 delegate这样就可以把 C# 变量传递给 Lua。传参时类型转换的规则和把变量从 C# 返回到 Lua 是一致的。void Start() { var env new Puerts.ScriptEnv(new Puerts.BackendLua()); // Get a Lua function as a C# delegate via Eval System.Actionint LogInt env.EvalSystem.Actionint( return function(a) print(a) end ); LogInt(3); // Output: 3 env.Dispose(); }⚠️重要差异与 Javascript 不同Lua 的Eval返回值需要使用return语句显式返回。如果你忘记写returnC# 侧将得到null。这个差异在 PuerTS 中体现得非常直接JS 的Eval会取最后一个表达式的值作为隐式返回值而 Lua 遵循语言本身语义——没有return就没有返回值。需要注意的是如果你生成的 delegate 带有值类型参数需要添加UsingAction或者UsingFunc声明。具体请参见 FAQ。从代码生成器 unity/upms/core/Editor/Src/Generator/CSharpFileExporter.cs 可以看到UsingAction/UsingFunc是供 IL2CPP 等反射受限环境提前声明泛型桥接代码的机制FAQ 中给出的原因是delegate 带有值类型参数或返回值时会报can not find delegate bridge for XXX解决方案为无返回值用UsingAction声明有返回值用UsingFunc声明例如jsEnv.UsingFuncint, int()。此外官方目前仅支持最多 4 个参数的 delegate且暂不支持含ref/out修饰符的参数。从 C# 调用 Lua 并获得返回值与传参部分类似只需要将Actiondelegate 变成Funcdelegate 即可拿到 Lua 函数的返回值void Start() { var env new Puerts.ScriptEnv(new Puerts.BackendLua()); // Get a Lua function that returns a value System.Funcint, int Add3 env.EvalSystem.Funcint, int( return function(a) return 3 a end ); System.Console.WriteLine(Add3(1)); // Output: 4 env.Dispose(); }如果你只需要一个简单的值也可以直接用EvalT求值并取回结果void Start() { var env new Puerts.ScriptEnv(new Puerts.BackendLua()); // Directly evaluate and get the return value int result env.Evalint(return 1 2); System.Console.WriteLine(result); // Output: 3 string str env.Evalstring(return hello lua); System.Console.WriteLine(str); // Output: hello lua env.Dispose(); }⚠️ 再次提醒Lua 中必须使用return语句来返回值这是与 Javascript 最大的区别之一。在 JS 中表达式的最后一个值会被自动返回而 Lua 中不写return则不会有返回值。从ScriptEnv的实现看unity/upms/core/Runtime/Src/PInvoke/ScriptEnv.cs 中的EvalTResult会把代码块按 UTF-8 编码后交给原生层pesapi_eval执行随后检查是否捕获到异常没有异常时再通过ExpressionsWrap.GetNativeTranlatorTResult()把返回值翻译成目标 C# 类型。这也是Evalint、Evalstring、EvalSystem.Funcint,int等重载能工作的底层原因——返回值与参数的类型转换走的是同一套原生翻译管线。需要注意如果你生成的 delegate 带有值类型参数同样需要添加UsingAction或者UsingFunc声明具体请参见 FAQ。Lua 中的错误处理当 Lua 代码中使用error()抛出异常时C# 侧可以通过try-catch捕获。这依赖EvalTResult实现中对pesapi_has_caught的检查一旦执行过程中抛出了 Lua 错误原生层会把它转换成 C# 的InvalidOperationException携带异常信息字符串从而让 C# 侧能像处理普通异常一样处理 Lua 侧的error()。void Start() { var env new Puerts.ScriptEnv(new Puerts.BackendLua()); // Lua error will be caught as a C# exception try { env.Eval(error(something went wrong)); } catch (Exception e) { Debug.Log(e.Message); // Contains: something went wrong } // Errors in Lua functions converted to delegates are also catchable try { var foo env.EvalAction( return function() error(error in function) end ); foo(); // This will throw } catch (Exception e) { Debug.Log(e.Message); // Contains: error in function } env.Dispose(); }这里覆盖了两种典型的出错场景顶层 Eval 抛错env.Eval(error(something went wrong))直接抛 Lua 错误被 C#catch捕获e.Message中包含something went wrongdelegate 调用时抛错从 Lua 转换来的Action在被foo()调用的瞬间抛出 Lua 内部错误同样能被捕获e.Message中包含error in function。在测试用例 unity/test/Src/Cases/Lua/ExceptionTest.cs 中也有对异常传播路径的专门验证可用于确认try-catch捕获行为。环境销毁与 Delegate 生命周期当 Lua 环境ScriptEnv被Dispose()后之前转换的 delegate 将不再可用。调用已销毁环境的 delegate 会抛出异常请务必注意管理好生命周期。void Start() { var env new Puerts.ScriptEnv(new Puerts.BackendLua()); System.Actionstring luaFunc env.EvalSystem.Actionstring( return function(msg) print(msg) end ); luaFunc(before dispose); // OK env.Dispose(); // ❌ This will throw an exception! // luaFunc(after dispose); }从ScriptEnv.Dispose(bool)的实现unity/upms/core/Runtime/Src/PInvoke/ScriptEnv.cs可以看到销毁流程会依次通知后端OnExit、关闭远程调试器、清空回调、GC.Collect()并等待终结器、清理待回收的脚本对象、最后销毁环境引用并将自身从scriptEnvs列表移除。此后 delegate 所引用的脚本函数已经不存在于一个有效的环境之中因此调用即抛异常。CheckLiveness方法也印证了这一点——它会在环境被销毁后抛出InvalidOperationException(JsEnv has been disposed!)。实践建议对于长期存在的 Lua 环境应尽量采用单例 随场景/进程生命周期管理的方式如下一节LuaBehaviour中static ScriptEnv的用法如果频繁创建销毁环境一定要在销毁前确保没有 delegate 或ScriptObject仍被 C# 侧持有引用。在 Lua 中实现 MonoBehaviour综合上面所有能力我们可以在 Lua 里实现 MonoBehaviour 的生命周期回调。这也是 PuerTS 里最经典的实战模式C# 只保留一个薄薄的 MonoBehaviour 壳把Start/Update/OnDestroy等回调全部委托给 Lua 函数业务逻辑的迭代完全发生在 Lua 侧。using System; using Puerts; using UnityEngine; public class LuaBehaviour : MonoBehaviour { public Action LuaStart; public Action LuaUpdate; public Action LuaOnDestroy; static ScriptEnv luaEnv; void Awake() { if (luaEnv null) luaEnv new ScriptEnv(new BackendLua()); var init luaEnv.EvalActionMonoBehaviour( return function(bindTo) -- Bind Lua functions to C# delegate properties bindTo.LuaUpdate function() print(update...) end bindTo.LuaOnDestroy function() print(onDestroy...) end end ); if (init ! null) init(this); } void Start() { if (LuaStart ! null) LuaStart(); } void Update() { if (LuaUpdate ! null) LuaUpdate(); } void OnDestroy() { if (LuaOnDestroy ! null) LuaOnDestroy(); LuaStart null; LuaUpdate null; LuaOnDestroy null; } }值得注意的工程细节环境复用Awake中通过static ScriptEnv luaEnv保证整个场景乃至进程只创建一个 Lua 环境避免每个物体各自建环境导致的内存与初始化开销null 守卫init可能为null例如 Lua 侧没有显式return函数因此调用前先判空——这正对应前面强调的Lua 必须用return返回函数的约束回调解绑OnDestroy中把三个 delegate 全部置null切断 C# 侧对 Lua 函数的引用配合环境统一销毁策略避免悬挂引用。⚠️ 注意 Lua 与 JS 的关键差异Lua 的Eval必须使用return返回函数Lua 中赋值 delegate 属性使用点号语法bindTo.LuaUpdate function() ... endLua 中调用 C# 实例方法使用冒号语法bindTo:SomeMethod()。同时PuerTS 的 Lua 后端在BackendLua.OnEnterunity/upms/lua/Runtime/Src/Backends/BackendLua.cs中做了不少开箱即用的初始化把require(csharp)、require(puerts)挂到 Lua 的package.searchers上用loadType机制实现按名字反射加载任意 C# 类型并重写print对接 Unity 日志。这意味着上例中CS.TestClass()、print(update...)等写法无需任何额外配置即可工作。Lua 与 Javascript 在 C# 调用方面的主要差异由于 PuerTS 同时支持多语言后端同一套 C# 代码在与不同语言交互时语法和语义差异是新手最容易踩坑的地方。下表总结了 Lua 与 JavaScript 在 C# 调用维度上的核心差异特性JavascriptLuaEval 返回值表达式最后一个值自动返回必须使用return显式返回函数语法(a) { ... }或function(a) { ... }function(a) ... enddelegate 赋值obj.Callback (msg) { ... }obj.Callback function(msg) ... end方法调用统一使用点号obj.Method()实例方法使用冒号obj:Method()输出到控制台console.log()print()空值null/undefinednil其中最容易引起 bug 的两点是return语义JS 的Eval会自动返回最后一个表达式的值而 Lua 必须显式写return否则 C# 侧拿到的是null——所有EvalAction、EvalFunc...、Evalint场景都受此影响点号 vs 冒号Lua 调用 C# 实例方法必须用冒号自动传入self而 JS 统一用点号。在 delegate 赋值上则反过来——赋值用点号一旦写成obj:Callback function() end就会得到报错。如果要在同一项目中混用多种语言后端建议将跨语言调用封装在统一的门面层Facade把语言差异收敛到一处同时参考 三语言对比速查表 做团队规范约定。进阶阅读C# 调用 JavascriptPuerTS 3.0 默认的 JavaScript 后端调用指南C# 调用 PythonPython 后端的对应教程三语言对比速查表JavaScript / Lua / Python 三语言横向对比FAQUsingAction/UsingFunc声明、delegate 桥接报错等常见问题的官方解答测试用例DelegateTest.cs、ExceptionTest.cs、EvalTest.cs 提供了 Lua 后端的可运行验证样例底层实现ScriptEnv.cs环境与Eval实现、BackendLua.csLua 后端初始化与桥接。【免费下载链接】puertsPUER(普洱) Typescript. Lets write your game in UE or Unity with TypeScript.项目地址: https://gitcode.com/GitHub_Trending/pu/puerts创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考