跳转到内容

调试

本指南介绍 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 = 3
setLogLevel(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位图文本
SpineSpine 骨骼
Particle粒子发射器
Shape形状渲染器
UIElementUI 元素
Mesh自定义网格

WASM 调试

引擎核心以 WebAssembly 运行。WASM 调试需要引擎的 debug 构建。

Debug 构建

以 debug 模式构建引擎:

Terminal window
node build-tools/cli.js build -t web -d

这会生成一个包含 DWARF 调试信息的未优化 WASM 二进制文件。该文件会比 release 构建大很多。

Chrome WASM 调试

  1. 安装 C/C++ DevTools Support Chrome 扩展
  2. 打开 Chrome DevTools
  3. 导航到 Sources 面板
  4. WASM 源文件会出现在 file:// 树下
  5. 在 C++ 源文件中设置断点并逐步执行代码

性能分析

帧时间

使用浏览器 Performance 面板录制跟踪:

  1. 打开 DevTools 并转到 Performance 标签
  2. 点击 Record,与游戏交互,然后停止
  3. 检查火焰图中的长帧

重点关注:

  • 系统执行时间过长 — 单个系统每帧耗时过多
  • 资源加载阻塞 — 大纹理或 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 使用

预览中显示空白或黑屏

原因: 可能有多种原因。

排查清单:

  1. 检查 DevTools 控制台的错误信息
  2. 验证场景文件存在且是有效的 JSON
  3. 确保场景中至少存在一个相机实体
  4. 检查精灵引用的纹理是否已加载(Network 标签中无 404)
  5. 验证 WASM 二进制文件已成功加载

构建产物与编辑器预览不一致

原因: 构建时的资源处理(图集打包、材质编译)可能暴露预览中不可见的问题。

解决:

  • 对比预览和构建的 DevTools 控制台输出
  • 验证所有资源都已包含(检查 asset-manifest.json
  • 对于微信构建,检查自定义文件扩展名是否在 packOptions.include

下一步