
你刚接触虚幻引擎CUEC时有没有过这样的困惑代码明明编译通过了运行时却看不到预期的效果或者直接崩溃屏幕上一片寂静完全不知道程序执行到哪一步、哪个变量出了问题你可能会本能地打开Visual Studio的调试器一步步跟进去。这当然有效但对于一个复杂的虚幻项目尤其是涉及蓝图交互、异步加载或网络同步时调试器有时会显得笨重断点也可能因为多线程而难以捕捉。这时你需要一双能“透视”程序运行过程的“眼睛”——日志Log。在UEC中日志远不止是简单的printf或std::cout它是一套深度集成在虚幻引擎框架内的诊断和追踪系统。很多人知道要用日志但往往停留在最基础的UE_LOG输出一两行信息就结束了。实际上UEC提供了两种核心的日志机制UE_LOG和GEngine-AddOnScreenDebugMessage。它们定位不同用法各异用好了不仅能快速定位问题还能在开发过程中实时监控游戏状态极大提升开发效率。本文将带你从零开始彻底搞懂这两种日志工具。我们不会只讲语法而是聚焦于一个核心判断UE_LOG是你的“飞行记录仪”用于事后深度分析而AddOnScreenDebugMessage是你的“驾驶舱仪表盘”用于实时状态监控。理解这个定位差异是高效使用它们的关键。接下来我们将通过具体实例一步步拆解它们的使用方法、适用场景以及那些新手最容易忽略的“坑”。1. 为什么说日志是UEC开发的“第一道防线”在开始具体技术细节前我们先要建立共识为什么在虚幻引擎中日志如此重要想象一下你写了一个角色移动函数在编辑器中按下按键角色却没动。可能的原因有几十种输入绑定错了移动组件没正确初始化碰撞体阻挡了物理模拟被禁用如果只靠猜或者盲目调试效率极低。而通过在不同关键节点插入日志你可以清晰地看到输入事件是否被触发。移动函数是否被调用。函数内部计算出的速度向量是多少。是否成功应用到了角色移动组件上。这个过程就像在黑暗的迷宫里丢下发光的路标。UE_LOG会将路标信息写入文件和控制台供你事后复盘AddOnScreenDebugMessage则直接把路标显示在游戏画面上让你实时看到。对于新手而言最容易犯的错误是把日志当作“一次性”的调试工具用完即删。实际上一套设计良好的日志系统应该贯穿开发始终。它不仅能帮你排查崩溃Crash、逻辑错误Logic Error更能辅助你理解引擎的执行流程Execution Flow和数据状态Data State尤其是在处理异步加载Async Loading、网络复制Replication等复杂机制时。2. UE_LOG深入引擎骨髓的“飞行记录仪”UE_LOG是虚幻引擎最核心、最强大的日志宏。它的输出目的地非常丰富开发者的输出日志窗口Output Log、保存到磁盘的日志文件.log甚至可以通过控制台命令实时查看。它的设计目标是详尽、可分类、可分级、可持久化。2.1 基础语法与日志类别Log Category最基本的UE_LOG格式如下UE_LOG(LogCategory, Verbosity, Format, ...);看起来简单但每个参数都有讲究。首先是LogCategory日志类别。这是新手最容易忽略但却是工程化使用日志的关键。类别用于对日志进行分类过滤。虚幻引擎自身有上百个内置类别如LogTemp临时、LogInit初始化、LogLoad加载等。你应该为自己的模块创建专属类别。创建自定义日志类别通常在头文件中进行// MyGameModule.h DECLARE_LOG_CATEGORY_EXTERN(LogMyGame, Log, All); // MyGameModule.cpp DEFINE_LOG_CATEGORY(LogMyGame);DECLARE_LOG_CATEGORY_EXTERN在头文件中声明。DEFINE_LOG_CATEGORY在源文件中定义。LogMyGame是你自定义的类别名。第二个参数Log是默认的Verbosity冗余级别第三个参数All表示在开发版本中编译所有级别的日志。使用自定义类别UE_LOG(LogMyGame, Warning, TEXT(Player %s has entered zone %d), *PlayerName, ZoneID);这样做的好处是你可以在编辑器或运行时通过控制台命令只显示或隐藏LogMyGame类别的日志避免被引擎其他海量日志淹没。2.2 日志级别Verbosity从闲聊到尖叫Verbosity决定了日志的“重要程度”。虚幻引擎定义了多个级别按严重性从低到高排列级别说明典型用途VeryVerbose极其详细追踪每帧的细微变化性能开销大通常只在深度调试时开启。Verbose详细记录常规流程信息如“函数A被调用”、“开始加载资源X”。Log一般信息默认级别记录重要的状态变化如“游戏模式已切换”。Display显示类似Log但更倾向于希望用户看到的信息。Warning警告关键级别表示可能有问题但程序还能继续运行。如“无效的输入参数使用默认值”、“资源加载超时”。这是你最应该频繁使用的级别之一用于暴露潜在风险。Error错误关键级别表示发生了错误功能可能无法正常工作但引擎尝试恢复。如“无法找到指定资源”、“网络连接断开”。Fatal致命错误无法恢复的错误记录日志后程序会立即崩溃。慎用仅用于绝对无法继续的情况。一个常见的误区是全部使用Log或Warning。正确的做法是根据信息的重要性选择合适的级别。例如一个可重试的网络请求失败用Error一个不影响核心玩法的材质丢失用Warning一个角色正常移动的每帧更新用Verbose并在发布版本中关闭。2.3 格式化输出与FStringUE_LOG的格式化字符串必须使用TEXT()宏包裹参数也需要适配虚幻引擎的类型。FString PlayerName TEXT(JohnDoe); int32 Score 100; float Health 75.5f; FVector Location GetActorLocation(); // 正确的写法 UE_LOG(LogMyGame, Log, TEXT(Player %s has score %d and health %.1f), *PlayerName, Score, Health); UE_LOG(LogMyGame, Log, TEXT(Actor Location: X%.2f, Y%.2f, Z%.2f), Location.X, Location.Y, Location.Z); // 注意FString需要解引用操作符 *注意FString转换为C风格字符串需要*操作符。直接传递FString对象会导致编译错误或运行时错误。2.4 高级用法与技巧条件日志避免不必要的字符串构建开销。// 不好的做法即使日志级别被关闭TEXT字符串拼接和参数计算也会发生 UE_LOG(LogMyGame, Verbose, TEXT(Expensive calculation result: %s), *SomeExpensiveFunction()); // 好的做法使用宏进行条件判断 UE_CLOG(bEnableDetailedLogging, LogMyGame, Verbose, TEXT(Detail: %s), *DetailInfo); // 或者手动判断 if (LogMyGame.GetVerbosity() ELogVerbosity::Verbose) { FString Info SomeExpensiveFunction(); UE_LOG(LogMyGame, Verbose, TEXT(Result: %s), *Info); }日志作用域自动记录函数的进入和退出对于分析性能或复杂调用链非常有用。void MyComplexFunction() { TRACE_CPUPROFILER_EVENT_SCOPE(MyComplexFunction); // 性能分析作用域 SCOPE_LOG_TIME_IN_SECONDS(TEXT(MyComplexFunction), nullptr); // 计时日志需要包含对应头文件 UE_LOG(LogMyGame, Verbose, TEXT(Entering MyComplexFunction)); // ... 函数逻辑 ... // 退出时会自动记录耗时 }查看日志编辑器内窗口 - 开发者工具 - 输出日志。运行时按~波浪号键打开控制台输入Log LogMyGame可以过滤日志。文件项目保存目录下的Saved/Logs/文件夹以.log结尾的文件。3. AddOnScreenDebugMessage实时反馈的“驾驶舱仪表盘”如果说UE_LOG是写给开发者或自动化工具看的详细报告那么GEngine-AddOnScreenDebugMessage就是展示给玩家或正在测试的开发人员看的实时状态信息。它的核心价值在于可视化、即时性、无需切换上下文。3.1 基础语法与参数解析其基本函数签名如下GEngine-AddOnScreenDebugMessage( Key, // 消息唯一键用于后续更新或移除 TimeToDisplay, // 显示时间秒-1表示永久显示直到手动清除 Color, // 显示颜色 Message, // 要显示的字符串FString bNewerOnTop, // 新消息是否显示在顶部 Scale // 文字缩放 );一个简单的例子if (GEngine) { GEngine-AddOnScreenDebugMessage( -1, // 使用-1作为Key表示每次都是新消息不覆盖 5.0f, // 显示5秒 FColor::Green, // 绿色文字 TEXT(Hello, OnScreen Debug!), true, // 新消息在顶部 FVector2D(1.0f, 1.0f) // 缩放 ); }3.2 关键参数详解与策略Key关键键这是最有用的参数却最常被忽略。-1每次调用都生成一条新消息永不覆盖。适用于一次性提示如“拾取物品”。非负整数如0, 1, 2...具有相同Key的消息会相互覆盖。这是实现“仪表盘”功能的核心。// 在Tick中更新角色的血量显示始终显示在屏幕固定位置 void AMyCharacter::Tick(float DeltaTime) { Super::Tick(DeltaTime); if (GEngine) { FString HealthText FString::Printf(TEXT(Health: %.0f / %.0f), CurrentHealth, MaxHealth); GEngine-AddOnScreenDebugMessage( 0, // 固定Key为0 0.0f, // 显示时间为0表示依赖Key机制实际会持续到下一帧覆盖 FColor::Red, HealthText, false, // 不重要因为会覆盖 FVector2D(1.5f, 1.5f) // 放大一点 ); } }这样无论Tick执行多少次屏幕上始终只有一条最新的血量信息不会堆积。TimeToDisplay显示时间正数定时消失。0.0f一个特殊用法。当Key 0时消息会持续显示直到被具有相同Key的新消息覆盖。配合Key使用可以实现持续更新的信息。-1.0f永久显示需调用GEngine-RemoveOnScreenDebugMessage(Key)手动清除。Color颜色善用颜色编码信息。例如红色表示警告/错误低血量、技能冷却绿色表示正常/增益获得Buff黄色表示中立信息距离提示蓝色表示系统信息连接状态。3.3 与UE_LOG的对比与联合使用特性UE_LOGAddOnScreenDebugMessage主要目的持久化记录、事后分析、文件追踪实时视觉反馈、调试监控输出目标输出日志窗口、日志文件、控制台游戏视口屏幕信息量可输出大量、复杂、格式化的信息适合简短、关键的状态摘要性能影响写入文件有I/O开销级别可控每帧渲染文本有GPU开销需控制数量使用场景错误报告、流程追踪、数据记录、自动化测试分析实时显示血量、分数、调试变量、临时提示最佳实践是联合使用用UE_LOG记录详细过程同时用AddOnScreenDebugMessage在屏幕上高亮最关键的状态变化或错误。void AMyCharacter::TakeDamage(float Amount) { CurrentHealth - Amount; // 详细日志用于分析 UE_LOG(LogMyGame, Warning, TEXT(%s took %.2f damage, health now: %.2f), *GetName(), Amount, CurrentHealth); if (GEngine) { // 屏幕实时警告 FString DamageMsg FString::Printf(TEXT(受到伤害: %.0f!), Amount); GEngine-AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, DamageMsg); // 更新常驻血量显示假设Key 0用于血量 FString HealthMsg FString::Printf(TEXT(生命值: %.0f), CurrentHealth); GEngine-AddOnScreenDebugMessage(0, 0.0f, (CurrentHealth 20.0f) ? FColor::Red : FColor::Green, HealthMsg); } if (CurrentHealth 0.0f) { UE_LOG(LogMyGame, Error, TEXT(%s has been defeated!), *GetName()); Die(); } }4. 从“能用”到“用好”工程化日志实践与避坑指南掌握了基本语法只是第一步。要把日志变成得力的开发助手而不是混乱的垃圾信息输出源你需要建立一些工程化的实践和意识。4.1 为不同构建配置设定日志级别你肯定不希望Verbose级别的调试日志出现在发布给玩家的版本中这会影响性能并可能暴露内部信息。虚幻引擎通过DefaultEngine.ini配置文件控制。; DefaultEngine.ini [Core.Log] ; 全局默认日志级别 GlobalVerbose ; 关闭特定类别在特定级别的日志 LogMyGameWarning ; 更细粒度的控制在Shipping构建中将LogMyGame的Warning也关闭 [Core.Shipping.Log] LogMyGameError在代码中你也可以通过#if预编译指令来控制#if !UE_BUILD_SHIPPING UE_LOG(LogMyGame, VeryVerbose, TEXT(Detailed debug info: %s), *DebugInfo); #endif4.2 结构化日志与上下文信息避免输出意义不明的日志。好的日志应该自带上下文。// 不好的日志 UE_LOG(LogMyGame, Warning, TEXT(Invalid value.)); // 好的日志 UE_LOG(LogMyGame, Warning, TEXT([%s::%s] Invalid weapon ID provided: %d. Defaulting to ID 0.), ANSI_TO_TCHAR(__FUNCTION__), // 函数名 *GetName(), // 对象名 ProposedWeaponID);可以创建一个辅助函数或宏来统一添加上下文如对象名、世界时间、网络角色等。4.3 性能考量与常见陷阱避免在热路径Hot Path中频繁记录高Verbosity日志比如在Tick函数中每帧记录Verbose日志在开发阶段可能没问题但会严重影响性能。使用条件判断或确保在发布版本中关闭。FString构建开销FString::Printf和字符串拼接在循环或每帧调用中会产生开销。对于频繁更新的屏幕信息考虑重用FString变量。屏幕消息数量爆炸无节制地使用Key-1的屏幕消息会导致文字堆满屏幕看不清也影响渲染。严格使用Key来管理重要信息的更新及时清理临时消息。空指针检查GEngine在游戏早期初始化阶段或某些特定上下文中可能为空。使用前务必检查。if (GEngine GWorld GWorld-GetNetMode() ! NM_DedicatedServer) { // 在非专用服务器上才添加屏幕消息 GEngine-AddOnScreenDebugMessage(...); }日志刷新默认情况下日志输出到控制台和文件可能不是立即刷新的。对于追踪崩溃前的最后信息可以使用FFlushLog但通常只在极端调试时使用。4.4 一个完整的实战排查案例问题玩家有时无法拾取地上的武器。排查步骤在拾取交互入口添加Verbose日志void AWeaponPickup::OnPlayerOverlap(AActor* OtherActor) { UE_LOG(LogMyGame, Verbose, TEXT(WeaponPickup %s: Overlap detected with %s), *GetName(), *OtherActor-GetName()); // ... 后续逻辑 }在条件判断处添加Warning日志AMyCharacter* PlayerChar CastAMyCharacter(OtherActor); if (!PlayerChar) { UE_LOG(LogMyGame, Warning, TEXT(WeaponPickup %s: Overlap actor is not a player character.), *GetName()); return; } if (PlayerChar-GetCurrentWeapon() WeaponClass) { UE_LOG(LogMyGame, Log, TEXT(Player already has this weapon.)); return; }在成功和失败路径添加Log和Error日志if (PlayerChar-AddWeaponToInventory(WeaponClass)) { UE_LOG(LogMyGame, Log, TEXT(Player %s successfully picked up weapon %s.), *PlayerChar-GetName(), *GetName()); if (GEngine) GEngine-AddOnScreenDebugMessage(-1, 3.0f, FColor::Green, TEXT(获得新武器)); Destroy(); } else { UE_LOG(LogMyGame, Error, TEXT(Failed to add weapon %s to player %ss inventory!), *GetName(), *PlayerChar-GetName()); if (GEngine) GEngine-AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, TEXT(拾取失败背包已满)); }分析运行游戏尝试拾取。通过输出日志你可能会发现根本没有触发Overlap事件检查碰撞设置。Overlap触发了但Actor不是PlayerCharacter检查碰撞过滤器。是PlayerCharacter但AddWeaponToInventory返回false进入该函数内部继续添加日志检查背包容量、武器是否重复等。通过这样一层层、有级别、有类别的日志你可以像侦探一样精准定位问题发生的环节而不是盲目地猜测和修改代码。回到我们最初的主判断UE_LOG是你的“飞行记录仪”它详尽、持久是事后分析问题的终极依据AddOnScreenDebugMessage是你的“驾驶舱仪表盘”它直观、实时是开发过程中监控状态的利器。理解并善用这两者意味着你在UEC开发中拥有了清晰的视野和强大的控制力。不要仅仅满足于让代码运行起来更要通过日志让它变得“透明”和“可观测”。从今天起在你写的每一个关键函数里有意识地加入一两行恰当的日志这会在未来某个调试的深夜里为你节省下无数个小时。