
这次我们来看虚幻引擎 C 开发中一个看似基础但至关重要的环节日志输出。对于刚接触 UEC 的开发者尤其是从 Unity 或其他引擎转过来的朋友可能会对虚幻引擎的日志系统感到陌生。它不像简单的printf或std::cout而是有一套更强大、更符合引擎架构的机制。掌握好日志是调试、追踪程序行为和线上问题排查的第一步。本文将聚焦于 UEC 中最核心的两种日志输出方式UE_LOG宏和GEngine-AddOnScreenDebugMessage。我们会直接切入主题不讲空泛的理论而是通过具体的实例让你快速理解它们各自的特点、适用场景以及如何在实际项目中灵活运用。无论你是想将关键信息打印到输出日志窗口还是需要实时在游戏画面中显示调试信息这篇文章都能给你清晰的指引。1. 核心能力速览在深入细节之前我们先通过一个表格快速了解这两种日志方式的核心区别和适用场景帮助你快速做出选择。能力项UE_LOG宏GEngine-AddOnScreenDebugMessage输出目标输出到编辑器/独立运行时的“输出日志”窗口、控制台以及保存到日志文件。直接在游戏运行时的屏幕上绘制文本信息。主要用途程序逻辑调试、性能分析、错误追踪、记录运行时状态。信息持久化可回溯。实时调试、显示变量值、标记物体位置、可视化调试信息。信息随帧消失。显存/性能影响极低主要是文本 I/O 开销。较低但频繁调用或大量文本可能影响渲染性能。是否需要 World 上下文否全局可用。是需要有效的UWorld上下文通常通过GetWorld()获取。信息持久性高。信息会保留在日志窗口和日志文件中除非清空或文件覆盖。低。默认显示时间结束后自动消失或可手动清除。是否支持格式化是支持类似printf的丰富格式化%s,%d,%f等。是支持FString::Printf或FString::Format进行格式化。适合场景所有需要记录和查看的调试、警告、错误信息。游戏运行时需要即时视觉反馈的调试如坐标、血量、状态机切换等。简单来说UE_LOG是你的“黑匣子”和“诊断报告”而AddOnScreenDebugMessage是你的“实时仪表盘”。两者结合使用能极大提升开发效率。2. 适用场景与使用边界了解核心能力后我们来看看具体在什么情况下该用哪一种以及使用时需要注意的边界。UE_LOG的适用场景函数入口/出口追踪记录某个函数是否被调用、调用时的参数。流程逻辑验证在复杂的条件分支或循环中打印状态确认程序流按预期执行。变量值监控在特定时刻如每帧、事件触发时记录关键变量的值。错误与警告报告当检测到非法状态、资源加载失败、网络异常时使用不同日志级别进行报告。性能 profiling配合FScopeLogTime等工具记录代码块的执行时间。发布后问题诊断即使在打包后的游戏中UE_LOG信息也可能被记录到文件如YourGame.log对于排查玩家端问题至关重要。GEngine-AddOnScreenDebugMessage的适用场景实时数据显示在角色头顶显示当前速度、生命值、状态。空间位置调试在物体位置绘制其坐标、名称或自定义标签。输入反馈显示最近按下的按键、鼠标位置等。游戏规则可视化如显示当前得分、倒计时、任务目标。临时提示信息用于快速验证某个逻辑是否在某一帧被触发。使用边界与注意事项性能避免在Tick函数中每帧调用UE_LOG输出大量或复杂的格式化信息尤其是在打包版本中。屏幕信息同样不宜过多过密。信息安全切勿通过日志记录玩家的敏感信息如密码、账号。打包前应审查日志输出移除不必要的调试信息。屏幕信息清理注意屏幕信息的生命周期。未指定Key或使用-1且设置较短显示时间的信息会自动清理。指定了Key的信息需要手动更新或清除否则可能残留。World 上下文有效性调用GEngine-AddOnScreenDebugMessage前必须确保GEngine有效且能获取到正确的UWorld。在游戏未开始或 World 即将销毁时调用可能导致崩溃。3. 环境准备与前置条件在开始编写日志代码之前你需要确保开发环境已经就绪。这不是关于显卡和显存而是关于项目配置和编程环境。虚幻引擎版本本文内容适用于虚幻引擎 4UE4和虚幻引擎 5UE5的大部分版本。不同小版本间 API 基本稳定但建议使用较新的稳定版本如 UE 5.3以获得最佳体验。开发环境Visual StudioWindows 平台推荐使用 Visual Studio 2022并安装“使用 C 的游戏开发”工作负载。Visual Studio Code或Rider for Unreal作为备选或辅助编辑器。C 项目你必须有一个已创建的C 项目。纯蓝图项目无法直接编写 C 代码。如果你只有蓝图项目可以通过编辑器菜单栏的“工具” - “新建 C 类…”来添加一个 C 类这会将项目转换为 C 项目。基础 C 知识你需要了解 C 的基础语法如头文件.h、源文件.cpp、类、函数、宏等概念。项目编译首次添加 C 代码或修改后需要右键点击.uproject文件选择“Generate Visual Studio project files”然后用 Visual Studio 打开生成的.sln文件进行编译。4.UE_LOG宏的详细使用与实例UE_LOG是虚幻引擎日志系统的基石。它的功能强大参数也较多我们一步步拆解。4.1 基本语法与参数解析UE_LOG宏的基本格式如下UE_LOG(LogCategory, Verbosity, Format, ...)LogCategory日志类别。这是一个FLogCategoryBase类型的对象用于对日志进行分类过滤。最常用的是预定义的LogTemp你也可以自定义类别。Verbosity日志冗长级别。决定这条日志的重要性以及在何种输出配置下显示。常见级别有Log普通信息默认显示。Warning警告信息通常用黄色显示表示可能有问题但不影响运行。Error错误信息用红色显示表示发生了需要关注的错误。Fatal致命错误用红色显示打印后通常会触发断言并中断程序在开发版本中。Display与Log类似但更倾向于给开发者显示。Verbose,VeryVerbose详细和非常详细的信息通常在需要深度调试时开启。Format格式化字符串。与 C 语言的printf风格类似但使用的是TCHAR字符串通常用TEXT()宏包裹。...可变参数对应格式化字符串中的占位符。4.2 基础使用实例让我们在一个常见的AActor派生类的BeginPlay函数中尝试几种基本的UE_LOG。// 在 YourActor.cpp 的 BeginPlay 函数中 void AYourActor::BeginPlay() { Super::BeginPlay(); // 1. 最简单的日志输出 UE_LOG(LogTemp, Log, TEXT(Actor %s 已开始播放。), *GetName()); // 2. 输出变量值整数浮点数 int32 CurrentScore 100; float Health 85.5f; UE_LOG(LogTemp, Display, TEXT(当前分数%d, 生命值%.1f), CurrentScore, Health); // 3. 输出 FVector 位置 FVector ActorLocation GetActorLocation(); UE_LOG(LogTemp, Verbose, TEXT(Actor 位置X%.2f, Y%.2f, Z%.2f), ActorLocation.X, ActorLocation.Y, ActorLocation.Z); // 4. 警告和错误 if (CurrentScore 0) { UE_LOG(LogTemp, Warning, TEXT(分数异常%d), CurrentScore); } if (!IsValid(this)) // 一个不可能发生的例子仅作演示 { UE_LOG(LogTemp, Error, TEXT(Actor 指针无效)); // 致命错误会中断 // UE_LOG(LogTemp, Fatal, TEXT(发生致命错误)); } }编译并运行后在编辑器里如何查看在虚幻编辑器Editor中运行游戏PIE然后打开“输出日志”窗口方法1菜单栏Window-Developer Tools-Output Log。方法2快捷键CtrlShiftO字母 O。你将在输出日志中看到类似下面的信息LogTemp: Actor YourActor_2 已开始播放。 LogTemp: Display: 当前分数100, 生命值85.5 LogTemp: Verbose: Actor 位置X120.34, Y45.67, Z0.00注意Verbose级别的日志默认可能不显示你需要在输出日志窗口的过滤器中勾选Verbose或All。4.3 自定义日志类别使用LogTemp很方便但在大型项目中所有日志混在一起难以筛选。自定义日志类别是更好的实践。步骤1在全局头文件中声明通常在项目的主要头文件如YourProject.h或一个专门的日志头文件中声明。// YourProject.h #pragma once #include CoreMinimal.h // 声明一个自定义的日志类别 DECLARE_LOG_CATEGORY_EXTERN(LogYourProject, Log, All);步骤2在对应源文件中定义在项目的主要源文件如YourProject.cpp中定义这个类别。// YourProject.cpp #include YourProject.h #include Modules/ModuleManager.h IMPLEMENT_PRIMARY_GAME_MODULE(FDefaultGameModuleImpl, YourProject, YourProject); // 定义日志类别 DEFINE_LOG_CATEGORY(LogYourProject);步骤3在代码中使用现在你可以在任何包含YourProject.h的文件中使用LogYourProject了。// 在某个类的.cpp文件中 #include YourProject.h void UYourGameInstance::Init() { Super::Init(); UE_LOG(LogYourProject, Log, TEXT(游戏实例初始化。)); UE_LOG(LogYourProject, Warning, TEXT(配置文件未找到使用默认值。)); }在输出日志中你可以通过过滤器只显示LogYourProject类别的信息从而快速聚焦于你的游戏模块日志。4.4 日志配置与过滤你可以在DefaultEngine.ini配置文件中控制日志行为。; DefaultEngine.ini [Core.Log] ; 全局默认日志级别 GlobalVerbose ; 设置特定类别的日志级别 LogYourProjectVerbose LogTempWarning ; 将 LogTemp 的默认显示级别提高到 Warning屏蔽 Log 级别 ; 将日志同时输出到文件打包后也有效 LogYourGame.log配置后只有Warning及以上级别的LogTemp日志才会显示而LogYourProject的Verbose级别日志也会显示。5.GEngine-AddOnScreenDebugMessage的详细使用与实例当你想在游戏画面上直接看到信息时屏幕调试信息是你的首选。5.1 基本语法与参数解析该函数的常用重载形式如下GEngine-AddOnScreenDebugMessage( Key, // int32信息的唯一标识键用于后续更新或删除。使用 -1 表示无需管理。 TimeToDisplay, // float信息在屏幕上显示的持续时间秒。 Color, // FColor信息的显示颜色。 DebugMessage, // const FString要显示的字符串信息。 bNewerOnTop, // bool是否将新信息显示在顶部默认为 true。 TextScale // const FVector2D文字的缩放默认为 FVector2D::UnitVector。 );Key核心参数。如果指定一个非负整数如0,1,100那么后续用相同Key调用此函数会更新这条信息而不是创建一条新的。这对于显示持续更新的值如帧率、坐标非常有用。如果设为-1则每次调用都会创建一条独立的新信息。TimeToDisplay显示时间。注意对于指定了Key的信息这个时间会被重置。如果你想让它永久显示直到手动清除可以设置一个很大的值如9999.0f。Color颜色使用FColor类型例如FColor::Red,FColor::Green,FColor::Yellow,FColor::White。DebugMessage要显示的文本。通常使用FString::Printf来格式化。bNewerOnTop控制信息堆叠顺序。true时新信息在上方。TextScale控制字体大小FVector2D(1.0f, 1.0f)是默认大小。5.2 基础使用实例假设我们在一个角色的Tick函数中显示其位置和速度。// 在 AYourCharacter.cpp 的 Tick 函数中 void AYourCharacter::Tick(float DeltaTime) { Super::Tick(DeltaTime); // 确保 GEngine 有效 if (GEngine) { // 示例1显示永久性标题使用 Key0长时间显示 GEngine-AddOnScreenDebugMessage( 0, // Key 10.0f, // 每帧都重置为10秒所以近乎永久 FColor::Cyan, // 青色 TEXT( 玩家角色调试信息 ), true, FVector2D(1.2f, 1.2f) // 稍微放大标题 ); // 示例2显示实时更新的位置信息使用 Key1 FVector Location GetActorLocation(); GEngine-AddOnScreenDebugMessage( 1, // Key 0.02f, // 显示时间很短但因为Key相同会每帧更新所以看起来是连续的 FColor::Yellow, FString::Printf(TEXT(位置: X%.1f, Y%.1f, Z%.1f), Location.X, Location.Y, Location.Z), true ); // 示例3显示实时更新的速度信息使用 Key2 FVector Velocity GetVelocity(); float Speed Velocity.Size(); GEngine-AddOnScreenDebugMessage( 2, 0.02f, FColor::Green, FString::Printf(TEXT(速度: %.1f (X%.1f, Y%.1f, Z%.1f)), Speed, Velocity.X, Velocity.Y, Velocity.Z), true ); // 示例4一次性提示信息使用 Key-1 if (bJustCollectedItem) { GEngine-AddOnScreenDebugMessage( -1, // Key-1每次都是新消息 3.0f, // 显示3秒后消失 FColor::Orange, TEXT(拾取了能量晶体), true ); bJustCollectedItem false; // 重置标志 } } }运行游戏你将在屏幕左上角看到持续更新的位置、速度信息以及触发事件时出现的临时提示。5.3 清除屏幕信息对于用特定Key创建的信息如果你希望它提前消失可以使用GEngine-RemoveOnScreenDebugMessage(Key)。// 当角色死亡时清除所有调试信息 void AYourCharacter::OnDeath() { if (GEngine) { GEngine-RemoveOnScreenDebugMessage(0); // 清除标题 GEngine-RemoveOnScreenDebugMessage(1); // 清除位置信息 GEngine-RemoveOnScreenDebugMessage(2); // 清除速度信息 // 或者如果你想清除所有屏幕信息慎用 // GEngine-ClearOnScreenDebugMessages(); } }6. 两种日志方式的结合与高级技巧在实际项目中我们往往需要将两者结合并运用一些技巧来提升调试效率。6.1 条件编译与开发/发布版本控制我们通常不希望调试日志和屏幕信息出现在最终发布的游戏中。虚幻引擎提供了方便的宏。// 使用 UE_LOG 的条件编译版本 UE_LOG(LogYourProject, Verbose, TEXT(这是一条详细日志在开发版本中可见。)); // 在 Shipping 等发布配置中Verbose 级别的日志默认不会被编译进去。 // 使用 ensure 宏进行断言并记录 // 如果条件为 false会在开发版本中触发一次性的警告日志和调用堆栈在发布版本中无影响。 ensureMsgf(MyPointer ! nullptr, TEXT(MyPointer 为空但本应有效)); // 屏幕信息也可以包装在 #if WITH_EDITOR 或 #if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) 中 #if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) // 非 Shipping 和非 Test 配置 if (GEngine) { GEngine-AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT(这是开发调试信息)); } #endif6.2 创建辅助函数或宏为了避免重复代码可以创建自己的调试输出函数。// 在你的游戏实例或工具类头文件中 #pragma once #include CoreMinimal.h #include Engine/Engine.h class YOURPROJECT_API FDebugHelper { public: // 同时输出到日志和屏幕仅开发版本 static void PrintDebug(const FString Message, float DisplayTime 5.0f, FColor Color FColor::White) { UE_LOG(LogYourProject, Log, TEXT(%s), *Message); #if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) if (GEngine) { GEngine-AddOnScreenDebugMessage(-1, DisplayTime, Color, Message); } #endif } // 带格式化版本的屏幕日志输出 templatetypename... TArgs static void PrintDebugF(const FString Format, TArgs... Args) { FString Message FString::Printf(*Format, ForwardTArgs(Args)...); PrintDebug(Message); } }; // 使用示例 FDebugHelper::PrintDebug(TEXT(角色进入警戒状态)); FDebugHelper::PrintDebugF(TEXT(敌人 %s 的生命值%d), *Enemy-GetName(), Enemy-Health);6.3 性能计时结合FScopeLogTime和UE_LOG可以方便地测量代码块耗时。#include HAL/PlatformTime.h void ExpensiveFunction() { // 创建一个作用域计时器析构时自动输出耗时 FScopeLogTime LogTime(*FString::Printf(TEXT(ExpensiveFunction 执行耗时)), nullptr, FScopeLogTime::ScopeLog_Seconds); // 或者手动计时 double StartTime FPlatformTime::Seconds(); // ... 执行一些耗时的操作 ... double EndTime FPlatformTime::Seconds(); double Duration EndTime - StartTime; UE_LOG(LogYourProject, Verbose, TEXT(手动计时耗时 %.4f 秒), Duration); }7. 资源占用与性能观察日志输出本身对性能的影响微乎其微但不当使用仍可能成为瓶颈。UE_LOG性能考量格式化开销复杂的字符串格式化尤其是涉及多次FString转换在频繁调用时如每帧会产生开销。对于高频日志考虑先判断日志级别是否启用。磁盘 I/O如果配置了输出到文件大量日志写入可能影响磁盘性能。在性能关键路径上应避免。检查日志级别UE_LOG宏内部会检查当前日志类别和级别的启用状态。如果该级别日志被禁用如在DefaultEngine.ini中设置为NoLogging则格式化参数甚至不会被求值。这是一个优化点。AddOnScreenDebugMessage性能考量渲染开销每一行屏幕文本都是一个独立的绘制调用。数十行文本影响不大但成百上千行可能会对渲染线程造成压力。字符串构建与UE_LOG类似在Tick中频繁构建复杂的FString会产生 CPU 开销。最佳实践只在需要时显示屏幕信息并尽量使用Key来更新已有文本而不是创建新的。在性能分析时可以观察STAT_GPU和STAT_SLATE来了解 UI 渲染开销。如何观察影响使用控制台命令stat unit查看帧时间。使用stat slate查看 Slate UI包括调试文本的渲染性能。在输出日志中开启LogSlate或LogSlateD3D类别Verbose 级别可以查看详细的文本渲染日志但信息量巨大仅用于深度调试。8. 常见问题与排查方法在使用日志系统时你可能会遇到一些问题。下表列出了常见问题及解决方法。问题现象可能原因排查方式解决方案UE_LOG无任何输出1. 日志级别设置过高。2. 输出日志窗口过滤器未勾选对应类别或级别。3. 代码未被编译或执行。1. 检查DefaultEngine.ini中的[Core.Log]配置。2. 在输出日志窗口勾选All或对应类别。3. 在代码中设置断点或添加更明显的日志如Fatal确认执行。1. 将Global或对应LogCategory设置为Verbose。2. 确保输出日志窗口正确打开和过滤。3. 确认项目已成功编译且代码逻辑会被触发。屏幕调试信息不显示1.GEngine为空或无效。2. 在BeginPlay之前或EndPlay之后调用。3. 信息被后续同Key信息覆盖或时间太短。4. 游戏视图被 UI 遮挡。1. 检查if (GEngine)判断。2. 确认调用时机在有效的游戏阶段。3. 检查Key的使用和TimeToDisplay。4. 切换到纯游戏视图F8。1. 确保在游戏运行时调用。2. 在Tick或由游戏事件触发的函数中调用。3. 使用不同的Key或增加显示时间。4. 按 F8 切换到无 UI 的游戏视图。自定义日志类别编译错误1.DECLARE_LOG_CATEGORY_EXTERN和DEFINE_LOG_CATEGORY不匹配或位置错误。2. 头文件未包含。1. 检查声明和定义是否在正确的文件中且类别名称一致。2. 确保使用该类别的.cpp文件包含了声明它的头文件。1. 声明通常在项目全局头文件定义在对应.cpp。2. 在需要使用的源文件中#include “YourProject.h”。打包后日志消失1. 默认的日志级别在打包配置中更高。2. 未配置输出日志文件。1. 检查DefaultEngine.ini中针对Shipping等配置的覆盖设置。2. 查看打包后生成的YourGame.log文件通常在游戏可执行文件同级目录。1. 在DefaultEngine.ini的[Core.Log]中明确设置LogYourProjectVerbose。2. 确保[Core.Log]下有LogYourGame.log配置。屏幕信息在打包后仍显示屏幕信息未用#if !(UE_BUILD_SHIPPING)等条件编译包裹。检查调用AddOnScreenDebugMessage的代码。将所有调试用的屏幕信息调用用 #if !(UE_BUILD_SHIPPING格式化字符串出错崩溃或乱码1. 格式化占位符与参数类型不匹配。2. 使用了char*而非TCHAR*。1. 仔细核对%s,%d,%f等与参数类型。2. 确保字符串字面量用TEXT()宏包裹。1.%s对应TCHAR*(用*FString)%d对应int32%f对应float/double。2. 所有传递给UE_LOG或用于FString::Printf的字符串字面量都应使用TEXT(“…” )。9. 最佳实践与使用建议遵循以下建议可以让你的日志系统更清晰、高效且易于维护。分层与分类使用自定义日志类别为不同的系统模块如LogGameplay,LogAI,LogNetwork,LogInventory创建独立的日志类别。便于在输出日志中过滤。合理使用日志级别Error仅用于真正的错误情况如资源加载失败、网络断开。Warning用于可能有问题但程序能继续运行的情况如使用默认值、降级处理。Display/Log用于重要的流程信息如“关卡加载完成”、“玩家登录”。Verbose/VeryVerbose用于详细的调试信息如每帧数据、循环内部状态。这些级别在开发时开启发布时关闭。信息清晰有用在日志信息中包含足够的上下文如对象名称、函数名、关键变量值。避免输出无意义或过于频繁的日志以免淹没重要信息。对于AddOnScreenDebugMessage保持信息简洁并合理使用颜色和Key进行分组。开发与发布分离将所有的调试日志尤其是Verbose级别和屏幕调试信息通过#if !(UE_BUILD_SHIPPING)条件编译或配置文件控制在开发版本中。确保Error和Warning级别的日志在发布版本中仍然有效这对于线上问题排查至关重要。善用引擎工具输出日志过滤器熟练使用输出日志窗口的搜索和过滤功能。控制台命令使用Log List查看所有日志类别使用Log LogCategory Verbose动态改变某个类别的日志级别。可视化调试器对于复杂的状态如行为树、EQS优先使用虚幻引擎内置的可视化调试工具它们比纯文本日志更直观。建立日志规范在项目初期就和团队约定日志的格式、类别命名规则和级别使用规范。考虑编写一个简单的日志工具类统一处理格式、添加时间戳、或自动将日志发送到服务器用于线上游戏分析。掌握UE_LOG和GEngine-AddOnScreenDebugMessage是 UEC 开发者调试基本功。从在输出日志中追踪程序流到在屏幕上实时监控游戏状态这两种工具覆盖了从底层排查到高层观察的大部分需求。建议你立即在项目中创建几个测试用例亲手体验不同参数的效果并尝试将它们整合到你的游戏逻辑调试中。当你习惯在关键节点添加清晰的日志后定位问题的效率会大幅提升。