UE_LOG(UE5)参考手册:可追溯的宏、开关与输出链路
目标读者:有 UE 经验的 C++ 程序员。定位:随手查的参考手册(结论可追溯到源码)。
范围与版本
- 适用范围:UE5(以 UE 5.7 源码为主要验证依据)。
- 验证方式:文中每个关键结论都给出“源码定位(文件/搜索关键字)”,你可以在任意 UE5 版本用全局搜索核对。
术语与心智模型
最小心智模型(你需要记住的 3 层过滤)
一次 UE_LOG(...) 是否真正产生输出,取决于:
- 编译期裁剪(compile-time):如果该条日志在编译期就被判定“永远不会输出”,它会被直接裁掉(没有运行时开销)。
- 运行期抑制(runtime suppression):如果该分类在当前 verbosity 下被 suppress,则不会输出(通常也避免执行昂贵的格式化/字段构造)。
- 输出设备链(output devices):最终会被分发到哪些目的地(文件、控制台、IDE debug 输出等)由
GLog/GWarn所挂载的FOutputDevice决定。
术语表
| 术语 | 含义 | 你应该关心的点 |
|---|---|---|
| Log Category(日志分类) | LogTemp/LogNet 这类“通道” |
每个分类有默认 verbosity与编译期上限;运行期还可调。 |
| Verbosity(级别) | Error/Warning/Display/Log/Verbose/... |
既用于过滤,也影响默认输出路由(见输出链路)。 |
| Compile-time verbosity(编译期上限) | 分类模板参数或全局宏 | 高于上限的日志会被编译期裁剪。 |
| Default verbosity(默认级别) | 分类模板参数 | 启动后可被 ini/命令行/控制台覆盖。 |
| Suppress(抑制) | 运行期过滤机制 | 由 FLogSuppressionInterface 解析 ini/命令行/控制台命令并应用。 |
| Output device(输出设备) | FOutputDeviceFile/console/debug 等 |
GLog(redirector)会把日志广播到已注册的设备。 |
GLog / GWarn |
全局输出入口 | Display/Warning/Error 通常更倾向走 GWarn,其它走 GLog,但两者可能继续互相转发(见链路)。 |
宏与 API 速查表
源码定位:
Engine/Source/Runtime/Core/Public/Logging/LogMacros.h(搜索关键字见附录)。
文本日志(printf 风格)
| 宏/接口 | 作用 | 关键注意点 |
|---|---|---|
UE_LOG(Category, Verbosity, TEXT("..."), ...) |
最常用日志输出 | 先经编译期裁剪,再经运行期 IsSuppressed 过滤。 |
UE_CLOG(Condition, Category, Verbosity, TEXT("..."), ...) |
条件日志 | Condition 通常只在“该分类/级别活跃时”才会求值(避免热路径开销)。 |
UE_LOG_ACTIVE(Category, Verbosity) |
判断“当前是否会输出该级别” | 常用于包住昂贵字符串构造/统计。 |
UE_SUPPRESS(Category, Verbosity, { ... }) |
“仅在未被 suppress 时执行代码块”并设置作用域默认分类/级别 | 不是简单开关;其语义更接近“guard + 作用域 override”。 |
UE_GET_LOG_VERBOSITY(Category) |
读取分类当前 verbosity | 运行期值。 |
UE_SET_LOG_VERBOSITY(Category, Verbosity) |
设置分类当前 verbosity | 等价于 Category.SetVerbosity(ELogVerbosity::Verbosity)。 |
UE_LOG_REF(CategoryRef, Verbosity, ...) |
以“引用形式”写日志 | UE 5.7 标注 DO NOT USE / 将来废弃;优先用 UE_LOG(CategoryName, ...)。 |
结构化日志(UE_LOGFMT 系列,推荐优先使用)
源码定位:
Engine/Source/Runtime/Core/Public/Logging/StructuredLog.h
| 宏 | 作用 | 关键注意点 |
|---|---|---|
UE_LOGFMT(Category, Verbosity, "...{Field}...", ...) |
结构化日志(字段 ≤16) | 支持位置参数或命名字段(二选一);字段名需匹配 [A-Za-z0-9_]+。 |
UE_LOGFMT_EX(...) |
同上但无字段数限制 | 位置参数需包 UE_LOGFMT_VALUE(...);命名字段用 UE_LOGFMT_FIELD("Name", Value)。 |
UE_CLOGFMT(...) / UE_CLOGFMT_EX(...) |
条件结构化日志 | 条件成立且日志活跃才会构造字段并输出。 |
UE_LOGFMT_LOC... |
本地化结构化日志 | 适用于需要本地化文本格式的场景。 |
UE_LOGFMT 详解
什么是结构化日志?
传统 UE_LOG 使用 printf 风格格式化:
cpp
UE_LOG(LogTemp, Warning, TEXT("Player %s died at (%f, %f)"), *PlayerName, X, Y);
UE_LOGFMT 支持命名字段和位置参数,更现代、更易读:
cpp
UE_LOGFMT(LogTemp, Warning, "Player '{Name}' died at ({X}, {Y})",
("Name", PlayerName), ("X", X), ("Y", Y));
两种参数风格
| 风格 | 语法 | 示例 |
|---|---|---|
| 命名字段(推荐) | {FieldName} + ("FieldName", Value) |
"Health: {HP}", ("HP", 100) |
| 位置参数 | {0}, {1}, {2} |
"Pos: ({0}, {1})", X, Y |
重要:命名字段和位置参数不能混用,必须二选一。
命名字段风格示例
cpp
#include "Logging/StructuredLog.h"
void OnPlayerDeath(const FString& PlayerName, const FVector& Location, int32 KillerID)
{
UE_LOGFMT(LogMyGame, Warning,
"Player '{Player}' died at ({X}, {Y}, {Z}), killed by ID {Killer}",
("Player", PlayerName),
("X", Location.X),
("Y", Location.Y),
("Z", Location.Z),
("Killer", KillerID));
}
位置参数风格示例
cpp
UE_LOGFMT(LogTemp, Log, "Position: ({0}, {1}, {2})", X, Y, Z);
UE_LOGFMT_EX:突破 16 字段限制
当字段超过 16 个时,需使用 _EX 后缀版本:
cpp
// 位置参数版本 - 使用 UE_LOGFMT_VALUE
UE_LOGFMT_EX(LogTemp, Log, "Values: {0}, {1}, {2}...",
UE_LOGFMT_VALUE(Value1),
UE_LOGFMT_VALUE(Value2),
UE_LOGFMT_VALUE(Value3));
// 命名字段版本 - 使用 UE_LOGFMT_FIELD
UE_LOGFMT_EX(LogTemp, Log, "Data: {A}, {B}, {C}...",
UE_LOGFMT_FIELD("A", ValueA),
UE_LOGFMT_FIELD("B", ValueB),
UE_LOGFMT_FIELD("C", ValueC));
UE_CLOGFMT:条件结构化日志
只在条件成立且日志级别活跃时才会构造字段并输出:
cpp
// 只有当 Health < 0 时才输出
UE_CLOGFMT(Health < 0, LogTemp, Error,
"Player '{Name}' has invalid health: {Health}",
("Name", PlayerName), ("Health", Health));
字段命名规则
字段名必须匹配正则:[A-Za-z0-9_]+
cpp
// ✅ 正确
("PlayerName", Name)
("HP_Current", Health)
("X1", PosX)
// ❌ 错误
("Player-Name", Name) // 不能有连字符
("HP.Current", Health) // 不能有点号
UE_LOGFMT vs UE_LOG 对比
| 特性 | UE_LOG | UE_LOGFMT |
|---|---|---|
| 格式风格 | printf (%s, %d) |
模板 ({Name}, {0}) |
| 可读性 | 需对照参数顺序 | 字段名自解释 |
| 类型安全 | 弱(运行时崩溃) | 强(编译期检查) |
| 结构化输出 | 不支持 | 支持(便于日志分析工具) |
| 字段数限制 | 无限制 | 基础版 ≤16,EX 版无限制 |
使用注意事项
- 必须包含头文件:
#include "Logging/StructuredLog.h" - 字段名区分大小写:
{Name}和{name}是不同字段 - 热路径优化:仍建议配合
UE_LOG_ACTIVE检查
UE_LOGFMT 源码实现解析
源码定位:
Engine/Source/Runtime/Core/Public/Logging/StructuredLog.h
1. 宏展开机制
源码定位:
StructuredLog.h第 46、64、543-555 行
cpp
// UE_LOGFMT 宏定义(源码原文)
#define UE_LOGFMT(CategoryName, Verbosity, Format, ...) \
UE_PRIVATE_LOGFMT_CALL(UE_LOGFMT_EX, (CategoryName, Verbosity, Format UE_PRIVATE_LOGFMT_FIELDS(__VA_ARGS__)))
#define UE_LOGFMT_EX(CategoryName, Verbosity, Format, ...) \
UE_PRIVATE_LOGFMT(UE_EMPTY, CategoryName, Verbosity, Format, ##__VA_ARGS__)
// UE_PRIVATE_LOGFMT 核心实现
#define UE_PRIVATE_LOGFMT(Condition, CategoryName, Verbosity, Format, ...) \
do \
{ \
/* 1. 编译期检查:是否超过 CompileTimeVerbosity */ \
if constexpr ((::ELogVerbosity::Verbosity & ::ELogVerbosity::VerbosityMask) == ::ELogVerbosity::Fatal || \
((::ELogVerbosity::Verbosity & ::ELogVerbosity::VerbosityMask) <= ::ELogVerbosity::COMPILED_IN_MINIMUM_VERBOSITY && \
(::ELogVerbosity::Verbosity & ::ELogVerbosity::VerbosityMask) <= CategoryName.CompileTimeVerbosity)) \
{ \
/* 2. 创建静态日志记录结构 */ \
static ::UE::Logging::Private::FStaticLogDynamicData LOG_Dynamic; \
static constexpr ::UE::Logging::Private::FStaticLogRecord LOG_Static{TEXT(Format), __builtin_FILE(), __builtin_LINE(), ::ELogVerbosity::Verbosity, LOG_Dynamic}; \
/* 3. 运行期检查 + 输出 */ \
UE_PRIVATE_LOGFMT_LOG_IF_ACTIVE(Condition, CategoryName, Verbosity, LOG_Static, ##__VA_ARGS__); \
} \
} \
while (false)
// 运行期检查与输出
#define UE_PRIVATE_LOGFMT_LOG_IF_ACTIVE(Condition, CategoryName, Verbosity, Log, ...) \
if constexpr ((::ELogVerbosity::Verbosity & ::ELogVerbosity::VerbosityMask) == ::ELogVerbosity::Fatal) \
{ \
Condition { ::UE::Logging::Private::FatalLogWithFields(CategoryName, LOG_Static, ##__VA_ARGS__); } \
} \
else if (!CategoryName.IsSuppressed(::ELogVerbosity::Verbosity)) \
{ \
Condition { ::UE::Logging::Private::LogWithFields(CategoryName, LOG_Static, ##__VA_ARGS__); } \
}
关键点:
if constexpr:编译期裁剪,超过上限的日志不生成任何代码FStaticLogRecord:静态日志记录,包含格式字符串、文件名、行号等元数据IsSuppressed():运行期检查,被抑制则跳过LogWithFields():实际执行字段构造和输出
2. UE_PRIVATE_LOGFMT_FIELD 宏展开机制(核心)
UE_PRIVATE_LOGFMT_FIELD 是 UE_LOGFMT 命名字段语法的核心实现,它使用一套精巧的宏技巧来区分"命名字段 ("Name", Value)"和"位置参数 Value"。
源码定位:
StructuredLog.h第 594-600 行
宏定义(源码原文):
cpp
// 入口宏:展开一个字段,可以是 (Name, Value) 或 Value
#define UE_PRIVATE_LOGFMT_FIELD(Field) UE_PRIVATE_LOGFMT_FIELD_EXPAND(UE_PRIVATE_LOGFMT_NAMED_FIELD Field)
// 只有当 Field 是带括号的 (Name, Value) 时才会被调用
#define UE_PRIVATE_LOGFMT_NAMED_FIELD(Name, ...) UE_PRIVATE_LOGFMT_NAMED_FIELD ::UE::Logging::Private::CheckFieldName(Name), __VA_ARGS__
// 移除 UE_PRIVATE_LOGFMT_NAMED_FIELD 前缀的三连宏
#define UE_PRIVATE_LOGFMT_FIELD_EXPAND(...) UE_PRIVATE_LOGFMT_FIELD_EXPAND_INNER(__VA_ARGS__)
#define UE_PRIVATE_LOGFMT_FIELD_EXPAND_INNER(...) UE_PRIVATE_LOGFMT_STRIP_ ## __VA_ARGS__
#define UE_PRIVATE_LOGFMT_STRIP_UE_PRIVATE_LOGFMT_NAMED_FIELD
完整展开示例(命名字段):
cpp
// 原始代码
UE_LOGFMT(LogTemp, Warning, "Player '{Name}' HP: {HP}", ("Name", PlayerName), ("HP", Health));
// 第一步:UE_LOGFMT 展开
// ("Name", PlayerName) 被传入 UE_PRIVATE_LOGFMT_FIELD
UE_PRIVATE_LOGFMT_FIELD(("Name", PlayerName))
// 第二步:入口宏展开
// Field = ("Name", PlayerName)
UE_PRIVATE_LOGFMT_FIELD_EXPAND(UE_PRIVATE_LOGFMT_NAMED_FIELD ("Name", PlayerName))
// ↑ 注意:Field 带括号,所以 UE_PRIVATE_LOGFMT_NAMED_FIELD 被当作函数式宏调用
// 第三步:UE_PRIVATE_LOGFMT_NAMED_FIELD 展开
// Name = "Name", __VA_ARGS__ = PlayerName
UE_PRIVATE_LOGFMT_FIELD_EXPAND(UE_PRIVATE_LOGFMT_NAMED_FIELD ::UE::Logging::Private::CheckFieldName("Name"), PlayerName)
// ↑ 关键:前面保留了 UE_PRIVATE_LOGFMT_NAMED_FIELD 作为标记
// 第四步:UE_PRIVATE_LOGFMT_FIELD_EXPAND 展开
UE_PRIVATE_LOGFMT_FIELD_EXPAND_INNER(UE_PRIVATE_LOGFMT_NAMED_FIELD ::UE::Logging::Private::CheckFieldName("Name"), PlayerName)
// 第五步:UE_PRIVATE_LOGFMT_FIELD_EXPAND_INNER 展开(## 拼接)
UE_PRIVATE_LOGFMT_STRIP_UE_PRIVATE_LOGFMT_NAMED_FIELD ::UE::Logging::Private::CheckFieldName("Name"), PlayerName
// ↑ 通过 ## 拼接,UE_PRIVATE_LOGFMT_STRIP_ 与 UE_PRIVATE_LOGFMT_NAMED_FIELD 拼成一个宏名
// 第六步:UE_PRIVATE_LOGFMT_STRIP_UE_PRIVATE_LOGFMT_NAMED_FIELD 展开(定义为空)
::UE::Logging::Private::CheckFieldName("Name"), PlayerName
// 最终结果
::UE::Logging::Private::CheckFieldName("Name"), PlayerName
完整展开示例(位置参数):
cpp
// 原始代码
UE_LOGFMT(LogTemp, Log, "Position: ({0}, {1})", X, Y);
// 第一步:X 被传入 UE_PRIVATE_LOGFMT_FIELD
UE_PRIVATE_LOGFMT_FIELD(X)
// 第二步:入口宏展开
// Field = X(不带括号)
UE_PRIVATE_LOGFMT_FIELD_EXPAND(UE_PRIVATE_LOGFMT_NAMED_FIELD X)
// ↑ X 不带括号,UE_PRIVATE_LOGFMT_NAMED_FIELD 不被当作函数式宏调用
// ↑ UE_PRIVATE_LOGFMT_NAMED_FIELD 只是一个标识符,与 X 拼在一起
// 第三步:UE_PRIVATE_LOGFMT_FIELD_EXPAND 展开
UE_PRIVATE_LOGFMT_FIELD_EXPAND_INNER(UE_PRIVATE_LOGFMT_NAMED_FIELD X)
// 第四步:UE_PRIVATE_LOGFMT_FIELD_EXPAND_INNER 展开(## 拼接)
UE_PRIVATE_LOGFMT_STRIP_UE_PRIVATE_LOGFMT_NAMED_FIELD X
// ↑ 同样拼接,但后面跟的是 X
// 第五步:UE_PRIVATE_LOGFMT_STRIP_UE_PRIVATE_LOGFMT_NAMED_FIELD 展开(定义为空)
X
// 最终结果
X
宏技巧解析:
| 技巧 | 说明 |
|---|---|
| 函数式宏的条件触发 | MACRO arg 只有当 arg 以 ( 开头时,才会被当作函数式宏调用 |
| 标记保留 | UE_PRIVATE_LOGFMT_NAMED_FIELD 在展开后保留自身作为后续识别的标记 |
| Token 拼接(##) | UE_PRIVATE_LOGFMT_STRIP_ ## __VA_ARGS__ 将前缀与内容拼接成一个 token |
| 空宏定义 | #define UE_PRIVATE_LOGFMT_STRIP_UE_PRIVATE_LOGFMT_NAMED_FIELD 定义为空,用于"消除"标记 |
CheckFieldName 的作用:
cpp
// 源码(StructuredLog.h 第 432-436 行)
template <typename NameType>
inline constexpr TLogFieldName<NameType> CheckFieldName(NameType&& Name)
{
static_assert(TIsArrayOrRefOfType<NameType, ANSICHAR>::Value, "Name must be an ANSICHAR string literal.");
return {(NameType&&)Name};
}
- 编译期检查:确保字段名是
ANSICHAR字符串字面量(如"Name") - 类型包装:返回
TLogFieldName<NameType>,用于后续区分字段名和字段值
FLogFieldCreator 的处理:
cpp
// 展开后的参数传入 LogWithFields 函数
// 命名字段:CheckFieldName("Name"), PlayerName, CheckFieldName("HP"), Health
// 位置参数:X, Y
// FLogFieldCreator::Create 根据参数类型区分处理
template <typename ValueType, typename... FieldArgTypes>
inline static void Create(FLogField* Fields, const ValueType& Value, ...)
{
// 值类型:创建匿名字段
new(Fields) FLogField{nullptr, &Value, FLogField::Write<ValueType>};
}
template <typename NameType, typename ValueType, typename... FieldArgTypes>
inline static void Create(FLogField* Fields, TLogFieldName<NameType> Name, const ValueType& Value, ...)
{
// 名字 + 值:创建命名字段
new(Fields) FLogField{Name.Name, &Value, FLogField::Write<ValueType>};
}
完整数据流图:
UE_LOGFMT(LogTemp, Warning, "Player '{Name}' HP: {HP}", ("Name", N), ("HP", H))
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ UE_PRIVATE_LOGFMT_FIELDS(("Name", N), ("HP", H)) │
│ → , UE_PRIVATE_LOGFMT_FIELD(("Name", N)), UE_PRIVATE_LOGFMT_FIELD(("HP", H)) │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ UE_PRIVATE_LOGFMT_FIELD(("Name", N)) │
│ → UE_PRIVATE_LOGFMT_FIELD_EXPAND(UE_PRIVATE_LOGFMT_NAMED_FIELD ("Name", N)) │
│ → ... → CheckFieldName("Name"), N │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ LogWithFields(Category, Log, CheckFieldName("Name"), N, │
│ CheckFieldName("HP"), H) │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ FLogFieldCreator::Create 识别 TLogFieldName 类型 │
│ → FLogField{"Name", &N, Write<decltype(N)>} │
│ → FLogField{"HP", &H, Write<decltype(H)>} │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ LogWithFieldArray(Category, Log, Fields, 2) │
│ → 格式化并输出 "Player 'Alice' HP: 100" │
└──────────────────────────────────────────────────────────────────────┘
4. 类型安全的编译期检查
cpp
// 字段值序列化(源码 StructuredLog.h 第 289-293 行)
template <typename ValueType UE_REQUIRES(TModels_V<CInsertable<FCbWriter&>, ValueType>)>
inline void SerializeForLog(FCbWriter& Writer, ValueType&& Value)
{
Writer << (ValueType&&)Value;
}
// 支持的类型包括:
// - 基础类型:int32, float, double, bool
// - 字符串:FString, FName, FText, TCHAR*
// - 自定义类型:实现了 operator<<(FCbWriter&, T) 或 SerializeForLog 的类型
5. 位置参数 vs 命名字段的内部处理
cpp
// 位置参数模式:{0}, {1}, {2}
// 内部按参数顺序索引数组
UE_LOGFMT(LogTemp, Log, "Pos: ({0}, {1})", X, Y);
// 展开为:Fields[0] = X, Fields[1] = Y
// 命名字段模式:{FieldName}
// 内部构建 Name->Value 映射表
UE_LOGFMT(LogTemp, Log, "Pos: ({X}, {Y})", ("X", X), ("Y", Y));
// 展开为:FieldMap["X"] = X, FieldMap["Y"] = Y
6. 16 字段限制的原因
源码定位:
StructuredLog.h第 604-627 行
cpp
// UE_PRIVATE_LOGFMT_FIELDS 宏根据参数数量选择对应的展开宏
#define UE_PRIVATE_LOGFMT_FIELDS(...) \
UE_PRIVATE_LOGFMT_CALL(UE_JOIN(UE_PRIVATE_LOGFMT_FIELDS_, UE_PRIVATE_LOGFMT_COUNT(__VA_ARGS__)), (__VA_ARGS__))
// 定义了 0-16 个参数的展开宏
#define UE_PRIVATE_LOGFMT_FIELDS_0()
#define UE_PRIVATE_LOGFMT_FIELDS_1(A) , UE_PRIVATE_LOGFMT_FIELD(A)
#define UE_PRIVATE_LOGFMT_FIELDS_2(A,B) , UE_PRIVATE_LOGFMT_FIELD(A), UE_PRIVATE_LOGFMT_FIELD(B)
// ... 最多到 16 个
// 计数宏(通过参数位置计数)
#define UE_PRIVATE_LOGFMT_COUNT(...) \
UE_PRIVATE_LOGFMT_CALL(UE_PRIVATE_LOGFMT_COUNT_IMPL, (_, ##__VA_ARGS__, 16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0))
#define UE_PRIVATE_LOGFMT_COUNT_IMPL(_, A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P, Count, ...) Count
为什么限制 16 个:
- 预处理器宏无法实现真正的递归,必须手动定义每个参数数量的展开版本
- 16 个字段对于绝大多数日志场景已经足够
- 超过 16 个字段时使用
UE_LOGFMT_EX+UE_LOGFMT_FIELD/UE_LOGFMT_VALUE包装
7. 完整执行流程图
UE_LOGFMT(LogTemp, Warning, "Player {Name} HP: {HP}", ("Name", N), ("HP", H))
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 0. 宏参数展开(UE_PRIVATE_LOGFMT_FIELDS) │
│ ("Name", N), ("HP", H) │
│ → , CheckFieldName("Name"), N, CheckFieldName("HP"), H │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 1. 编译期:if constexpr 检查 Verbosity vs CompileTime上限 │
│ - Warning <= All? → 通过,生成代码 │
│ - 若不通过 → 整个调用被裁剪,零开销 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. 创建静态日志记录 │
│ - FStaticLogDynamicData LOG_Dynamic; │
│ - FStaticLogRecord LOG_Static{Format, File, Line, ...}; │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. 运行期:LogTemp.IsSuppressed(Warning) 检查 │
│ - 被抑制 → return,不执行后续 │
│ - 未抑制 → 继续 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. LogWithFields 构造字段: │
│ - FLogFieldCreator::Create 识别 TLogFieldName 类型 │
│ - FLogField{"Name", &N, Write<T>} │
│ - FLogField{"HP", &H, Write<T>} │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 5. LogWithFieldArray 格式化: │
│ - 解析 "Player {Name} HP: {HP}" │
│ - 序列化字段值到 FCbWriter │
│ - 替换占位符生成:"Player Alice HP: 100" │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 6. 输出:FMsg::LogV() → GLog/GWarn → OutputDevices │
└─────────────────────────────────────────────────────────────┘
8. 为什么推荐 UE_LOGFMT?
| 维度 | UE_LOG | UE_LOGFMT | 源码依据 |
|---|---|---|---|
| 类型检查 | 运行时 | 编译期模板 | CheckFieldName 的 static_assert |
| 格式错误 | 运行时崩溃 | 编译失败 | 模板参数推导 |
| 字段解析 | printf 顺序依赖 | 名称/位置映射 | FLogFieldCreator::Create 重载 |
| 扩展性 | 固定 | 可添加元数据 | FLogRecord 结构、FStaticLogRecord |
| 序列化 | 字符串格式化 | 二进制 CompactBinary | FCbWriter、SerializeForLog |
日志分类声明/定义
| 宏 | 放置位置 | 说明 |
|---|---|---|
DECLARE_LOG_CATEGORY_EXTERN(Name, DefaultVerbosity, CompileTimeVerbosity) |
.h |
声明全局分类对象;两组 verbosity 是模板参数(编译期常量)。 |
DEFINE_LOG_CATEGORY(Name) |
某个 .cpp |
与 DECLARE_LOG_CATEGORY_EXTERN 配对,生成实体。 |
DEFINE_LOG_CATEGORY_STATIC(Name, DefaultVerbosity, CompileTimeVerbosity) |
.cpp 文件顶部 |
文件内静态分类;适合小范围模块。 |
Verbosity 级别与标志位
源码定位:
Engine/Source/Runtime/Core/Public/Logging/LogVerbosity.h
基础级别(从严到宽)
Fatal:致命错误,通常会触发崩溃/中断。Error:错误。Warning:警告。Display:默认更偏“给人看的重要信息”。Log:常规信息。Verbose/VeryVerbose:大量细节信息,通常只在专项排查时打开。
额外标志位(常被忽略但很有用)
ELogVerbosity::Type 还包含若干 bit flag(例如 SetColor、BreakOnLog、VerbosityMask)。
实战建议:如果你要写“带 break/only/reset 的运行期调试命令”,不要只记住级别名,还要记住日志系统内部用 token 解析这些控制语义(见“运行期控制”一节)。
从 UE_LOG 到输出设备:完整链路
这一节解释“Display/Log 具体输出到哪”的根因:不是一句话能概括的规则,而是由输出设备链 +
FMsg::LogV的路由决定。
关键链路(UE 5.7 验证)
UE_LOG/UE_CLOG宏展开(LogMacros.h)- 进入
UE::Logging::Private::BasicLog...(StructuredLog.cpp) - 进入
FMsg::LogV(...)(LogMacros.cpp) FMsg::LogV根据 verbosity 选择GWarn或GLogGLog(CoreGlobals.h)实际是FOutputDeviceRedirector单例,负责广播到所有 output device- 平台层在启动时安装 output devices(例如文件、控制台、debug 输出)(
GenericPlatformOutputDevices.cpp)
解释:为什么 Display 往往“更像控制台输出”
在 UE 5.7 的 FMsg::LogV 里,Error/Warning/Display/SetColor 这几类会优先走 GWarn(若不为空),其它级别默认走 GLog。但 GWarn 的实现常会继续把日志转发到 GLog(避免环路时除外)。
因此结论是:
- “Display 一定上控制台、Log 一定只进文件”这种说法过于绝对。
- 更可靠的说法是:最终去哪取决于当前安装了哪些
FOutputDevice,以及它们对不同 verbosity 的处理策略。
编译期与运行期控制:优先级与入口
1)编译期:全局裁剪(COMPILED_IN_MINIMUM_VERBOSITY)
- 入口宏:
COMPILED_IN_MINIMUM_VERBOSITY(LogMacros.h) - 作用:作为全局编译期上限,高于该级别的日志会被
if constexpr直接裁掉。 - UE 5.7 关键边界:源码中明确限制 只能在 monolithic build 中定义(非 monolithic 会
#error)。
2)编译期:分类级别上限(CompileTimeVerbosity)
- 入口:
DECLARE_LOG_CATEGORY_EXTERN(..., DefaultVerbosity, CompileTimeVerbosity)第三个参数 - 作用:给单个分类设置编译期上限;高于该级别的该分类日志也会被裁剪。
3)运行期:ini 配置([Core.Log])
- 入口:
[Core.Log]配置段(由FLogSuppressionInterface读取并解析)。 - 语法(概念):
类别名=token 串(例如 verbosity、break、only 等)。
源码定位:
Engine/Source/Runtime/Core/Private/Logging/LogSuppressionInterface.cpp(搜索Core.Log)
4)运行期:命令行参数(-LogCmds=...)
- 入口:
-LogCmds="..."(可多次传入) - UE 5.7 边界:该解析逻辑在源码中被
#if !UE_BUILD_SHIPPING包住,Shipping 会编译掉。
源码定位:
LogSuppressionInterface.cpp(搜索-LogCmds=)
5)运行期:控制台命令(Log ...)
- 入口:控制台/Exec 命令
Log - 常用子命令(以源码文案为准):
Log list/Log list <substr>Log resetLog <cat> off|on|none|error|warning|display|log|verbose|all|defaultLog <cat> breakLog <cat> only
源码定位:
LogSuppressionInterface.cpp(搜索FParse::Command(&Cmd, TEXT("LOG")))
日志系统完整工作流程
系统初始化与配置加载流程
运行时日志控制流程
配置热重载流程
UE_LOG 宏执行流程(从宏到输出)
关键数据结构关系
优先级与覆盖关系
ProcessConfigAndCommandLine 深度解析
函数概述
ProcessConfigAndCommandLine 是 UE 日志系统的核心初始化函数,负责在引擎启动时加载并应用所有日志配置。它是连接编译期设置和运行期控制的关键桥梁。
源码定位:
Engine/Source/Runtime/Core/Private/Logging/LogSuppressionInterface.cpp(搜索ProcessConfigAndCommandLine)
调用时机
- 引擎启动时:在
FEngineLoop::PreInit()阶段被调用 - 配置热重载时:当
Engine.ini的[Core.Log]节被修改时自动重新调用
执行流程详解
步骤 1:处理待注册的日志类别
cpp
// 将所有在系统初始化前构造的静态日志类别注册到系统中
ReverseAssociations.Reserve(PendingAssociations.Num());
for (FLogCategoryBase* Category : PendingAssociations)
{
AssociateSuppressImpl(Category);
}
PendingAssociations.Empty();
为什么需要延迟注册?
- 静态日志类别(如
DEFINE_LOG_CATEGORY_STATIC)可能在main()之前就构造 - 此时配置文件和命令行参数还未加载
- 需要先放入
PendingAssociations队列,等待统一处理
步骤 2:加载配置文件(Engine.ini)
cpp
// 从 Engine.ini 的 [Core.Log] 节读取配置
const FConfigSection* RefTypes = GConfig->GetSection(TEXT("Core.Log"), false, GEngineIni);
if (RefTypes != NULL)
{
for(FConfigSectionMap::TConstIterator It(*RefTypes); It; ++It)
{
// 格式:LogTemp=Verbose
ProcessCmdString(It.Key().ToString() + TEXT(" ") + It.Value().GetValue(), true);
}
}
配置文件示例(Engine.ini 或 DefaultEngine.ini):
ini
[Core.Log]
; 全局默认级别
Global=Warning
; 特定类别配置
LogTemp=Verbose
LogShaders=Display Break
LogNet=Log
LogAnimation=Off
; 组合命令(启动时全局关闭,但保留特定类别)
BootGlobal=None
LogCore=Warning
步骤 3:处理命令行参数(非 Shipping 版本)
cpp
#if !UE_BUILD_SHIPPING
FString CmdLine(FCommandLine::Get());
FString LogCmds(TEXT("-LogCmds="));
// 支持多个 -LogCmds 参数
while (1)
{
FString Cmds;
if (!FParse::Value(*CmdLine, *LogCmds, Cmds, false))
{
break;
}
ProcessCmdString(Cmds, true);
// 移除已处理的命令,继续查找下一个
// ...
}
#endif
命令行示例:
bash
# 单个命令
MyGame.exe -LogCmds="LogTemp verbose"
# 多个命令(逗号分隔)
MyGame.exe -LogCmds="LogTemp verbose, LogShaders off, LogNet display"
# 多个 -LogCmds 参数
MyGame.exe -LogCmds="global none" -LogCmds="LogTemp verbose"
# 组合使用(先全局关闭,再开启特定类别)
MyGame.exe -LogCmds="global none, LogShaders verbose, LogNet log"
步骤 4:环境变量支持
cpp
// 支持通过 UE-CmdLineArgs 环境变量设置
// 环境变量中的参数会先于命令行参数处理
int32 IndexOfEnv = CmdLine.Find(TEXT("-EnvAfterHere"));
if (IndexOfEnv != INDEX_NONE)
{
FString CmdLineEnv = CmdLine.Mid(IndexOfEnv);
// 处理环境变量中的 -LogCmds
// ...
}
环境变量示例(Windows):
cmd
REM 设置环境变量
set UE-CmdLineArgs=-LogCmds="LogTemp verbose, LogShaders display"
REM 启动游戏(会自动读取环境变量)
MyGame.exe
环境变量示例(Linux/Mac):
bash
# 设置环境变量
export UE-CmdLineArgs="-LogCmds=\"LogTemp verbose, LogShaders display\""
# 启动游戏
./MyGame
步骤 5:应用到所有类别
cpp
// 最后,将启动配置应用到所有已注册的类别
for (TMultiMap<FName, FLogCategoryBase*>::TIterator It(ReverseAssociations); It; ++It)
{
SetupSuppress(It.Value(), It.Key());
}
步骤 6:注册配置热重载回调
cpp
if (!bInitialized)
{
FCoreDelegates::TSOnConfigSectionsChanged().AddLambda([this](const FString& IniFilename, const TSet<FString>& SectionNames)
{
if (IniFilename == GEngineIni && SectionNames.Contains(TEXT("Core.Log")))
{
ProcessConfigAndCommandLine(); // 配置文件改变时重新加载
}
});
}
热重载特性:
- 在编辑器中修改
Engine.ini的[Core.Log]节后,无需重启即可生效 - 适合在开发过程中快速调整日志级别
实战使用方式
方式 1:配置文件控制(推荐用于项目默认设置)
适用场景:
- 设置项目的默认日志级别
- 团队共享的日志配置
- 不同构建配置的日志策略
配置位置:
Config/DefaultEngine.ini(项目默认配置)Saved/Config/Windows/Engine.ini(本地覆盖配置)
示例配置:
ini
[Core.Log]
; === 开发阶段:详细日志 ===
Global=Log
LogTemp=Verbose
LogGameplay=Verbose
LogAnimation=Display
LogPhysics=Warning
; === 性能分析:关闭大部分日志 ===
; Global=Warning
; LogStats=Verbose
; LogRendering=Display
; === 调试特定模块 ===
; Global=None
; LogShaders=VeryVerbose Break
方式 2:命令行参数(推荐用于临时调试)
适用场景:
- 快速测试特定日志级别
- CI/CD 自动化测试
- 不修改配置文件的临时调试
使用示例:
bash
# 场景 1:调试着色器编译问题
MyGame.exe -LogCmds="LogShaders verbose, LogMaterial verbose"
# 场景 2:性能分析(关闭大部分日志)
MyGame.exe -LogCmds="global warning, LogStats verbose"
# 场景 3:网络调试
MyGame.exe -LogCmds="LogNet verbose, LogNetTraffic log, LogNetPlayerMovement verbose"
# 场景 4:只看特定类别(使用 only 命令)
MyGame.exe -LogCmds="LogTemp only"
# 场景 5:启用断点调试
MyGame.exe -LogCmds="LogGameplay verbose break"
方式 3:环境变量(推荐用于开发环境配置)
适用场景:
- 个人开发环境的持久化配置
- 避免修改项目配置文件
- 团队成员各自的调试偏好
Windows 配置:
cmd
REM 临时设置(当前会话)
set UE-CmdLineArgs=-LogCmds="LogTemp verbose"
REM 永久设置(系统环境变量)
setx UE-CmdLineArgs "-LogCmds=\"LogTemp verbose, LogShaders display\""
Linux/Mac 配置:
bash
# 临时设置(当前会话)
export UE-CmdLineArgs="-LogCmds=\"LogTemp verbose\""
# 永久设置(添加到 ~/.bashrc 或 ~/.zshrc)
echo 'export UE-CmdLineArgs="-LogCmds=\"LogTemp verbose\""' >> ~/.bashrc
方式 4:运行时控制台命令(推荐用于实时调试)
适用场景:
- 游戏运行时动态调整日志
- 不重启游戏的快速测试
- 编辑器 PIE(Play In Editor)调试
控制台命令示例:
# 列出所有日志类别
log list
# 列出包含 "Shader" 的类别
log list Shader
# 设置特定类别级别
log LogTemp verbose
log LogShaders display
# 只显示特定类别(关闭其他所有)
log LogTemp only
# 重置所有类别到默认值
log reset
# 全局设置
log global warning
# 启用断点
log LogGameplay break
配置优先级总结
从低到高的优先级顺序:
- 编译期默认值(
DECLARE_LOG_CATEGORY_EXTERN的DefaultVerbosity参数) - BootGlobal(配置文件或命令行中的
Global设置,在启动时处理) - 配置文件(
Engine.ini的[Core.Log]节) - 环境变量(
UE-CmdLineArgs中的-LogCmds) - 命令行参数(直接传入的
-LogCmds) - 运行时控制台命令(
log命令)
重要限制:
- 所有运行期设置都不能突破
CompileTimeVerbosity的限制 - Shipping 构建会禁用命令行参数和控制台命令
常见使用模式
模式 1:开发阶段 - 详细日志
ini
[Core.Log]
Global=Log
LogTemp=Verbose
LogGameplay=Verbose
LogAI=Verbose
模式 2:性能分析 - 最小日志
ini
[Core.Log]
Global=Error
LogStats=Verbose
LogCore=Warning
模式 3:调试特定模块 - 只看关键日志
bash
# 命令行方式
MyGame.exe -LogCmds="global none, LogShaders verbose, LogMaterial verbose"
ini
# 配置文件方式
[Core.Log]
BootGlobal=None
LogShaders=Verbose
LogMaterial=Verbose
模式 4:CI/CD 自动化测试
bash
# 只记录错误和警告,减少日志文件大小
MyGame.exe -LogCmds="global warning" -unattended -nullrhi
调试技巧
技巧 1:验证配置是否生效
在游戏启动后,使用控制台命令查看当前配置:
log list
输出示例:
LogTemp Verbose
LogShaders Display
LogNet Log
LogAnimation Warning
技巧 2:临时提升日志级别排查问题
# 运行时提升级别
log LogGameplay verbose
# 复现问题...
# 恢复默认
log LogGameplay default
技巧 3:使用 only 命令减少噪音
# 只看着色器日志
log LogShaders only
# 调试完成后重置
log reset
技巧 4:组合使用断点和日志
ini
[Core.Log]
LogGameplay=Verbose Break
当 LogGameplay 输出日志时,会触发断点(需要调试器附加)。
注意事项
-
Shipping 构建限制:
-LogCmds命令行参数在 Shipping 中被编译掉- 控制台命令也不可用
- 只能通过配置文件控制
-
CompileTimeVerbosity 限制:
- 运行期无法突破编译期设置的上限
- 如果需要更高级别的日志,必须重新编译
-
配置文件路径:
Config/DefaultEngine.ini:项目默认配置(会提交到版本控制)Saved/Config/Windows/Engine.ini:本地覆盖配置(不提交)
-
热重载限制:
- 只有
Engine.ini的[Core.Log]节支持热重载 - 其他配置文件的修改需要重启
- 只有
可编译示例(最小集)
目标:每段示例都能放进 UE5 C++ 工程并通过编译(细节如 include、类上下文都补齐)。
示例 1:最小 UE_LOG
cpp
#include "CoreMinimal.h"
void LogHello()
{
UE_LOG(LogTemp, Warning, TEXT("Hello UE_LOG"));
}
示例 2:自定义分类(Header/CPP 分离)
cpp
// MyGameLogs.h
#pragma once
#include "CoreMinimal.h"
DECLARE_LOG_CATEGORY_EXTERN(LogMyGameplay, Log, All);
cpp
// MyGameLogs.cpp
#include "MyGameLogs.h"
DEFINE_LOG_CATEGORY(LogMyGameplay);
示例 3:用 UE_LOG_ACTIVE/UE_CLOG 避免热路径开销
cpp
#include "CoreMinimal.h"
#include "MyGameLogs.h"
static FString BuildExpensiveString();
void TickHotPath(float DeltaSeconds)
{
if (UE_LOG_ACTIVE(LogMyGameplay, VeryVerbose))
{
const FString S = BuildExpensiveString();
UE_LOG(LogMyGameplay, VeryVerbose, TEXT("Detail: %s"), *S);
}
UE_CLOG(DeltaSeconds > 0.1f, LogMyGameplay, Warning, TEXT("Big delta: %.3f"), DeltaSeconds);
}
示例 4:UE_SUPPRESS 的真实语义(guard + 作用域 override)
cpp
#include "CoreMinimal.h"
#include "MyGameLogs.h"
void DoSyncWork();
void MaybeDoSyncWork()
{
UE_SUPPRESS(LogMyGameplay, VeryVerbose,
{
// 只有当 LogMyGameplay 在 VeryVerbose 未被 suppress 时,才会执行到这里。
DoSyncWork();
UE_LOG(LogMyGameplay, VeryVerbose, TEXT("Sync done"));
});
}
示例 5:结构化日志(UE_LOGFMT)
cpp
#include "CoreMinimal.h"
#include "Logging/StructuredLog.h"
#include "MyGameLogs.h"
void LogStructured(const FString& Name, int32 ErrorCode)
{
UE_LOGFMT(LogMyGameplay, Warning, "Loading '{Name}' failed with error {Error}",
("Name", Name), ("Error", ErrorCode));
}
常见坑与边界条件
- 示例"能不能编译"是第一准则:凡是
GetName()/GetWorld()/Target等上下文依赖,都要明确"这是哪个类的成员函数"或通过参数传入。 - 别把输出目的地当成 verbosity 的固定属性:真实决定因素是
FMsg::LogV的路由 +GLog上挂载的 output devices。 - Shipping 行为差异要显式标注:例如
-LogCmds=在 UE 5.7 源码中被!UE_BUILD_SHIPPING门控。 - 全局编译期裁剪不是随便加宏就行:
COMPILED_IN_MINIMUM_VERBOSITY在 UE 5.7 源码里要求 monolithic 才能自定义。 UE_LOG_REF虽然存在但标注 DO NOT USE:如果你在项目里见到它,建议逐步迁移到UE_LOG。
常见问题排查
问题 1:为什么我的 Verbose/VeryVerbose 日志不打印?
症状:
cpp
UE_LOG(LogControlFlows, Verbose, TEXT("This doesn't show up!"));
根因分析:
- 检查日志类别的声明(以 UE 5.7 ControlFlows 插件为例):
cpp
// Engine/Plugins/Experimental/ControlFlows/Source/ControlFlows/Public/ControlFlows.h
DECLARE_LOG_CATEGORY_EXTERN(LogControlFlows, Log, All);
// ^^^ ^^^
// | CompileTimeVerbosity = All(编译期上限)
// DefaultVerbosity = Log(默认运行期级别)
-
理解两个 Verbosity 参数的作用:
DefaultVerbosity = Log:启动后默认只输出Log及以上级别(Fatal/Error/Warning/Display/Log)CompileTimeVerbosity = All:编译期允许所有级别(不会被裁剪)
-
为什么 Verbose 不打印:
Verbose比Log更详细(级别更低)- 运行期默认会被
FLogSuppressionInterface过滤掉 - 虽然编译期没有裁剪(
CompileTimeVerbosity = All),但运行期被抑制了
解决方案:
方案 1 - 运行时提升(推荐用于调试)
控制台命令:
log LogControlFlows verbose
或启动参数:
bash
-LogCmds="LogControlFlows verbose"
验证是否生效:
log list ControlFlows
输出示例:
LogControlFlows Verbose
方案 2 - 配置文件(持久化)
在 DefaultEngine.ini 或 Engine.ini 中添加:
ini
[Core.Log]
LogControlFlows=Verbose
方案 3 - 修改源码默认值(永久生效)
修改声明(需要重新编译插件):
cpp
// 修改前
DECLARE_LOG_CATEGORY_EXTERN(LogControlFlows, Log, All);
// 修改后
DECLARE_LOG_CATEGORY_EXTERN(LogControlFlows, Verbose, All);
// ^^^^^^^
// 提升默认级别到 Verbose
方案 4 - 代码中动态设置
cpp
#include "ControlFlows.h"
void EnableVerboseLogging()
{
UE_SET_LOG_VERBOSITY(LogControlFlows, Verbose);
// 或者
LogControlFlows.SetVerbosity(ELogVerbosity::Verbose);
}
关键要点:
- 编译期 vs 运行期:即使
CompileTimeVerbosity = All允许编译,DefaultVerbosity = Log仍会在运行期过滤掉 Verbose 日志。 - 优先级:配置文件 < 命令行参数 < 运行时控制台命令 < 代码动态设置。
- Shipping 构建限制:
-LogCmds=在 Shipping 构建中会被编译掉(#if !UE_BUILD_SHIPPING)。
实战技巧:
如果你在热路径中使用 Verbose 日志,建议用 UE_LOG_ACTIVE 避免不必要的字符串构造:
cpp
if (UE_LOG_ACTIVE(LogControlFlows, Verbose))
{
FString ExpensiveDebugInfo = BuildComplexString();
UE_LOG(LogControlFlows, Verbose, TEXT("Detail: %s"), *ExpensiveDebugInfo);
}
附录:源码入口索引(UE 5.7)
下面路径以引擎根为参照(更方便在不同安装路径下复用)。
宏定义(文本日志)
UE_LOG/UE_CLOG/UE_LOG_ACTIVE/UE_SUPPRESS/UE_LOG_REF/DECLARE_LOG_CATEGORY_EXTERN/DEFINE_LOG_CATEGORY(_STATIC)/UE_GET_LOG_VERBOSITY/UE_SET_LOG_VERBOSITY- 文件:
Engine/Source/Runtime/Core/Public/Logging/LogMacros.h - 搜索关键字:
#define UE_LOG(、#define UE_CLOG(、#define UE_LOG_ACTIVE(、#define UE_SUPPRESS(、#define UE_LOG_REF(
- 文件:
Verbosity 枚举
ELogVerbosity::Type- 文件:
Engine/Source/Runtime/Core/Public/Logging/LogVerbosity.h - 搜索关键字:
namespace ELogVerbosity
- 文件:
结构化日志
UE_LOGFMT/UE_CLOGFMT/UE_LOG_CONTEXT等- 文件:
Engine/Source/Runtime/Core/Public/Logging/StructuredLog.h - 搜索关键字:
#define UE_LOGFMT(、UE_PRIVATE_LOGFMT
- 文件:
输出链路(从格式化到 OutputDevice)
-
UE::Logging::Private::BasicLog...→FMsg::LogV- 文件:
Engine/Source/Runtime/Core/Private/Logging/StructuredLog.cpp - 搜索关键字:
BasicLog、FMsg::LogV
- 文件:
-
FMsg::LogV(verbosity 路由到GWarn/GLog)- 文件:
Engine/Source/Runtime/Core/Private/Logging/LogMacros.cpp - 搜索关键字:
void FMsg::LogV(
- 文件:
-
GLog定义与单例- 文件:
Engine/Source/Runtime/Core/Public/CoreGlobals.h - 搜索关键字:
#define GLog GetGlobalLogSingleton
- 文件:
-
FOutputDeviceRedirector(广播到输出设备)- 文件:
Engine/Source/Runtime/Core/Public/Misc/OutputDeviceRedirector.h - 搜索关键字:
class FOutputDeviceRedirector
- 文件:
-
平台安装输出设备
- 文件:
Engine/Source/Runtime/Core/Private/GenericPlatform/GenericPlatformOutputDevices.cpp - 搜索关键字:
SetupOutputDevices、AddOutputDevice
- 文件:
运行期抑制系统(ini/命令行/控制台)
FLogSuppressionInterface与ProcessConfigAndCommandLine- 文件:
Engine/Source/Runtime/Core/Private/Logging/LogSuppressionInterface.cpp - 搜索关键字:
Core.Log、-LogCmds=、FParse::Command(&Cmd, TEXT("LOG"))
- 文件:

