在虚幻引擎CUEC开发中你是否曾为调试信息输出而烦恼是直接在屏幕上打印字符串还是使用更专业的日志系统面对复杂的游戏逻辑和偶现的Bug如何快速定位问题而不是在茫茫代码中大海捞针本文将为你彻底梳理UEC中的两种核心日志机制UE_LOG和GEngine-AddOnScreenDebugMessage。从零开始手把手带你掌握它们的使用方法、适用场景、高级技巧以及生产环境中的最佳实践让你在虚幻引擎C的调试之路上从入门到精通。1. 背景与核心概念为什么需要日志在软件开发中日志Log是记录程序运行时状态、事件和错误信息的关键工具。它就像程序的“黑匣子”当程序出现异常或行为不符合预期时日志是开发者进行问题诊断和回溯的第一手资料。在虚幻引擎Unreal Engine的C开发中日志系统尤为重要。游戏运行时状态复杂涉及渲染、物理、AI、网络等多个子系统同步工作。一个简单的逻辑错误可能导致角色无法移动、资源加载失败或游戏崩溃。如果没有有效的日志输出定位这些问题将如同盲人摸象。虚幻引擎内置了一套强大且灵活的日志系统主要为我们提供了两种面向开发者的输出方式UE_LOG 控制台/文件日志本质 将格式化后的字符串输出到引擎的日志系统。这些日志默认会显示在编辑器的“输出日志”Output Log窗口并可以写入到磁盘文件如Saved/Logs/目录下的.log文件中。特点 信息持久化可以记录大量历史数据支持分类Category和分级Verbosity适合记录程序运行的全流程、关键事件、警告和错误。它是事后分析和长期监控的首选。GEngine-AddOnScreenDebugMessage 屏幕调试信息本质 在游戏运行时的屏幕上Game Viewport直接绘制一段文本信息。特点 信息实时、直观会随着游戏帧的刷新而显示或消失。它非常适合实时监控变量的变化、验证代码执行路径、或者给测试人员提供临时的状态提示。但信息是临时的一旦消失便无法追溯。简单来说UE_LOG是写给“未来”的自己或同事看的诊断报告而屏幕信息是给“现在”的自己看的实时仪表盘。两者相辅相成共同构成了UEC调试的基石。2. 环境准备与版本说明在开始实践之前请确保你的开发环境已就绪。操作系统 Windows 10/11 或 macOS。本文示例以Windows为主但核心逻辑跨平台通用。虚幻引擎版本UE 5.0 及以上。本文示例代码在 UE 5.2 和 UE 5.3 中测试通过。UE4的大部分API也兼容但建议使用较新版本以获得更好的开发体验。开发环境Visual Studio 2022(Windows) 或Xcode(macOS) 用于C代码编译和调试。Visual Studio Code 可选用于代码编辑需安装C和Unreal Engine相关插件。项目类型 一个已创建的C 项目如 Third Person Template。纯蓝图项目无法直接使用C日志API。前置知识 基础的C语法了解如何在虚幻编辑器中创建C类并编译项目。重要提示 虚幻引擎版本迭代较快但核心日志API保持高度稳定。如果你的项目使用的是特定版本如UE 4.27本文的绝大多数代码依然适用但部分宏或头文件路径可能需要微调。请以你实际引擎版本的官方文档为最终参考。3. 核心语法与原理拆解3.1 UE_LOG 强大的控制台日志系统UE_LOG是一个功能强大的宏其核心设计基于日志类别Log Category和详细级别Log Verbosity。基本语法UE_LOG(LogCategory, Verbosity, Format, ...)LogCategory 日志类别用于对日志进行分类过滤。例如LogTemp是一个通用的临时类别。Verbosity 日志详细级别决定日志的重要性。级别从高到低常见的有Fatal 致命错误。打印日志后通常会调用abort终止程序。Error 错误。用红色显示表示操作失败。Warning 警告。用黄色显示表示可能存在问题的非致命情况。Display 显示。用白色显示重要的常规信息。Log 日志。用灰色显示一般的调试信息。Verbose 详细。更细致的调试信息。VeryVerbose 非常详细。最细致的调试信息可能影响性能。Format 格式化字符串类似printf。支持%s(FString, FName, FText需转换)%d,%f,%lld等。... 可变参数对应格式化字符串中的占位符。为什么需要类别和级别在一个大型项目中所有模块都往一个日志流里写信息会很快导致信息过载。通过类别你可以只查看LogAI、LogPhysics或LogNet相关的日志。通过级别你可以在开发时打开Verbose级别进行深度调试而在发布或测试时只显示Warning及以上级别的重要信息避免控制台被刷屏。3.2 GEngine-AddOnScreenDebugMessage 直观的屏幕输出这是一个通过全局引擎指针GEngine调用的方法用于在屏幕上绘制2D文本。基本语法if (GEngine) { GEngine-AddOnScreenDebugMessage( Key, // int32 消息的唯一键值用于后续更新或移除该消息。使用-1则每次创建新消息。 TimeToDisplay, // float 消息在屏幕上显示的持续时间秒。 Color, // FColor 消息的颜色。 Message, // const FString 要显示的字符串信息。 bNewerOnTop, // bool 如果为true新消息将显示在旧消息上方。 Scale // const FVector2D 文本的缩放比例。 ); }Key 的作用 这是屏幕消息与UE_LOG最大的不同之一。如果你使用一个非负的Key如1并且多次调用此函数它会更新Key对应的那条消息而不是创建一条新的。这非常适合用于持续显示某个变量的实时值如角色血量、帧率。性能注意 每帧在屏幕上绘制大量文本会影响性能。应避免在Tick函数中无节制地调用尤其是显示大量不同信息时。4. 完整实战案例在角色类中集成日志让我们通过一个具体的例子在一个第三人称模板的角色Character类中综合使用两种日志方法。4.1 创建或打开项目打开虚幻引擎使用第三人称游戏C模板创建一个新项目命名为LogDemo。创建完成后引擎会自动生成一个C角色类通常名为ALogDemoCharacter。在内容浏览器中右键点击它选择“在文件资源管理器中显示”然后用Visual Studio打开解决方案文件.sln。4.2 编写核心日志代码打开LogDemoCharacter.h文件在类声明中添加一个用于测试的变量和一个函数声明。// 文件路径 Source/LogDemo/LogDemoCharacter.h #pragma once #include CoreMinimal.h #include GameFramework/Character.h #include LogDemoCharacter.generated.h UCLASS(configGame) class ALogDemoCharacter : public ACharacter { GENERATED_BODY() public: ALogDemoCharacter(); protected: virtual void BeginPlay() override; virtual void Tick(float DeltaTime) override; // 新增一个测试变量 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category LogDemo) float TestHealth; // 新增一个测试函数用于触发日志 UFUNCTION(BlueprintCallable, Category LogDemo) void TakeDamage(float DamageAmount); };接下来打开LogDemoCharacter.cpp文件实现日志功能。// 文件路径 Source/LogDemo/LogDemoCharacter.cpp #include LogDemoCharacter.h #include Engine/Engine.h // 需要包含此头文件以使用GEngine #include DrawDebugHelpers.h // 可选用于绘制调试图形 // 定义自定义日志类别。通常放在.cpp文件顶部.h文件中用extern声明。 // 这里我们在.cpp中定义并使用它。 DEFINE_LOG_CATEGORY_STATIC(LogMyGameCharacter, Log, All); ALogDemoCharacter::ALogDemoCharacter() { // 设置默认值 TestHealth 100.0f; } void ALogDemoCharacter::BeginPlay() { Super::BeginPlay(); // 示例1使用默认的临时日志类别记录游戏开始。 UE_LOG(LogTemp, Display, TEXT(角色 %s 开始游戏), *GetName()); // 示例2使用自定义的日志类别。 UE_LOG(LogMyGameCharacter, Log, TEXT(BeginPlay called. Initial Health: %f), TestHealth); // 示例3在屏幕上显示一条欢迎信息显示5秒。 if (GEngine) { GEngine-AddOnScreenDebugMessage( -1, // 键值-1每次创建新消息 5.0f, // 显示5秒 FColor::Green, // 绿色 TEXT(欢迎来到LogDemo游戏), false, // 不置顶 FVector2D(1.0f, 1.0f) // 正常缩放 ); } } void ALogDemoCharacter::Tick(float DeltaTime) { Super::Tick(DeltaTime); // 示例4在Tick中持续更新屏幕上的健康值信息。 // 使用固定的Key如1这样它只会更新同一条消息而不是每帧创建新行。 if (GEngine) { FString HealthMessage FString::Printf(TEXT(当前健康值: %.1f), TestHealth); GEngine-AddOnScreenDebugMessage( 1, // 固定键值1用于更新健康值显示 0.0f, // 显示时间为0表示持续显示直到被更新或手动清除 FColor::Yellow, // 黄色 HealthMessage, false, FVector2D(1.0f, 1.0f) ); } // 示例5模拟一个条件触发警告日志。 if (TestHealth 30.0f TestHealth 0.0f) { // 使用自定义类别记录警告避免在Tick中重复打印。 // 这里用一个静态变量控制打印频率。 static float LastWarningTime -1.0f; if (GetWorld()-GetTimeSeconds() - LastWarningTime 2.0f) // 每2秒打印一次警告 { UE_LOG(LogMyGameCharacter, Warning, TEXT(警告角色 %s 健康值过低: %f), *GetName(), TestHealth); LastWarningTime GetWorld()-GetTimeSeconds(); } // 在屏幕上用红色显示警告使用另一个固定Key if (GEngine) { GEngine-AddOnScreenDebugMessage( 2, 1.0f, // 显示1秒会闪烁 FColor::Red, TEXT(警告生命值低), true, // 置顶显示 FVector2D(1.2f, 1.2f) // 稍微放大 ); } } } void ALogDemoCharacter::TakeDamage(float DamageAmount) { // 示例6在函数中记录错误和逻辑。 if (DamageAmount 0.0f) { UE_LOG(LogMyGameCharacter, Error, TEXT(TakeDamage 被调用但伤害值无效: %f), DamageAmount); return; } float OldHealth TestHealth; TestHealth - DamageAmount; TestHealth FMath::Max(TestHealth, 0.0f); // 确保不低于0 UE_LOG(LogMyGameCharacter, Display, TEXT(角色受到伤害。旧健康值: %f, 伤害: %f, 新健康值: %f), OldHealth, DamageAmount, TestHealth); // 示例7健康值归零时记录致命错误模拟。 if (TestHealth 0.0f) { UE_LOG(LogMyGameCharacter, Fatal, TEXT(角色 %s 已死亡), *GetName()); // 注意Fatal级别日志会终止程序。实际游戏中应使用其他逻辑如播放死亡动画、销毁角色等。 // 这里为了演示我们注释掉Fatal改用Error。 // UE_LOG(LogMyGameCharacter, Error, TEXT(角色 %s 已死亡), *GetName()); } }4.3 编译与运行验证在Visual Studio中编译整个解决方案通常按F5或选择“生成 - 生成解决方案”。编译成功后返回虚幻编辑器它会自动检测到更改并重新加载模块。点击编辑器工具栏上的“播放”按钮在编辑器视口中运行游戏。观察结果输出日志窗口 在编辑器底部找到“输出日志”Output Log标签页。你应该能看到类似以下的输出LogTemp: Display: 角色 LogDemoCharacter_C_0 开始游戏 LogMyGameCharacter: Log: BeginPlay called. Initial Health: 100.000000 LogMyGameCharacter: Display: 角色受到伤害。旧健康值: 100.000000, 伤害: 20.000000, 新健康值: 80.000000你可以尝试在“输出日志”窗口的过滤框中输入LogMyGameCharacter来只查看自定义类别的日志或者输入Warning来只查看警告。游戏视口 在运行的游戏中你应该能看到屏幕上方有绿色的“欢迎来到LogDemo游戏”字样5秒后消失。屏幕某处通常是左上角持续显示黄色的“当前健康值: 100.0”。如果你在角色蓝图中调用TakeDamage函数例如绑定到一个按键让健康值低于30屏幕会间歇性出现红色的“警告生命值低”字样并且“输出日志”中会每2秒出现一次警告日志。4.4 在蓝图中调用C函数为了测试TakeDamage函数我们可以在角色蓝图中添加一个简单的触发在内容浏览器中找到并打开你的角色蓝图通常是BP_LogDemoCharacter。在事件图表中右键搜索“键盘事件”例如添加一个“F键按下”事件。从事件节点拖出引线搜索“Take Damage”你定义的函数并连接。在DamageAmount引脚上输入一个值比如20.0。运行游戏按下F键观察控制台日志和屏幕健康值的变化。5. 常见问题与排查思路在使用UEC日志时你可能会遇到以下问题问题现象可能原因解决思路编译错误UE_LOG未定义没有包含必要的头文件。确保源文件顶部包含了#include CoreMinimal.h。对于自定义日志类别需要#include “YourModuleName.h”或在.cpp中定义。UE_LOG输出中文或特殊字符乱码UE_LOG的格式化字符串必须使用TEXT()宏包裹且源文件编码需为UTF-8。1. 检查所有字符串字面量是否用TEXT(“你的内容”)包裹。2. 确保你的C源文件保存为UTF-8 with BOM或UTF-8编码在VS中可通过“文件 - 高级保存选项”设置。GEngine为空指针导致崩溃在构造函数或某些非常早的初始化阶段GEngine可能还未初始化。在使用GEngine前必须进行空指针检查if (GEngine) { ... }。最佳实践是在BeginPlay()或Tick()等游戏循环开始后使用。屏幕调试信息不显示1.GEngine为空。2. 显示时间TimeToDisplay设置为0但Key不是固定值导致信息被瞬间覆盖。3. 信息被其他UI元素遮挡。4. 在编辑器模式而非“独立游戏”或“模拟”模式下运行。1. 检查if (GEngine)。2. 对于持续显示的信息使用固定的、非负的Key如1。3. 尝试调整信息位置可通过修改GEngine-AddOnScreenDebugMessage的后续参数但更复杂的位置控制需要Slate。4. 确保在正确的视口模式下运行。输出日志窗口被大量信息刷屏在Tick中使用了Verbose或Log级别且未加限制的UE_LOG。1.提升日志级别在编辑器“输出日志”窗口点击“日志”下拉菜单选择只显示Warning及以上级别。2.使用自定义日志类别并过滤。3.优化代码避免在每帧都执行的函数中打印低级别日志或使用静态变量、时间间隔来控制打印频率如示例5所示。自定义日志类别不起作用DEFINE_LOG_CATEGORY_STATIC宏放在了头文件(.h)中导致多重定义。正确做法在.cpp文件中使用DEFINE_LOG_CATEGORY_STATIC定义在对应的.h文件中使用DECLARE_LOG_CATEGORY_EXTERN声明。对于模块级类别通常在ModuleName.cpp中定义在ModuleName.h中声明。发布的游戏中仍然有大量日志输出影响性能默认的日志级别在开发Development和发布Shipping配置下不同但可能仍有残留。1. 使用#if !UE_BUILD_SHIPPING宏来包裹调试用的日志和屏幕信息代码确保它们在发布版本中不被编译。2. 在项目设置中配置最终的日志详细级别。6. 最佳实践与工程建议将日志用好能极大提升开发效率和项目可维护性。6.1 日志策略与规范合理使用日志级别Fatal/Error 仅用于不可恢复的错误或断言失败。Warning 用于可能有问题但程序仍可继续运行的情况如加载资源失败使用默认值。Display 用于记录重要的业务流程节点如关卡加载完成、玩家登录。Log/Verbose 用于详细的调试信息务必用#if !UE_BUILD_SHIPPING包裹或通过运行时配置控制。创建有意义的日志类别不要滥用LogTemp。为你的游戏模块、子系统创建专用的日志类别如LogMyGameAI、LogMyGameInventory、LogMyGameNetwork。这能让你在调试时快速过滤出相关领域的日志。格式化字符串要清晰在日志中包含足够的上下文信息如对象名称*GetName()、函数名FUNCTION_FNAME、时间戳FDateTime::Now()。示例UE_LOG(LogInventory, Display, TEXT([%s] 玩家 %s 获得了物品 %s (ID: %d)), *FDateTime::Now().ToString(), *PlayerName, *ItemName, ItemId);6.2 屏幕信息使用准则区分用途临时调试 使用Key为-1显示几秒后自动消失。适合一次性验证。持续监控 使用固定的Key如1,2,3...来更新显示变量值。确保在对象销毁或不再需要时考虑调用GEngine-RemoveOnScreenDebugMessage(Key)进行清理。分层显示 利用bNewerOnTop和不同的Key区域来组织信息如系统信息在顶部角色状态在中部调试命令在底部。性能考量绝对避免在每帧的Tick中创建大量新的屏幕消息Key为-1。这会导致严重的性能下降和视觉混乱。对于需要每帧更新的信息必须使用固定Key的更新模式。6.3 高级技巧与集成使用ensure和checkcheck(Expression) 如果表达式为假在开发配置下会触发断言并中断调试器。用于捕捉绝不应该发生的错误。ensure(Expression) 如果表达式为假会记录一次警告带调用堆栈但程序继续执行。用于捕捉可能发生但不一定致命的错误。ensure在第一次触发时会记录后续相同位置触发会静默避免日志刷屏。它们比单纯的UE_LOG(Error)更强大能自动捕获文件和行号。将日志输出到文件虚幻引擎默认就会将日志写入Saved/Logs/YourProjectName.log。你可以通过命令行参数-log来运行游戏确保日志被记录。在代码中你也可以使用FPlatformMisc::LowLevelOutputDebugString或自定义文件流来记录日志但通常引擎自带的日志系统已足够强大。在蓝图中查看C日志在蓝图中你可以使用“Print String”节点输出到屏幕相当于简易的屏幕信息但其输出也会显示在“输出日志”中类别为LogBlueprintUserMessages。这是一个连接蓝图和C调试的桥梁。掌握UE_LOG和GEngine-AddOnScreenDebugMessage是每一位UEC开发者必备的调试技能。从今天起在你的代码中 strategically 地加入日志而不是仅靠“猜”和“断点”。当你习惯用日志描绘程序的运行轨迹后你会发现解决Bug的速度有了质的飞跃。试着为你的下一个功能模块定义一个专属的日志类别并制定简单的日志级别规范从项目初期就建立起良好的调试习惯。