尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

虚幻引擎C++调试:UE_LOG与OnScreenDebugMessage实战指南

虚幻引擎C++调试:UE_LOG与OnScreenDebugMessage实战指南 1. 为什么在虚幻引擎C里日志是比断点更优先的调试手段如果你刚开始用C写虚幻引擎项目遇到一个功能不生效或者程序崩溃第一反应是什么很多人会直接去打断点。但在虚幻引擎UE的开发里尤其是UEC我更建议你先看日志。断点当然有用但它打断的是单次执行流程而日志记录的是程序运行的完整“现场”特别是当问题发生在你无法直接打断点的线上打包版本、或者需要复现一个偶发Bug时日志几乎是唯一可靠的线索。UEC的日志系统核心就是两种一种是UE_LOG这是虚幻引擎自己的一套宏功能强大能分级、分类、格式化输出是开发中最常用的另一种是GEngine-AddOnScreenDebugMessage它直接把信息打印到游戏画面的屏幕上实时反馈调试UI逻辑、角色移动、动画状态机时尤其直观。这篇文章不讲虚的直接拆这两种日志怎么用、什么时候用、以及新手最容易踩的坑。我会假设你刚搭好UE和Visual Studio的环境连第一个C类都还没创建从零开始带你走一遍。你会发现用好日志能让你排查问题的效率提升好几个档次。2. 环境准备与第一个带日志的C类在开始写任何日志代码之前你得先有一个能编译运行的UEC项目。这里不重复安装引擎和配置VS的步骤我们直接从在项目里创建第一个C Actor开始。第一步创建C类在虚幻编辑器的内容浏览器里右键 -新建C类。选择Actor作为父类给它起个名字比如MyFirstLogActor。点击创建编辑器会调用Visual Studio或你设置的IDE打开这个新类的源文件。现在你有了两个文件MyFirstLogActor.h头文件和MyFirstLogActor.cpp源文件。所有的日志代码都将写在这里面。第二步理解基本代码结构打开.cpp文件你会看到类似下面的代码UE版本不同可能略有差异#include “MyFirstLogActor.h” // 构造函数 AMyFirstLogActor::AMyFirstLogActor() { PrimaryActorTick.bCanEverTick true; }PrimaryActorTick.bCanEverTick true;这行意味着这个Actor每一帧都会调用Tick函数。对于我们的日志演示可以先把它设为false因为我们暂时不需要每帧都执行。AMyFirstLogActor::AMyFirstLogActor() { PrimaryActorTick.bCanEverTick false; // 改为false节省性能 }准备工作完成。接下来我们让这个Actor在游戏开始时在后台和屏幕上都打个招呼。3. 深入UE_LOG你的后台诊断工具箱UE_LOG是输出到“输出日志”Output Log窗口和可能保存到文件里的日志。它不会显示在游戏画面上但信息最全是排查复杂问题的核心。3.1 基础语法与日志级别UE_LOG的基本格式像一个加强版的printfUE_LOG(LogCategory, Verbosity, Format, ...)LogCategory (日志类别)一个FLogCategory类型的对象用于对日志进行分类过滤。最常用、也最省事的是LogTemp临时日志。Verbosity (详细程度/级别)决定这条日志的重要性。级别从低到高即从“话多”到“话少”常见的有VeryVerbose,Verbose: 最详细的跟踪信息用于深入追踪流程。在开发期可以打开发布时通常关闭。Log: 一般的 informational 信息比如“关卡加载完成”、“玩家进入区域”。这是默认的信息级别。Display: 在屏幕上也会显示的信息注意这里指的是编辑器的输出日志窗口的“屏幕”不是游戏画面。Warning:警告。表示可能有问题但程序还能继续运行。比如“找不到某个资源使用了默认值”。这是你需要重点关注的级别之一。Error:错误。表示发生了严重问题功能可能已经不正常但引擎试图恢复。比如“无效的参数传入函数”。Fatal:致命错误。记录后程序会立即崩溃。用于处理不可恢复的错误。Format (格式字符串): 和C语言的printf类似%s对应FString或TCHAR*%d对应int%f对应float等。UE也支持一些特有的格式符但基础的就够用了。... (可变参数): 对应格式字符串中占位符的实际变量。3.2 动手写第一个UE_LOG让我们修改MyFirstLogActor.cpp的BeginPlay函数。BeginPlay在Actor被放入世界且游戏开始时调用一次是放初始化日志的理想位置。首先在头文件里确保BeginPlay被声明通常UE生成的Actor类默认就有。然后在.cpp文件中实现#include “MyFirstLogActor.h” #include “Engine/Engine.h” // 为了后面使用GEngine #include string // 可选用于std::string void AMyFirstLogActor::BeginPlay() { Super::BeginPlay(); // 千万别忘了调用父类方法 // 1. 最简单的日志 UE_LOG(LogTemp, Log, TEXT(“MyFirstLogActor 已开始播放”)); // 2. 带变量的日志 FString PlayerName TEXT(“新手开发者”); int32 PlayerScore 100; UE_LOG(LogTemp, Log, TEXT(“玩家 %s 的初始分数是%d”), *PlayerName, PlayerScore); // 3. 输出一个警告 float InvalidValue -1.0f; if (InvalidValue 0.0f) { UE_LOG(LogTemp, Warning, TEXT(“检测到无效的值%f。将使用默认值0.0。”), InvalidValue); } // 4. 输出一个错误模拟 bool bIsDataLoaded false; // 假设数据加载失败 if (!bIsDataLoaded) { UE_LOG(LogTemp, Error, TEXT(“关键游戏数据加载失败功能可能受限。”)); } }编译并运行在VS里编译你的项目通常是F5启动调试或者CtrlShiftB编译。回到虚幻编辑器从内容浏览器把你的MyFirstLogActor拖到场景中。点击编辑器上的运行Play按钮。不要看游戏画面看编辑器下方的“输出日志”Output Log窗口。如果没找到可以在窗口Window-开发者工具Developer Tools-输出日志Output Log中打开。你应该能看到类似这样的输出LogTemp: 玩家 新手开发者 的初始分数是100 LogTemp: Warning: 检测到无效的值-1.000000。将使用默认值0.0。 LogTemp: Error: 关键游戏数据加载失败功能可能受限。注意不同级别的日志可能有不同的颜色Warning可能是黄色Error是红色方便识别。3.3 高级用法与自定义日志类别一直用LogTemp没问题但当项目变大日志成千上万条时你想只看角色系统的日志或者只看网络同步的日志LogTemp就混在一起了。这时需要自定义日志类别。第一步定义自己的日志类别通常在某个模块的顶级头文件比如MyGame.h或自己类的头文件中定义。我们就在MyFirstLogActor.h里做// MyFirstLogActor.h #pragma once #include “CoreMinimal.h” #include “GameFramework/Actor.h” #include “MyFirstLogActor.generated.h” // 声明一个自定义的日志类别 DECLARE_LOG_CATEGORY_EXTERN(LogMyGame, Log, All); UCLASS() class YOURPROJECT_API AMyFirstLogActor : public AActor { GENERATED_BODY() public: // ... 其他声明 };第二步在.cpp文件中实现这个类别在MyFirstLogActor.cpp文件顶部// MyFirstLogActor.cpp #include “MyFirstLogActor.h” // 定义实现那个日志类别 DEFINE_LOG_CATEGORY(LogMyGame); // ... 剩下的代码第三步使用自定义类别现在你可以把LogTemp换成LogMyGamevoid AMyFirstLogActor::BeginPlay() { Super::BeginPlay(); UE_LOG(LogMyGame, Verbose, TEXT(“[Verbose] 开始进行详细的初始化步骤检查...”)); UE_LOG(LogMyGame, Log, TEXT(“[Log] 角色系统初始化完成。”)); }在输出日志窗口的过滤栏你可以输入LogMyGame来只显示这个类别的日志这在调试特定系统时非常清爽。3.4 在哪里查看和保存这些日志编辑器内运行主要在输出日志Output Log窗口查看。你可以复制、清除、过滤。打包后游戏运行Windows: 如果你用-log参数启动游戏日志会显示在命令行窗口。也可以使用-stdout和-FullStdOutLogOutput参数让日志输出更完整。日志文件通常位于Saved/Logs目录下例如YourProject.log。对于更复杂的日志收集比如服务器集群UE有更完善的日志框架但那是后话。核心建议在开发阶段养成习惯遇到任何奇怪的现象先打开输出日志窗口看看有没有Warning或Error。很多“玄学”Bug的根源就在这里。4. 使用OnScreenDebugMessage你的实时画面监视器有时候你看后台日志不方便或者你想实时观察某个变量比如角色速度、生命值、动画状态在游戏每一帧的变化。这时候就该GEngine-AddOnScreenDebugMessage出场了。4.1 基础语法与参数这个函数会把文字直接绘制在游戏画面的左上角默认位置可调。GEngine-AddOnScreenDebugMessage( Key, // int32: 消息的唯一键用于后续更新或移除该条消息。用-1则每次都创建新消息。 TimeToDisplay, // float: 消息在屏幕上停留的时间秒。 Color, // FColor: 消息的颜色。 DebugMessage, // FString: 要显示的消息文本。 bNewerOnTop, // bool: 如果为true新消息显示在旧消息上方。 FVector2D::ZeroVector // FVector2D: 可选的文本缩放非必需参数常用默认值。 );4.2 在屏幕上显示实时信息让我们修改MyFirstLogActor在Tick函数里显示一些实时信息。首先把构造函数里bCanEverTick改回true并确保头文件里Tick函数被声明。// MyFirstLogActor.h public: virtual void Tick(float DeltaTime) override;// MyFirstLogActor.cpp void AMyFirstLogActor::BeginPlay() { Super::BeginPlay(); UE_LOG(LogMyGame, Log, TEXT(“OnScreen Debug 演示开始。”)); } void AMyFirstLogActor::Tick(float DeltaTime) { Super::Tick(DeltaTime); // 获取这个Actor在世界中的位置 FVector CurrentLocation GetActorLocation(); // 构造要显示的字符串 FString DebugString FString::Printf(TEXT(“位置: X%.2f, Y%.2f, Z%.2f”), CurrentLocation.X, CurrentLocation.Y, CurrentLocation.Z); // 在屏幕上显示键值为-1表示每帧都创建新消息会闪烁 // 更好的做法是使用一个固定的Key来更新同一条消息 GEngine-AddOnScreenDebugMessage( -1, // 键值-1表示每帧新消息 0.02f, // 显示时间略长于一帧假设60帧一帧约0.016s FColor::Green, // 颜色 DebugString, // 文本 false, // 新消息不在顶部这个参数影响不大 FVector2D(1.0f, 1.0f) // 缩放 ); // 示例用一个固定Key来更新一条独立的消息比如显示Tick计数 static int32 TickCount 0; TickCount; FString TickString FString::Printf(TEXT(“Tick 计数: %d”), TickCount); GEngine-AddOnScreenDebugMessage( 12345, // 固定Key 0.02f, FColor::Yellow, TickString, true // 这条消息始终显示在顶部区域 ); }运行游戏你会在画面左上角看到绿色的位置信息和黄色的Tick计数在不断更新。4.3 关键技巧与避坑指南性能注意Tick里每帧调用AddOnScreenDebugMessage并创建新的FString会有微小开销。对于需要持续显示的信息务必使用一个唯一的、固定的Key。这样引擎会更新同一条消息而不是每帧创建新的效率更高显示也更稳定不会闪烁。Key的管理为不同类型的信息规划好Key的范围避免冲突。比如角色状态用1000-1999UI状态用2000-2999。清除消息你可以使用GEngine-RemoveOnScreenDebugMessage(Key)来手动清除某条消息。只在开发阶段使用GEngine在打包后的非编辑器构建中可能为nullptr。直接调用会导致崩溃。安全的做法是if (GEngine) { GEngine-AddOnScreenDebugMessage(...); }或者更好的方法是使用ENABLE_DRAW_DEBUG宏或检查GIsEditor标志确保调试代码不会出现在发布版本中。对于新手先用if (GEngine)是最简单的保护。5. 实战用日志系统调试一个常见问题我们设计一个简单场景来串联两种日志的使用。假设我们有一个函数根据输入的距离计算伤害但有时伤害计算异常。// MyFirstLogActor.h public: UFUNCTION(BlueprintCallable, Category “Combat”) float CalculateDamage(float Distance);// MyFirstLogActor.cpp float AMyFirstLogActor::CalculateDamage(float Distance) { // 规则距离越近伤害越高基础伤害100每单位距离衰减10 const float BaseDamage 100.0f; const float DamageFalloffPerUnit 10.0f; // 防御性编程记录输入 UE_LOG(LogMyGame, Verbose, TEXT(“CalculateDamage 被调用输入距离: %f”), Distance); if (Distance 0.0f) { // 输入非法记录错误并返回一个安全值如0 UE_LOG(LogMyGame, Error, TEXT(“错误距离参数不能为负数输入值: %f”), Distance); // 同时在屏幕上给开发者一个醒目的提示 if (GEngine) { GEngine-AddOnScreenDebugMessage(999, 5.0f, FColor::Red, TEXT(“[错误] CalculateDamage 收到负距离”)); } return 0.0f; } float CalculatedDamage BaseDamage - (Distance * DamageFalloffPerUnit); CalculatedDamage FMath::Max(CalculatedDamage, 0.0f); // 确保伤害不为负 // 记录计算结果 UE_LOG(LogMyGame, Log, TEXT(“计算完成。距离: %f, 最终伤害: %f”), Distance, CalculatedDamage); // 如果伤害为0记录一个警告可能是距离太远 if (CalculatedDamage 0.0f) { UE_LOG(LogMyGame, Warning, TEXT(“警告计算出的伤害为0或负数。目标可能超出有效射程。”)); } return CalculatedDamage; }然后在BeginPlay或某个地方测试它void AMyFirstLogActor::BeginPlay() { Super::BeginPlay(); // 测试正常情况 float Damage1 CalculateDamage(5.0f); if (GEngine) { GEngine-AddOnScreenDebugMessage(-1, 3.0f, FColor::Cyan, FString::Printf(TEXT(“5米距离伤害: %.1f”), Damage1)); } // 测试边界情况 float Damage2 CalculateDamage(12.0f); // 伤害应为 100 - 120 -20 - 被钳制为0 // 测试异常情况 float Damage3 CalculateDamage(-3.0f); // 应触发错误 }运行后你会在输出日志看到不同级别的记录并在屏幕上看到红色的错误信息和青色的伤害结果。通过这种方式你不仅能知道函数是否被调用还能知道它接收了什么参数、内部逻辑走到了哪一步、以及最终结果是什么。当游戏测试员报告“有时候打不出伤害”时你查看保存的日志文件就能快速定位是因为距离输入错误负数还是距离太远伤害为0而不是盲目地猜测。6. 日志配置、文件与生产环境实践6.1 配置日志详细程度你可能会发现Verbose和VeryVerbose级别的日志默认不显示。这是因为引擎有默认的日志详细程度配置。你可以在以下地方控制命令行参数启动游戏时加上-Verbose或-VeryVerbose来开启对应级别。例如在编辑器的“高级设置”中为Play模式添加参数或者打包后通过命令行启动。配置文件在DefaultEngine.ini或项目配置文件中可以针对特定日志类别设置[Core.Log] LogMyGameVeryVerbose LogTempWarning // 只显示Warning及以上级别的LogTemp日志运行时控制台命令在游戏内按~波浪号打开控制台输入Log LogMyGame VeryVerbose这会将LogMyGame类别的日志级别临时设置为VeryVerbose。6.2 日志文件与循环在非编辑器环境下打包的游戏日志默认会写入Saved/Logs/YourProject.log。对于长时间运行的服务器或客户端日志文件可能会非常大。虚幻引擎内置了日志循环功能。你可以在DefaultEngine.ini中配置LogFiles[Core.Log] LogTimesUTC LogTimesLocal LogTimesCounter LogTimesProgramTime LogFiles5 // 保留5个日志文件循环这会在日志文件达到一定大小后重命名为YourProject.log.1,YourProject.log.2等最多保留指定数量。6.3 生产环境下的日志策略对于要上线的项目日志策略需要仔细设计减少Verbose日志在发布版本中默认关闭Verbose和VeryVerbose以提升性能并减少日志体积。结构化日志考虑将关键信息时间戳、玩家ID、事件类型、关键参数以更结构化的格式如JSON的一行记录便于后续用日志分析工具处理。使用专门的日志分类不要全用LogTemp。为Gameplay、Network、AI、Inventory等系统创建独立的日志类别方便按模块过滤和开关。敏感信息脱敏绝对不要在日志中记录玩家的密码、Token、个人身份信息等敏感数据。错误上报可以结合UE_LOG(Error, ...)和游戏内的错误上报系统将关键错误自动发送到服务器端收集分析。7. 常见问题排查清单当你写的日志没有按预期输出时按这个顺序检查编译了吗修改C代码后必须重新编译在VS里生成解决方案否则更改不会生效。代码执行到了吗你放日志的函数真的被调用了吗在函数入口处加一条最简单的UE_LOG(LogTemp, Log, TEXT(“Function Entered”))确认。日志级别过滤了吗检查输出日志窗口顶部的过滤栏。是不是不小心过滤掉了Log级别或者你的日志是Verbose级别但运行配置没有开启详细日志尝试用Display或Warning级别输出看看。屏幕消息没显示首先检查if (GEngine)保护了吗在打包版本中GEngine可能不可用。消息显示时间TimeToDisplay是不是设得太短比如0.0设成2.0或5.0试试。消息被其他消息顶掉了尝试使用一个独一无二的、大的Key值。游戏画面本身有UI覆盖了调试信息尝试调整消息位置这需要更复杂的画布绘制非AddOnScreenDebugMessage默认功能。自定义日志类别不工作确保在.cpp文件中使用了DEFINE_LOG_CATEGORY(LogCategoryName)并且这个.cpp文件被项目正确编译链接。有时在PCH预编译头中使用需要额外注意。打包后没有日志文件确保打包时没有完全剥离调试信息Development 或 Debug 构建通常有日志Shipping构建默认日志很少。并且通过命令行参数-log启动打包后的程序。把日志用好是UEC开发者从“能写代码”到“能高效解决问题”的关键一步。它不仅仅是printf而是你嵌入在游戏里的、随时可以回放的“黑匣子”。先从在每一个你觉得不确定的地方打一条Log开始慢慢你会形成自己的日志规范这会让你在复杂的游戏开发中始终保持清晰的思路。
返回列表