调试
本指南介绍 Estella 的运行时调试工具,包括错误处理、播放控制、日志系统和编辑器调试面板。
错误处理
系统错误处理
当 ECS 系统抛出异常时,引擎默认会在控制台打印错误并继续运行。通过 app.onSystemError() 注册处理器可以控制出错后的行为——返回 'continue' 继续执行后续系统,返回 'pause' 暂停当前帧剩余的所有系统:
app.onSystemError((error: Error, systemName?: string) => { console.error(`System "${systemName}" failed:`, error.message);
if (error.message.includes('critical')) { return 'pause'; } return 'continue';});返回 'pause' 后,当前帧中尚未执行的系统会被跳过,但渲染循环不会停止。下一帧开始时所有系统恢复正常执行。
WASM 错误处理
C++ 引擎核心通过 WebAssembly 运行。当 WASM 层发生错误时,通过 app.onWasmError() 捕获:
app.onWasmError((error: unknown, context: string) => { console.error(`WASM error in ${context}:`, error);});context 参数标识出错的 WASM 操作(如渲染、物理计算等),便于定位问题来源。
播放控制
SDK 提供了一组方法用于在运行时控制游戏的执行节奏,适用于逐帧调试和慢动作分析。
暂停与恢复
app.setPaused(true); // 暂停:系统停止执行,仅运行 Last schedule(渲染)app.setPaused(false); // 恢复正常执行
app.isPaused(); // 查询当前暂停状态暂停状态下引擎仍然运行渲染循环(Schedule.Last),画面会保持在最后一帧的状态。
单帧步进
暂停状态下调用 stepFrame() 可以推进一帧,执行完整的系统调度后再次暂停:
app.setPaused(true);app.stepFrame(); // 执行一帧完整的系统调度播放速度
通过 setPlaySpeed() 调整时间流速,有效范围为 0.1 到 4.0:
app.setPlaySpeed(0.25); // 四分之一速度,适合观察快速动画app.setPlaySpeed(2.0); // 双倍速度
app.getPlaySpeed(); // 获取当前速度倍率播放速度通过缩放每帧的 delta 时间实现。所有读取 Time.delta 的系统会自动受到影响。
日志系统
SDK 提供集中式日志系统,支持分级过滤和自定义处理。
LogLevel
import { LogLevel, setLogLevel } from 'esengine';
// Debug = 0, Info = 1, Warn = 2, Error = 3setLogLevel(LogLevel.Debug); // 输出所有级别setLogLevel(LogLevel.Warn); // 仅输出 Warn 和 Error使用日志
日志函数接受分类标签、消息和可选的附加数据:
import { debug, info, warn, error } from 'esengine';
debug('Physics', 'Collision detected', { entityA: 1, entityB: 2 });info('Scene', 'Level loaded');warn('Asset', 'Texture not found, using fallback');error('Render', 'WebGL context lost');默认的 ConsoleLogHandler 会格式化输出到浏览器控制台:
[14:32:05.123] [INFO ] [Scene] Level loaded自定义 LogHandler
实现 LogHandler 接口可以将日志发送到自定义目标(如远程服务、文件、UI 面板):
import { getLogger, type LogHandler, type LogEntry } from 'esengine';
const customHandler: LogHandler = { handle(entry: LogEntry) { // entry.timestamp, entry.level, entry.category, entry.message, entry.data myRemoteService.send(entry); }};
const logger = getLogger();logger.addHandler(customHandler);
// 移除处理器logger.removeHandler(customHandler);编辑器调试
Output 面板
编辑器的 Output 面板捕获运行时的所有控制台输出:
- 时间戳 — 每条日志附带精确到毫秒的时间
- 类型过滤 — 按 info、warn、error 分类筛选
- 搜索 — 在日志文本中搜索关键字
Game View 播放控制
编辑器的 Game View 面板提供了图形化的播放控制工具栏:
- Play / Pause — 启动或暂停游戏预览
- Step — 暂停状态下单帧步进
- Speed — 调整播放速度
这些控件直接调用上述 setPaused()、stepFrame() 和 setPlaySpeed() API。
帧调试器
帧调试器可以捕获单帧并检查每个 draw call——适用于诊断批处理问题、异常的 flush 原因和渲染顺序问题。
编辑器面板
在编辑器中打开 Frame Debugger 面板(支持浮动窗口)。点击 Capture 冻结当前帧。面板会显示每个 draw call 的类型、纹理、材质、三角形数量、flush 原因和 stencil/scissor 状态。点击某个 draw call 可以回放渲染到该步骤,查看中间结果。
SDK API
也可以通过代码捕获和分析帧数据:
import { Renderer, FlushReason, RenderType } from 'esengine';
// 触发捕获(下一帧生效)Renderer.captureNextFrame();
// 帧渲染完成后获取捕获数据const capture = Renderer.getCapturedData();if (capture) { for (const dc of capture.drawCalls) { console.log( `#${dc.index} ${RenderType[dc.type]} | ` + `${dc.triangleCount} tris | ` + `flush: ${FlushReason[dc.flushReason]}` ); }}
// 回放到特定 draw call 进行视觉检查Renderer.replayToDrawCall(3);const snapshot = Renderer.getSnapshotImageData();FlushReason
渲染器开始新 draw call 的原因:
| 原因 | 说明 |
|---|---|
BatchFull | 顶点缓冲区已满 |
TextureSlotsFull | 所有纹理槽位已被占用 |
ScissorChange | 裁剪区域变化(UIMask) |
StencilChange | 模板测试状态变化 |
MaterialChange | 材质/着色器不同 |
BlendModeChange | 混合模式变化 |
StageEnd | 渲染阶段边界 |
TypeChange | 渲染类型切换(sprite → text) |
FrameEnd | 帧末尾刷新 |
RenderType
| 类型 | 说明 |
|---|---|
Sprite | 精灵渲染 |
Text | 位图文本 |
Spine | Spine 骨骼 |
Particle | 粒子发射器 |
Shape | 形状渲染器 |
UIElement | UI 元素 |
Mesh | 自定义网格 |
WASM 调试
引擎核心以 WebAssembly 运行。WASM 调试需要引擎的 debug 构建。
Debug 构建
以 debug 模式构建引擎:
node build-tools/cli.js build -t web -d这会生成一个包含 DWARF 调试信息的未优化 WASM 二进制文件。该文件会比 release 构建大很多。
Chrome WASM 调试
- 安装 C/C++ DevTools Support Chrome 扩展
- 打开 Chrome DevTools
- 导航到 Sources 面板
- WASM 源文件会出现在
file://树下 - 在 C++ 源文件中设置断点并逐步执行代码
性能分析
帧时间
使用浏览器 Performance 面板录制跟踪:
- 打开 DevTools 并转到 Performance 标签
- 点击 Record,与游戏交互,然后停止
- 检查火焰图中的长帧
重点关注:
- 系统执行时间过长 — 单个系统每帧耗时过多
- 资源加载阻塞 — 大纹理或 Spine 数据阻塞主线程
- GC 暂停 — 大量分配导致频繁垃圾回收
Draw Call 计数
使用 Draw API 监控渲染统计:
import { Draw } from 'esengine';
console.log('Draw calls:', Draw.getDrawCallCount());console.log('Primitives:', Draw.getPrimitiveCount());ECS 查询性能
如果一个查询范围较广的系统运行缓慢,通过添加更具体的组件过滤器来缩小查询范围。匹配实体更少的查询执行更快。
const narrowSystem = defineSystem( [Query(Mut(Transform), Velocity, Player)], (query) => { for (const [entity, transform, velocity] of query) { transform.position.x += velocity.linear.x; } });常见错误
”WASM module failed to compile”
原因: WASM 二进制文件损坏或与当前浏览器不兼容。
解决:
- 重新构建 WASM 二进制文件:
node build-tools/cli.js build -t web -c - 验证 Emscripten 版本与项目预期一致
- 检查浏览器是否支持 WebAssembly
”Entity not found” / 无效实体访问
原因: 访问已被销毁的实体,或使用了过期的实体 ID。
解决:
- 访问前检查实体是否存在:
if (world.isAlive(entity)) { ... } - 避免跨帧存储实体 ID 而不进行验证
- 记住
Commands是延迟执行的 — 实体不会在调度阶段结束前被销毁
”Component not registered”
原因: 使用了未定义或未导入的组件类型。
解决:
- 确保在使用组件前调用了
defineComponent()或defineBuiltin() - 检查组件模块是否在入口文件中导入
- 对于内置组件,验证 SDK 版本与 WASM 二进制版本匹配
”WebGL context lost”
原因: GPU 驱动重置了 WebGL 上下文,通常由资源耗尽引起。
解决:
- 减少纹理内存使用(更小的图集、压缩纹理)
- 减少活跃的渲染目标数量
- 在移动设备上,最小化后台 GPU 使用
预览中显示空白或黑屏
原因: 可能有多种原因。
排查清单:
- 检查 DevTools 控制台的错误信息
- 验证场景文件存在且是有效的 JSON
- 确保场景中至少存在一个相机实体
- 检查精灵引用的纹理是否已加载(Network 标签中无 404)
- 验证 WASM 二进制文件已成功加载
构建产物与编辑器预览不一致
原因: 构建时的资源处理(图集打包、材质编译)可能暴露预览中不可见的问题。
解决:
- 对比预览和构建的 DevTools 控制台输出
- 验证所有资源都已包含(检查
asset-manifest.json) - 对于微信构建,检查自定义文件扩展名是否在
packOptions.include中