日志与调试绘制
Logging & Debug Drawing
几乎所有专业团队都会为引擎打造一套调试与开发工具。本页记录其中最基础的两类:日志与追踪(格式化调试输出、详细级别、通道、镜像到文件、崩溃报告)和调试绘制(在游戏世界或屏幕上画线、形状和文字以可视化数学计算)。(卷 I 第 10 章 引言、10.1–10.2 节)
为什么仍要 printf 调试(卷 I 10.1)
在实时程序里,有些 bug 依赖时序、只在全速运行时出现,有些由一长串复杂事件引起,难以靠断点和监视窗口逐一追踪;这时最有力的工具往往是一串打印语句(C/C++ 程序员称 printf 调试)。各平台都有某种控制台/TTY 输出:Linux 或 Win32 控制台程序打印到 stdout/stderr;Win32 窗口程序没有控制台,但在 Visual Studio 调试器下可用 OutputDebugString() 打印到调试输出窗口;PlayStation 的 PC 端 Target Manager 负责在主机上启动程序,并提供一组 TTY 输出窗口。
格式化输出(卷 I 10.1.1)
OutputDebugString() 只能打印原始字符串、不支持格式化,所以 Windows 引擎通常包装它:VDebugPrintF(format, va_list) 用 vsnprintf 格式化到 1024 字符的静态缓冲再调用 OutputDebugString();DebugPrintF(format, ...) 用 va_start/va_end 转调前者。拆成两个函数是因为省略号参数无法在函数间传递,而 va_list 可以,其他打印函数都能基于 VDebugPrintF() 构建。
注(实现细节):
vsnprintf在输出被截断时返回的是「本应写入」的字符数(不含结尾空字符),而非实际写入数,所以书中命名为charsWritten的返回值可能大于MAX_CHARS,负值表示编码错误(见 cppreferencevsnprintf条目);另外函数内的static缓冲在多线程同时打印时会互相覆盖,实际引擎需加锁或改用线程局部/栈上缓冲。示例中sprintf(buffer, ...)写入 128 字节缓冲也同理宜用snprintf防止溢出。
详细级别、通道与文件镜像(卷 I 10.1.2–10.1.4)
- 详细级别 (verbosity):打印语句写好后最好留着以备再用,所以引擎通常允许通过命令行或运行时动态控制详细级别:最低(通常 0)只打印严重错误,越高输出越多。最简单的实现是全局
g_verbosity加VerboseDebugPrintF(int verbosity, ...),仅当g_verbosity >= verbosity才打印。 - 通道 (channel):按系统分类输出(动画、物理……)。PlayStation Target Manager 的 TTY 支持多个通道、各占一个标签页,另有汇总全部输出的标签页:查动画问题看动画通道,来源不明的问题看「全部」。Windows 只有一个调试控制台,但仍可分通道:每通道不同颜色,运行时可开关的过滤器只显示指定通道。实现上给打印函数加一个通道参数(数字,或更好的
enum符号值,或字符串/哈希字符串 id),只在通道处于激活列表时打印;通道不超过 32 或 64 个时可用一个位掩码表示过滤器,1 为激活、0 为静音。 - Naughty Dog 的做法(10.1.3.1):游戏把调试文本输出到与各引擎系统(动画、渲染、AI、声音等)关联的命名通道,由轻量的 Redis 键值存储收集(参考文献 [226]),网页界面 Connector 让用户在任意浏览器中查看和过滤。
- 镜像到文件:把所有调试输出镜像到一个或多个日志文件(如每通道一个),便于事后诊断;理想情况下文件应包含全部输出,与当前详细级别和通道掩码无关。可考虑每次调用后刷新日志文件,以免崩溃时丢失最后一缓冲的输出(最后的输出往往对查崩溃最有用);但刷新开销大,只在日志不多、或确有必要时这么做,可做成引擎配置开关。日志写盘本身的性能问题另见 引擎文件系统与异步文件读写 中《红色警戒 3》的例子。
崩溃报告(卷 I 10.1.5)
多数操作系统可安装顶层异常处理函数捕获大部分崩溃,在其中输出各种有用信息,甚至把报告发邮件给整个程序团队——看到美术和策划崩溃得多频繁,程序员会重新感到修 bug 的紧迫。报告可包含:当时所在关卡;玩家角色的世界坐标;玩家的动画/动作状态;正在运行的玩法脚本(脚本本身就是崩溃原因时尤其有用);栈回溯(多数 OS 提供遍历调用栈的机制,但非标准、高度平台相关,可打印栈上所有非内联函数的符号名);所有内存分配器的状态(剩余量、碎片程度等,对低内存引起的 bug 有用);崩溃瞬间的截图;以及其他相关信息。断言与错误处理本身见 错误处理与断言。
PlayStation 生态有强大的崩溃报告系统:PS5 系统软件检测到崩溃后把大量信息上传到 SIE 托管的 Recap 服务器,开发者可通过 RESTful API 获取崩溃统计、下载核心转储、TTY 输出、截图以及崩溃前的视频等。
调试绘制(卷 I 10.2)
现代游戏几乎完全由数学驱动(摆放和移动物体、碰撞测试、射线判定视线、矩阵把物体从对象空间变换到世界空间再到屏幕空间),即使 2D 游戏也很难在脑中想象这些计算结果。所以好引擎都提供画彩色线条、简单形状和 3D 文字的 API——调试绘制 (debug drawing),只在开发调试期使用、发售前移除。例:子弹打不中敌人时,与其在调试器里解读一堆数字,不如在游戏里画出弹道线,逻辑和数学错误一目了然(作者:一图胜过千分钟调试)。
Naughty Dog 引擎中的例子(在专门的试玩测试关卡中):
- 敌方 NPC 对玩家的感知:小「火柴人」表示 NPC 认为的玩家位置,玩家切断视线后火柴人停在最后已知位置(《最后生还者 重制版》);
- 用线框球可视化爆炸不断扩大的冲击半径;
- 用圆表示德雷克寻找可攀附岩架的搜索半径,一条线表示当前攀附的岩架(《神秘海域》);
- AI 角色的特殊调试模式:「大脑」关闭,开发者用简单的 HUD 菜单完全控制其移动与动作,瞄准摄像机在世界中标记目标点,命令其走/跑/冲刺过去、进出掩体、开火等。
调试绘制 API 的要求(卷 I 10.2.1)
- 简单易用;
- 支持实用的图元:线、球、点(通常画成小十字或小球,单个像素太难看清)、坐标轴(通常 x 红、y 绿、z 蓝)、包围盒、格式化文字;
- 灵活控制:颜色、线宽、球半径、点大小、坐标轴长度等「预制」图元的尺寸;
- 可在世界空间(用游戏摄像机的透视投影,用于标注 3D 场景中的物体)或屏幕空间(正交或透视投影,用于与摄像机无关的 HUD 式调试信息)绘制;
- 可开或关深度测试:开启时图元会被场景中真实物体遮挡,深度易判断但可能看不见;关闭时图元「浮」在物体之上,难判断深度但永远可见;
- 可在代码任何地方调用:多数渲染引擎要求几何体在游戏循环的特定阶段(通常帧末)提交,所以系统必须把所有请求排队,到合适时机再提交;
- 每个图元最好有生命期:每帧都调用的代码生命期可为一帧(每帧刷新所以一直显示);很少或间歇调用的代码(如计算抛射体初速度的函数)应能给图元几秒的生命期,避免只闪一帧就消失;
- 能高效处理大量图元:为 1000 个游戏对象画调试信息时图元数会迅速累积,开启调试绘制不应让游戏无法使用。
Naughty Dog 引擎的 API 大致如下:DebugDrawManager 提供 AddLine(起点、终点、颜色、线宽默认 1、持续时间默认 0、是否深度测试默认 true)、AddCross(轴对齐十字:3 条线交于一点)、AddSphere(线框球)、AddCircle(中心、平面法线、半径)、AddAxes(按给定变换画坐标轴)、AddTriangle、AddAABB、AddOBB(中心变换 + xyz 缩放)、AddString。全局 g_debugDrawMgr 用透视投影在完整 3D 中绘制;g_debugDrawMgr2D 在 2D 屏幕空间绘制,点的 z 分量是一个特殊代码,表示 (x,y) 是绝对像素还是 0.0–1.0 的归一化坐标(后者与屏幕实际分辨率无关)。
void Vehicle::Update() {
// 画出速度向量
const Point& start = GetWorldSpacePosition();
Point end = start + GetVelocity();
g_debugDrawMgr.AddLine(start, end, kColorRed);
// 在车辆位置显示名字与乘客数(不做深度测试)
char buffer[128];
sprintf(buffer, "Vehicle %s: %d passengers", GetName(), GetNumPassengers());
g_debugDrawMgr.AddString(GetWorldSpacePosition(), buffer, kColorWhite, 0.0f, false);
}
函数名用 add 而不是 draw,正是因为图元不会立即绘制,而是加入稍后绘制的视觉元素列表(渲染引擎见第 11 章)。下图示意这一排队流程:
flowchart LR
A["游戏代码任意位置<br/>AddLine / AddString…"] --> Q["调试图元队列<br/>带生命期"]
Q --> R["帧末渲染阶段统一提交"]
R --> E{"生命期到期?"}
E -- "否" --> Q
E -- "是" --> X["移除"]与 UE5 的对照
UE5 中面向玩法调试的可视化工具见 Gameplay 调试器与可视化日志;本页记录 GEA 的通用设计要求与 Naughty Dog 的实现,两者细节以各自来源为准。
出处
- Game_Engine_Architecture_4Ed_-_Jason_Gregory.pdf
- Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 I 第 10 章 10.1–10.2 节