中EN
概念页

日志与调试绘制

Logging & Debug Drawing

更新于 2026-10-13 约 10 分钟调试日志TTY通道崩溃报告调试绘制GEA

几乎所有专业团队都会为引擎打造一套调试与开发工具。本页记录其中最基础的两类:日志与追踪(格式化调试输出、详细级别、通道、镜像到文件、崩溃报告)和调试绘制(在游戏世界或屏幕上画线、形状和文字以可视化数学计算)。(卷 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,负值表示编码错误(见 cppreference vsnprintf 条目);另外函数内的 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 的实现,两者细节以各自来源为准。

  1. Game_Engine_Architecture_4Ed_-_Jason_Gregory.pdf
  2. Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 I 第 10 章 10.1–10.2 节

相关页面

游戏开发错误处理与断言Error Handling & AssertionsGEA 把错误分为用户错误和程序员错误,两者处理方式相反:用户错误要尽量平稳地告知并继续,程序员错误应立即停机并给出详细的底层信息。“用户”又分玩家和制作游戏的开发者。实现手段有错误返回码、异常和断言。Naughty Dog、EA、Midway、Insomniac 等在运行时代码中通常关闭 C++ 异常,靠断言和静态断言 (staticassert) 捕获 bug,并用 RAII(Naughty Dog 称为 janitor)管理资源。游戏开发游戏内调试工具In-Game Debugging Tools各子系统(渲染、动画、碰撞、物理、音频、网络、玩家机制、AI……)都有大量配置项,能在游戏运行时调整而无需改代码、重新编译链接再运行,可大幅节省调试和搭建关卡/玩法的时间。GEA 介绍支撑这一点的几类游戏内工具:游戏内菜单(及 Dear ImGui 等第三方 GUI)、游戏内控制台、调试摄像机与暂停/慢动作、作弊功能、截图与录像。(卷 I 10.3–10.7 节)游戏开发游戏内性能与内存剖析In-Game Profiling & Memory Tracking游戏是实时系统,要保持 30 或 60 FPS,同时受目标硬件内存限制。第三方剖析工具有局限、在主机上未必可用,所以许多引擎自带游戏内剖析器(手动插桩的代码块计时、层级关系、调用次数、导出 CSV)和内存统计与泄漏检测工具。GEA 讲解它们的实现要点和常见限制。(卷 I 10.8–10.9 节)游戏开发 · UE5Gameplay 调试器与可视化日志Gameplay Debugger & Visual LoggerGameplay 调试器 (GDT) 用于在运行时实时查看玩法数据(Pawn、AIController、行为树与黑板、EQS、感知、导航网格),联网时客户端也能看,并且只复制已启用类别的数据。可视化日志 (Visual Logger) 则把 Actor 的状态和图形化信息录下来,之后在编辑器里按时间轴来回拖动查看,适合排查难以复现的 bug。游戏开发引擎文件系统与异步文件读写Engine File System & Asynchronous File IO资源管理器大量依赖文件系统。引擎通常把操作系统的文件 API 包装成自己的 API:一是跨平台、屏蔽硬件差异;二是操作系统缺少引擎需要的功能(如流式加载),主机还要统一访问记忆棒、硬盘、蓝光盘、网络文件系统等介质。引擎文件系统 API 通常涵盖:文件名与路径操作、打开/关闭/读写文件、扫描目录、处理异步 I/O 请求(流式加载)。游戏开发字符串与唯一标识符Strings & Unique Identifiers字符串在 C/C++ 中只是字符数组,变长意味着要么硬编码长度上限、要么动态分配;比较和复制都是 O(n)。引擎内部大量用字符串做资源名和对象 ID,处理不当会严重拖慢游戏。GEA 的建议:运行时代码尽量避免字符串类,若用则了解其成本并按引用传递;路径类是值得专门做的例外;对象与资产的唯一标识用哈希字符串 ID(string id),兼得字符串的可读性与整数的比较速度。图形学覆盖层与用户界面渲染Overlays & User Interface RenderingHUD、武器选择界面、各种菜单等图形用户界面,加上调试绘制,都在主 3D 场景渲染完之后才画,所以统称覆盖层 (overlay)。它们一般关闭深度测试、用屏幕空间四边形和正交投影绘制,由文字、位图、嵌入视频和矢量图形几类二维图元组成(GEA4 卷 II 12.8)。

输入关键词开始搜索

滚轮或双指缩放 · 拖动平移 · 双击放大 / 还原