news 2026/10/21 21:29:29

UE日志等级实战指南:从Fatal到VeryVerbose的选型与避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
UE日志等级实战指南:从Fatal到VeryVerbose的选型与避坑

UE 的日志系统看起来简单,无非是 UE_LOG 加一个等级参数,可真到排查问题的时候才发现,日志等级选得不好,比没有日志还难受。我之前在一个项目里接过一次网上反馈的疑难杂症,玩家反馈“进游戏后过几秒黑屏”,我们让玩家把日志文件发回来,结果打开一看,密密麻麻全是当初联调阶段随手打的临时 Log 级别的输出,真正能定位问题的 Error 早被淹没在几万行噪音里。从那以后我在每个项目里都会重新梳理一遍日志等级规范。

这篇文章不打算像文档那样把每个枚举值念一遍,而是想从实际使用的角度聊聊 UE 日志等级到底该怎么理解、怎么选、怎么配,以及我踩过的那些坑。内容比较长,涉及从开发态到发布态的完整链路,适合刚接触 UE 日志系统的新手,也适合已经写了一年 UE_LOG 但没系统性梳理过等级逻辑的开发者。

1. 等级体系全览:从 Fatal 到 VeryVerbose 的语义边界

UE 的日志等级定义在ELogVerbosity枚举里,常用的一段从高到低是 Fatal、Error、Warning、Display、Log、Verbose、VeryVerbose,另外还有一个 NoLogging 级别的“关闭”开关。很多人把它当成一个简单的严重程度排序,但实际上每一级对应着不同的输出行为、过滤阈值和使用场景,选错等级会导致两种典型后果:要么关键信息被噪音淹没,要么日志文件彻底没有东西可看。

1.1 Fatal:唯一一个必须以“断”为结果的等级

Fatal 是 UE 日志等级里最特殊的一个。它不仅仅是“打印一条红色错误”,而是打印后会直接触发程序中止逻辑。在开发模式下,编辑器会弹出一个带有堆栈的 assert 窗口;在打包版本里,它会尝试生成 crash 文件、把最后这段日志写入崩溃信息,然后结束进程。

所以我在项目里对 Fatal 的使用条件是极其苛刻的:只有“继续跑下去必定产生错误结果,且无法恢复”的情况才用。举个例子,某个模块初始化时依赖的配置文件完全缺失,而且没有任何兜底方案,这个时候可以 Fatal;但仅仅是某个资源加载失败,哪怕后果很严重,也应该用 Error,给上层一个把场景降级继续运行的机会。

还要提一个容易踩的编译坑:在部分引擎版本里,UE_LOG(LogXXX, Fatal, TEXT("boom"))这种写法会直接报编译错误,要求 Fatal 分支至少带一个格式化参数。起初我以为是引擎 bug,后来看了宏展开才明白,Fatal 内部要附带额外的日志信息,编译器强制要求调用方式不能太“裸”。保险的写法是:

UE_LOG(LogTemp, Fatal, TEXT("%s"), TEXT("boom"));

这个习惯我保留到现在,哪怕在不需要额外参数的情况下也统一带一个TEXT("%s"),省得切换引擎版本时被编译错误卡住。

1.2 Error 与 Warning:问题分级不是情绪分级

Error 在 Output Log 里显示为红色,语义是“当前操作失败,但程序还在跑”。我见过不少同事会把 Error 当成“我很生气”的标识,比如某个数值超出预期就丢一条 Error,结果真出问题时 Error 刷了几千条,反而没有一条包含有效堆栈。我的个人标准是:Error 必须能被某个自动化脚本或日志分析工具抓出来,并且看消息内容就能知道“哪块逻辑、哪个对象、期望值是多少、实际值是多少”。

Warning 则更适合“可能有坑,但目前不影响主流程”的情况。它的显示颜色是黄色,常见于“配置项缺失但使用默认值”“某个对象为空但调用方做了保护”“网络请求超时已自动重试”这类场景。Warning 和 Error 之间应该有明确的语义分界线,不能靠“严重程度”这种模糊感觉来判断。

这里给一个我在项目里常用的判定表:

