Puerts Unity VSCode 断点调试完整指南:JsEnv 调试端口、等待调试器与 launch.json 配置
【免费下载链接】puertsPUER(普洱) Typescript. Let's write your game in UE or Unity with TypeScript.项目地址: https://gitcode.com/GitHub_Trending/pu/puerts
本文基于当前仓库 doc/unity/zhcn/knowjs/debugging.md(英文版见 doc/unity/en/knowjs/debugging.md)整理而成,并结合 JsEnv.cs 等源码对底层机制做了进一步解读。读者将掌握:如何在 Unity 中通过
JsEnv构造参数开启调试端口、用“等待调试器连接”能力让最早期脚本也能命中断点、以及如何在 VSCode 中配置自动附加或手写launch.json完成断点、单步、变量查看等操作。
Puerts 在 Unity 中的脚本以 V8 为 JS 引擎,因此天然复用 V8 Inspector 协议:调试器通过 WebSocket 连接到一个本地 TCP 端口,即可像调试 Node.js 一样调试运行在游戏内的 TypeScript/JavaScript 代码。本指南介绍官方推荐的 VSCode 调试链路,从 C# 侧开启端口开始,一直讲到 VSCode 侧的附加配置与 Unity 侧的配套设置。如果目标平台是手机等移动设备,端口转发与真机调试方式可参考开发博客(原文档建议阅读对应开发 blog,仓库内不包含该内容,此处不再展开)。
一、调试前置:为 JsEnv 开启调试端口并驱动 Tick
VSCode 调试的第一步,是在创建JsEnv时传入调试端口。端口号会通过ScriptEnv一路传递到后端,最终由 V8 后端启动一个本地 Inspector 服务(见下文“底层原理”一节)。
以Start为例,最简单的开启方式如下:
// 8080 是连接的端口,和 vscode 工程目录下的 .vscode\launch.json 保持一致 void Start() { jsEnv = new JsEnv(new TSLoader(), 8080); // 推荐使用 TSLoader,就不需要你手动指定 JS 输出目录 jsEnv = new JsEnv(new DefaultLoader("F:/puerts/unity/TsProj/output/"), 8080); // 使用 DefaultLoader 时需要手动指定你的 JS 输出目录 } void Update() { jsEnv.Tick(); }两个关键点:
- 端口必须与 VSCode 的
launch.json一致。8080只是示例,换成任意空闲端口均可,但两侧必须保持相同。 Tick()不可省略。JsEnv.Tick()在Update中每帧被调用,它负责驱动调试器的消息循环。从源码看,JsEnv.cs 的Tick()转发到ScriptEnv.Tick(),而后者在debugPort != -1时会调用backend.DebuggerTick()处理 Inspector 收发(见 ScriptEnv.cs)。换言之,端口开启后若不做Tick,调试器将无法正常工作。
关于 Loader 的选择,原文档给出的建议同样适用:
- TSLoader:推荐使用,自动处理 TS 编译与输出目录,无需手工指定 JS 输出位置;
- DefaultLoader:需要手动传入 JS 输出目录(如
F:/puerts/unity/TsProj/output/),适合你已经自行完成编译、仅需加载产物的场景。
提示:
new JsEnv()的第二个参数默认值为-1(见 JsEnv.cs),此时不开启调试端口;只有传入有效端口号才会启动调试服务。
二、等待调试器连接:让早期脚本也能断点
连接耗时与断点盲区
调试器通过 WebSocket 与 V8 建立连接,期间包含TCP 握手、WebSocket 握手,以及建立连接后调试器与 V8 之间交换协议信息,整个过程大约几百毫秒。
在这几百毫秒内执行的脚本无法被断点命中——因为调试协议尚未就绪。如果你的模块入口(如QuickStart.mjs)在启动瞬间就会执行大量代码,而这些代码恰好落在“盲区”里,断点就会失效。解决方案就是 Puerts 提供的“等待调试器连接”功能:让JsEnv阻塞等待,直到 V8 Inspector 与调试器完成握手后再执行业务脚本。
选择依据(原文档说明):C# 版本高于 7.2(支持 async)时推荐异步等待,否则使用同步阻塞等待。
异步等待(推荐,C# 7.2+)
async void RunScript() { jsEnv = new JsEnv(new DefaultLoader("E:/puerts_unity_demo/TsProj/output/"), 8080); await jsEnv.WaitDebuggerAsync(); jsEnv.ExecuteModule("QuickStart.mjs"); } void Start() { RunScript(); } void Update() { jsEnv.Tick(); }同步阻塞等待
void Start() { jsEnv = new JsEnv(new DefaultLoader("E:/puerts_unity_demo/TsProj/output/"), 8080); jsEnv.WaitDebugger(); jsEnv.ExecuteModule("QuickStart.mjs"); } void Update() { jsEnv.Tick(); }底层实现:Task 驱动与轮询
两个 API 在 JsEnv.cs 中都有封装,具体逻辑位于 ScriptEnv.cs:
WaitDebugger():同步版本,内部while (!backend.DebuggerTick()) { }空转轮询,直到 V8 Inspector 检测到调试器已连接才返回;WaitDebuggerAsync():异步版本,构造一个TaskCompletionSource<bool>返回Task,后续Tick()中一旦DebuggerTick()为真,就通过waitDebugerTaskSource.SetResult(true)唤醒等待方——因此异步等待同样依赖每帧调用Tick()。
注意:
WaitDebuggerAsync()在debugPort == -1时会直接返回null(见 ScriptEnv.cs),所以只有开启调试端口的JsEnv才适用这两个等待 API。
仓库自带的 Unity 测试工程也演示了这一用法:HelloWorlder.cs 中创建JsEnv(new DefaultLoader(), 8080)后立即调用env.WaitDebugger(),可作为最小可运行参考。
调试器连接流程的源码印证
从实现看,调试链路是这样的(Unity V8 后端与 Unreal 共享同一份 Inspector 实现,即 V8InspectorImpl.cpp):
- C# 侧
new JsEnv(loader, 8080)最终触发BackendV8.OpenRemoteDebugger(8080)→ 原生CreateInspector(BackendV8.cs); - 原生层使用
websocketpp::server<config::asio>在指定端口上listen、start_accept,并注册 HTTP/Open/Message/Close/Fail 事件处理器(V8InspectorImpl.cpp); - V8 Inspector 通过
v8_inspector::V8Inspector::create创建并注册当前上下文(V8InspectorImpl.cpp); - 该服务还实现了
GET /json列表接口,返回webSocketDebuggerUrl等信息(V8InspectorImpl.cpp),这正是调试器用于发现与连接目标的信息来源。
这也解释了“几百毫秒”的构成:TCP 握手 + WebSocket 握手 + 协议信息交换,全部发生在连接建立阶段。
三、VSCode 端配置:自动附加或手写 launch.json
连接方式二选一:
方式 A:开启 Auto Attach(简单快捷)
在 VSCode 中打开设置(Ctrl+,),搜索auto attach,将Debug > Node: Auto Attach设置为on。此后 VSCode 会自动发现并附加到上述端口上的调试会话。
原文档特别说明:高版本 VSCode 可能没有该选项,此时可以跳过此项设置,直接用手写
launch.json的方式。
方式 B:手动创建 launch.json(推荐,更可控)
在 VSCode 工程目录下创建(或打开).vscode/launch.json,新增一个Node.js Attach类型的调试配置,并把port改为你在JsEnv构造函数里传入的端口号。要点:
- 调试类型选择node.js attach(Node.js 附加模式),而不是 launch/启动模式;
port必须与new JsEnv(loader, 8080)的端口完全一致;- 配置完成后,在调试面板启动该 Attach 会话,VSCode 即开始尝试连接 Unity 内运行的 V8 Inspector。
launch.json的最小示意如下(端口以你实际使用的为准):
{ "version": "0.2.0", "configurations": [ { "type": "node", "request": "attach", "name": "Attach to Puerts", "port": 8080, "restart": true, "localRoot": "${workspaceFolder}", "remoteRoot": "${workspaceFolder}" } ] }原文档同时给出了“选择 node.js attach”的界面示意图(见英文版 doc/unity/en/knowjs/debugging.md,仓库内该图为外部托管图片)。实际操作中,只要能建立对
127.0.0.1:8080的 WebSocket 附加,即可获得断点、单步、调用栈、变量监视等能力。
断点盲区的完整规避方案
把等待调试器与启动顺序组合起来,就是最稳妥的启动模板:先await WaitDebuggerAsync()(或同步WaitDebugger())确保 Inspector 已就绪,再ExecuteModule("QuickStart.mjs")执行入口模块,从而保证入口代码的所有断点都能命中。
四、Unity 侧配套设置:勾选 Run In Background
最后一步是让游戏在失焦(后台)时继续运行,否则一旦切到 VSCode 调试,Unity 主循环暂停、Tick()不再执行,调试自然中断。
操作路径:打开Project Settings / Player页面,把Run In Background勾选上。
图中高亮的正是该选项:它位于 Player 设置的 Resolution and Presentation 区域,勾选后游戏窗口失去焦点时仍保持运行,Update/Tick持续被调用,调试器消息循环才不会停摆。
五、常见问题与调试实践建议
结合原文档要点与源码行为,整理如下实战清单:
- 断点不生效,先查端口:确认
launch.json的port与new JsEnv(loader, port)完全一致; - 早期代码断不到:启动瞬间执行的代码落在“握手盲区”,改用
WaitDebuggerAsync()/WaitDebugger()等调试器连接后再执行入口模块; Tick()必须持续调用:调试器的消息收发依赖Update()中的jsEnv.Tick(),任何一帧的阻塞都会让调试响应变慢甚至超时;- 务必勾选 Run In Background:否则切换到 VSCode 时 Unity 进入后台,主循环被暂停;
- Loader 差异:
TSLoader免去手动指定输出目录,DefaultLoader需要传入 JS 产物目录,且两者均可搭配调试端口使用; - 仅 V8 后端开启调试:从 Backend.cs 的虚方法可以看出,
OpenRemoteDebugger/DebuggerTick是后端级能力,V8 与 NodeJS 后端均有对应实现(BackendV8.cs、BackendNodeJS.cs),使用这些后端才能获得完整的断点调试支持。
六、相关阅读
- 模块加载与入口执行:
ExecuteModule的用法见 JsEnv.cs,调试时配合等待 API 使用可确保入口代码可断点; - TypeScript 调试辅助:TypeScript 指引;
- 模块系统:见 模块文档,理解
ExecuteModule("QuickStart.mjs")的模块解析规则; - 调试链路底层的 Inspector 实现:见 V8InspectorImpl.cpp,可深入阅读 WebSocket 服务与协议分发细节。
【免费下载链接】puertsPUER(普洱) Typescript. Let's write your game in UE or Unity with TypeScript.项目地址: https://gitcode.com/GitHub_Trending/pu/puerts
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考