等级显示色写入后的行为典型场景我的判定标准
Fatal红输出后中断进程致命配置缺失、核心系统初始化失败继续跑一定出错且无法恢复
Error红继续运行资源加载失败、网络请求永久失败本次逻辑失败,但程序整体可运行
Warning黄继续运行配置缺失走默认值、请求超时重试不影响功能,但值得关注
Display白继续运行玩家登录信息、关卡切换完成玩家或客服能看懂的关键节点
Log灰继续运行系统初始化流程、状态机切换开发人员排业务逻辑时需要的常规信息
Verbose灰继续运行函数调用细节、临时变量追踪跟踪一条内部链路时的中间状态
VeryVerbose灰继续运行每帧数值、高频循环内数据只在定位某一次具体问题时临时开启

1.3 Display、Log、Verbose、VeryVerbose:过滤价值大于字面义

Display 和 Log 在字面上看起来像同一个东西,但实际使用差异很大。Display 是会在默认控制台和日志面板里直接显示的信息,面向人群可以是策划、测试、客服。我习惯把“玩家切场景成功”“存档完成”“关键系统启动”这类事件放在 Display 级别,这样测试拿到的日志即使不做任何过滤也能看出流程走到哪一步。

Log 级别的信息默认在编辑器 Output Log 面板里也能看到,但它在语义上更偏向“开发过程留痕”。Verbose 和 VeryVerbose 则几乎默认被过滤掉,需要手动抬高输出阈值才能看到。这两个等级在我这里是“临时调试专用通道”,平时代码里可以留着,但默认不输出,等需要排查具体问题时再打开。

比如某个寻路模块一段时间内偶发卡顿,我会把寻路路径生成的具体步骤打进 Verbose,把每个采样点的数值打进 VeryVerbose。平时这两类日志根本不会出现,需要时启动参数加一行,几万条细节就全出来了。

我见过不少项目把 Verbose 当 Log 用,日志文件里全是“进入函数 A”“离开函数 A”这种内容。这会让真正有价值的 Verbose 信息失去意义。一个最简单的检验标准:这条日志如果不让我看,我是否还能从 Error 和 Warning 里推断出问题所在?如果能,这条日志就不该是 Verbose 以上级别。

2. Category 管理:UE_LOG 背后容易被忽略的“二级命名空间”

UE 日志等级并不是孤立存在的,它必须挂在一个 Category 下面。说白了,等级解决的是“这条日志有多严重”,Category 解决的是“这条日志属于哪个模块”。两者的组合才是 UE_LOG 宏的完整调用方式。很多新手把精力全放在等级选型上,反而忽略了对 Category 的规划,等到日志量一上去,连过滤都不知道从哪下手。

2.1 声明一个日志类别到底要几步

引擎内置了大量 Category,比如 LogTemp、LogLoad、LogGame 等。但工程里自己定义的 Category 通常是这么来的:

在头文件里:

DECLARE_LOG_CATEGORY_EXTERN(LogBT, Log, All);

这里的三个参数分别是:Category 名称、默认日志等级、编译期允许的最大等级。All 表示这个类别运行时可以调到任意等级,Log 表示默认输出级别是 Log。

在 cpp 文件里:

DEFINE_LOG_CATEGORY(LogBT);

这两行配合起来,模块内的其他文件才能直接使用UE_LOG(LogBT, Warning, TEXT("..."))。如果 Category 只需要在单个 cpp 里用,也可以用DECLARE_LOG_CATEGORY_CLASS之类的方式限制作用域。

稍微解释一下第一个参数里的All:它相当于编译期的“天花板”,运行时通过命令行调级只能在这个范围内调整。如果这里写成Log,那后面就算用-LogCmds想把该类别调到 Verbose 也不会生效。所以我在项目里统一用All作为编译期上限,让运行期调级有更多自由度。性能上这点开销可以忽略,但对排查问题非常关键。

2.2 格式化字符串的三个经典翻车点

UE_LOG 的格式串和 printf 类似,但细节很不一样,下面这三个问题我在项目里都实打实遇到过。

第一个是FString不能直接作为%s的参数。正确写法是:

FString PlayerName = TEXT("Alice"); UE_LOG(LogBT, Error, TEXT("PlayerName: %s"), *PlayerName);

少了那个*,日志会在运行时读取到非法内存,轻则输出乱码,重则直接崩溃。每次看到%s我都会下意识确认一遍有没有加取指针符,这个习惯救了我很多次。

第二个是TCHAR和char的混淆。在非 Windows 平台或者处理第三方字符串时,经常拿到const char*,直接塞进%s会打印出中文乱码。稳妥做法是先转成FString再传指针:

const char* RawStr = SomeLegacyApi(); UE_LOG(LogBT, Warning, TEXT("RawStr: %s"), *FString(RawStr));

第三个是宏参数里出现逗号会导致展开错误。如果你写的是UE_LOG(LogBT, Error, TEXT("%d"), SomeFunc(a, b));,预处理器会把SomeFunc(a和b)当成两个参数传给宏,编译直接报错。项目里遇到这种情况,我的做法是先算完再传:

int32 Result = SomeFunc(a, b); UE_LOG(LogBT, Error, TEXT("Result: %d"), Result);

2.3 多类别协作与 LogTemp 的正确使用姿势

LogTemp 是引擎提供的一个“快速随便用”的 Category,特点是不用自己声明,走到哪都能打。很多项目因为图省事,全工程只有一个 LogTemp,相当于把日志系统变成了一个巨大的垃圾桶。一旦要区分“战斗模块的日志”和“网络模块的日志”,就只能靠字符串关键字过滤,效率极低,还容易误伤。

我在项目里通常按模块划分 Category:LogBattle、LogNet、LogSave、LogUI、LogAITree这样。每个 Category 在代码库里对应一个明确的模块归属,并且模块负责人有权限在启动参数里单独调整它的输出等级。

这样做的核心好处是:当线上某个战斗类问题需要排查时,我可以直接把LogBattle调到 Verbose,其他模块维持默认级别。日志量从“十万行废话”变成“几百行战斗链路”,排查时间能缩短一个数量级。LogTemp 我会保留,但约法三章:临时定位问题可以用,提交代码前必须改成正式 Category,否则 review 不通过。

3. 日志从产生到落盘的四条路与过滤逻辑

UE_LOG 打出来的内容最终有多个去向,编辑器里有 Output Log 面板,打包版本里可以看日志文件,移动端还要考虑系统级日志通道。不同去向对日志等级的默认过滤策略还不一样,这也是很多人“在电脑上能看到日志,打包后什么都看不到”的原因之一。

3.1 Output Log、游戏内控制台与日志文件

我在编辑器里最常看的是 Output Log 面板,它默认会显示 Log 及以上等级的信息,Verbose 和 VeryVerbose 默认被折叠。面板顶部有过滤下拉框,可以临时把最低显示级别改到 Verbose,背后的原理和运行时设置是一样的。

日志文件的位置各平台不一样,Windows 打包版一般在项目目录下的Saved/Logs/<ProjectName>.log,开发态在项目本体的Saved/Logs下。移动端的话,Android 可以借助系统日志工具查看,iOS 则在设备的系统日志里。不同引擎版本对日志的归档策略有差异,有的会在启动时把上次日志改名留存,有的直接覆盖,所以别把“只有一份日志文件”当成理所当然,真遇到文件积累异常时先去确认默认行为。

游戏内还有一条不太常走的路:运行时按下键打开控制台,输入ShowLog` 可以把日志直接叠印到屏幕上。不过我建议把这条能力和 UE_LOG 分开看待,它更多是用来给开发态临时观察输出,不适合做成正式功能。

3.2 临时调级:启动参数 LogCmds 与运行期 Log 命令

在开发和联调阶段,我会避免为了看某条 Verbose 日志去改代码重新编译,而是用启动参数直接把对应 Category 的等级抬上去。比较常用的一条配置是这样:

-LogCmds="LogBattle Verbose"

如果同时想关掉某个吵到不行的模块日志:

-LogCmds="LogTemp off"

多个命令用逗号分隔,比如:

-LogCmds="LogBattle Verbose, LogUI off"

这段参数可以放在编辑器启动快捷方式的目标参数里,也可以放在打包版程序的快捷方式后面。我在项目里一般会把这个参数做成配置文件读取,方便测试同学在出问题机器上手动输入。

运行期也有对应的控制台命令,大致语法是:

Log LogBattle Verbose

在编辑器控制台敲一下,当前会话立刻生效,不需要重启。这个命令我很常用,特别是遇到那种偶现 bug 时,现场跑着跑着把日志级别调高,抓到一次完整现场,可比事后猜快太多。

3.3 屏幕打印与 UE_LOG 是两个世界

很多人以为在屏幕上显示一条调试信息就是“日志等级为 Display”,这是个误区。屏幕上的调试文字走的是另一个体系,常见的是:

GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, TEXT("Hello"));

它和 UE_LOG 没有直接关系。屏幕文字是给当前视线内的人看的,日志是给未来回看的人读的。我在项目里会把两者分开处理:需要长期追踪的内容走 UE_LOG,临时盯一下的数值走屏幕打印。如果把调试信息全做成屏幕打印,那测试拿回来给我们的截图里能看到,但日志文件里没有任何记录,排查问题少了一条重要线索。

4. 发布包里的日志生存法则

开发态一切正常,打包完却发现日志文件几乎是空的,这应该是 UE 开发者都经历过的困惑。这其实不是“日志系统坏了”,而是发布配置和开发配置对日志的默认策略压根不一样。

4.1 Shipping 构建默认到底还剩下什么

默认情况下,Shipping 构建会启用引擎的日志剥离机制,大量 UE_LOG 调用会被编译期静态剥离,表现为日志文件只有很少量的启动信息,甚至什么都没有。第一次遇到这个现象时我也以为是打包配置出了问题,折腾好久才发现是引擎有意为之。

这背后不是一句“发布版不需要日志”这么简单。日志系统在剥离后可以减少二进制体积和运行时开销,也避免把资源路径、内部参数等敏感信息暴露给玩家。但从我们开发者角度看,发布包没有日志,线上问题就会变得特别难查。所以我在项目里会提前确认,我们到底需不需要在发布版本保留日志能力。

如果确定要留,需要修改构建相关的日志开关,把USE_LOGGING_IN_SHIPPING之类的配置打开。这个开关在不同引擎版本里位置和名称可能不一样,最稳的方式是去引擎源码里搜关键词,确认当前版本的实际行为,再改到项目配置里,而不是依赖某个固定路径。我只能说结论:保留日志后,Warning 和 Error 是很值得长期驻留的等级。

4.2 日志被“吃掉”时如何自救

有时候发布版日志并不是全没了,而是某个 Category 的等级被整体压掉了。遇到这种情况,我会按下面这个顺序排查:

先看日志文件里有没有类别名出现。如果类别名完全不存在,多半是编译期剥离或者整个日志系统没启动。再试试启动参数能不能把等级抬回来,如果参数对某个类别无效,检查 Category 声明时All那个编译期上限是不是写得太低。

还有一个小技巧:某些平台下,UE_LOG 的内容会同时出现在系统日志里,比如 Android 的日志通道。如果本地日志文件看着不对劲,先去系统日志里搜看一眼,有时候能捞到比文件更完整的内容。

4.3 别让 Verbose 把帧率拖下水:性能与过滤的关系

日志等级不只是“显示不显示”的问题,它会影响运行效率。高频循环内部如果打了一条 Log 级别的日志,即使控制台没显示,格式化字符串、日志队列、写盘操作仍然在发生。我在项目里见过某段业务逻辑每帧调用三次 UE_LOG,固定一秒几百条输出,直接让调试版的帧率掉了十几帧,原因就是Floor和Ceil这种高频流程里塞了太多日志。

所以我对 Verbose 和 VeryVerbose 的使用原则是:低频的关键链路可以保留,高频循环内的日志要么加上节流,要么干脆只在特殊调试宏开启时才编译。简单说,日志代码不是越详细越好,而是越“按需可见”越好。

5. 我沉淀下来的日志分级实操习惯

日志等级这个东西,文档上说再多都不如在实际项目里定一套规则并严格执行。下面这几条是我踩了不少坑之后总结出来的,每个新项目启动时都会直接把规范贴在团队文档里,照着做基本不会出大乱子。

5.1 每条日志输出前过一遍“未来视角”

写代码时我会问自己一个问题:一个月后这个功能出 bug 了,我拿着日志文件最希望看到什么?如果答案是“某个关键节点是否到达”,这条日志就值得 Display 或 Log;如果答案是“中间过程的具体参数”,那它更适合 Verbose;如果这条日志只是为了告诉我“这段代码被跑到了”,那我大概率不会加它。

说白了,日志是写给未来的自己看的,不是写给当前调试的我看的。当前调试我可以用断点、可以用屏幕打印,但未来回看时只有日志文件能帮上忙。所以每一条 UE_LOG 我都会在写完后再读一遍,确认消息里包含了对象名、环境信息、关键参数这些能支撑定位的内容。

5.2 项目里的统一日志规范示例

我们项目组现在用的规范大致是下面这样:

  • Display:给测试和策划看的流程性信息,例如“战斗结束,结算完成”。
  • Log:开发人员排业务逻辑用的常规信息,例如“进入状态机 Idle 状态”。
  • Warning:非阻塞但需要关注的异常,例如“存档文件缺失,使用默认配置”。
  • Error:功能失败且会留下可见副作用,例如“初始化存档失败,当前玩家数据不可保存”。
  • Fatal:核心系统初始化失败且无法恢复。

统一规范最大的好处是,团队里任何人看到日志文件都会用同一套标准判断严重程度。不同成员对“红色”和“黄色”的解读可能完全不同,但有了等级定义后,红黄颜色只代表约定好的语义,不再代表个人情绪。

5.3 临时调试通道的开关设计

有些调试日志需要在特殊情况下打开,但不能让它们默认参与编译输出。我会给这些日志挂一个自己封装的宏,类似这样:

#if WITH_DEV_AUTOMATION_TESTS || UE_BUILD_DEBUG #define BT_DEBUG_LOG(Verbosity, Format, ...) UE_LOG(LogBT, Verbosity, Format, ##__VA_ARGS__) #else #define BT_DEBUG_LOG(Verbosity, Format, ...) #endif

这样在开发版里它们正常工作,打包到正式版时自动消失,不会产生任何开销,也不用担心发布包日志文件被这些临时内容刷爆。用宏包一层还能顺便统一 Category,以后想全局关闭一批调试日志时只需改一个地方。

最后再补充一点我在实际项目中反复验证过的体会:日志等级选对了,很多问题根本不需要现场,光靠玩家发回来的日志就能定位;选错了,等于是把本来就难解的问题又加了一层阻碍。与其在系统上线后焦头烂额地翻十万行噪音,不如在写第一行 UE_LOG 时就想清楚这条日志到底该是什么等级。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 21:28:03

无需GPU也能跑:CPU本地部署Llama2与图片识别实操指南

把Llama2这样的大模型部署到本地&#xff0c;还能在不碰GPU的情况下陪聊、分析文字、甚至识别图片内容——放在两年前我会觉得离谱。但实测下来&#xff0c;这件事不仅可行&#xff0c;而且整个过程就是两条命令的事&#xff0c;真正属于“一键部署”的范畴。今天这篇文章就把我…

作者头像 李华
网站建设 2026/10/21 21:27:20

上下文工程实战:六个技巧让大模型应用稳定交付

1. 为什么我们开始认真对待上下文工程 做过大模型应用开发的同学应该都有过这种体验&#xff1a;明明同一个模型&#xff0c;别人调的Agent能稳定执行复杂任务&#xff0c;自己写的却经常答非所问、丢三落四。模型本身没变&#xff0c;变的只是喂给它的上下文。我前几个月跟进一…

作者头像 李华
网站建设 2026/10/21 21:26:02

Agentic AI智能制造案例集:提示工程架构师实战指南

这两年做智能制造AI落地&#xff0c;我越来越确认一个判断&#xff1a;提示工程架构师的活儿&#xff0c;正在从“写好问答提示词”变成“设计一套会干活的智能体流程”。所谓Agentic AI智能制造案例集&#xff0c;就是围绕这个转变沉淀下来的一批实战样本——别人踩过的坑、验…

作者头像 李华
网站建设 2026/10/21 21:25:55

泄流效应下光伏配电网无功补偿优化:基于IEEE33节点算例

1. 泄流效应到底是什么&#xff0c;它凭什么推翻你的无功补偿方案1.1 先说一个让我印象深刻的仿真异常去年帮某课题组复核一个台区级的光伏接入方案&#xff0c;他们在原有配电网模型上加了一个容量不大的分布式光伏&#xff0c;然后在原本的补偿节点&#xff08;线路末端&…

作者头像 李华
网站建设 2026/10/21 21:25:35

从万维网到Lisp再到电商:11月13日三大技术事件如何塑造信息世界

1. 三个改变信息世界的事件&#xff0c;为什么值得放在同一天讲 11 月 13 日这个日子&#xff0c;在技术圈里其实挺有意思。它不像某些被反复纪念的“大日子”那样自带光环&#xff0c;但如果你把这一天发生的几件事摆在一起看&#xff0c;会发现一条很清晰的线索&#xff1a; …

作者头像 李华
网站建设 2026/10/21 21:25:17

从零搭建AI任务调度助手:LangGraph多智能体编排指南

你有没有遇到过这种情况&#xff1a;想让一个模型同时搞定"写脚本""改bug""写使用说明"三件事&#xff0c;结果它顾此失彼&#xff0c;代码里夹着解释文字&#xff0c;文案里又带着代码片段&#xff0c;最后两边都不像样。我一开始也是这么干的&…

作者头像 李华