虚幻引擎C++调试:UE_LOG与OnScreenDebugMessage实战指南
2026/8/9 3:05:10 网站建设 项目流程

1. 为什么在虚幻引擎C++里,日志是比断点更优先的调试手段

如果你刚开始用C++写虚幻引擎项目,遇到一个功能不生效或者程序崩溃,第一反应是什么?很多人会直接去打断点。但在虚幻引擎(UE)的开发里,尤其是UEC++,我更建议你先看日志。断点当然有用,但它打断的是单次执行流程,而日志记录的是程序运行的完整“现场”,特别是当问题发生在你无法直接打断点的线上打包版本、或者需要复现一个偶发Bug时,日志几乎是唯一可靠的线索。

UEC++的日志系统,核心就是两种:一种是UE_LOG,这是虚幻引擎自己的一套宏,功能强大,能分级、分类、格式化输出,是开发中最常用的;另一种是GEngine->AddOnScreenDebugMessage,它直接把信息打印到游戏画面的屏幕上,实时反馈,调试UI逻辑、角色移动、动画状态机时尤其直观。

这篇文章不讲虚的,直接拆这两种日志怎么用、什么时候用、以及新手最容易踩的坑。我会假设你刚搭好UE和Visual Studio的环境,连第一个C++类都还没创建,从零开始带你走一遍。你会发现,用好日志,能让你排查问题的效率提升好几个档次。

2. 环境准备与第一个带日志的C++类

在开始写任何日志代码之前,你得先有一个能编译运行的UEC++项目。这里不重复安装引擎和配置VS的步骤,我们直接从在项目里创建第一个C++ Actor开始。

第一步:创建C++类

  1. 在虚幻编辑器的内容浏览器里,右键 ->新建C++类
  2. 选择Actor作为父类,给它起个名字,比如MyFirstLogActor
  3. 点击创建,编辑器会调用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的基本格式像一个加强版的printf

UE_LOG(LogCategory, Verbosity, Format, ...)
  • LogCategory (日志类别):一个FLogCategory类型的对象,用于对日志进行分类过滤。最常用、也最省事的是LogTemp(临时日志)。

  • Verbosity (详细程度/级别):决定这条日志的重要性。级别从低到高(即从“话多”到“话少”)常见的有:

    • VeryVerbose,Verbose: 最详细的跟踪信息,用于深入追踪流程。在开发期可以打开,发布时通常关闭。
    • Log: 一般的 informational 信息,比如“关卡加载完成”、“玩家进入区域”。这是默认的信息级别。
    • Display: 在屏幕上也会显示的信息(注意,这里指的是编辑器的输出日志窗口的“屏幕”,不是游戏画面)。
    • Warning:警告。表示可能有问题,但程序还能继续运行。比如“找不到某个资源,使用了默认值”。这是你需要重点关注的级别之一
    • Error:错误。表示发生了严重问题,功能可能已经不正常,但引擎试图恢复。比如“无效的参数传入函数”。
    • Fatal:致命错误。记录后程序会立即崩溃。用于处理不可恢复的错误。
  • Format (格式字符串): 和C语言的printf类似,%s对应FStringTCHAR*%d对应int%f对应float等。UE也支持一些特有的格式符,但基础的就够用了。

  • ... (可变参数): 对应格式字符串中占位符的实际变量。

3.2 动手写第一个UE_LOG

让我们修改MyFirstLogActor.cppBeginPlay函数。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(“关键游戏数据加载失败!功能可能受限。”)); } }

编译并运行

  1. 在VS里编译你的项目(通常是F5启动调试,或者Ctrl+Shift+B编译)。
  2. 回到虚幻编辑器,从内容浏览器把你的MyFirstLogActor拖到场景中。
  3. 点击编辑器上的运行(Play)按钮。
  4. 不要看游戏画面,看编辑器下方的“输出日志”(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换成LogMyGame

void 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有更完善的日志框架,但那是后话。

核心建议:在开发阶段,养成习惯,遇到任何奇怪的现象,先打开输出日志窗口看看有没有WarningError。很多“玄学”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 关键技巧与避坑指南

  1. 性能注意Tick里每帧调用AddOnScreenDebugMessage并创建新的FString会有微小开销。对于需要持续显示的信息,务必使用一个唯一的、固定的Key。这样引擎会更新同一条消息,而不是每帧创建新的,效率更高,显示也更稳定(不会闪烁)。
  2. Key的管理:为不同类型的信息规划好Key的范围,避免冲突。比如角色状态用1000-1999,UI状态用2000-2999。
  3. 清除消息:你可以使用GEngine->RemoveOnScreenDebugMessage(Key)来手动清除某条消息。
  4. 只在开发阶段使用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 配置日志详细程度

你可能会发现,VerboseVeryVerbose级别的日志默认不显示。这是因为引擎有默认的日志详细程度配置。你可以在以下地方控制:

  1. 命令行参数:启动游戏时加上-Verbose-VeryVerbose来开启对应级别。例如在编辑器的“高级设置”中为Play模式添加参数,或者打包后通过命令行启动。
  2. 配置文件:在DefaultEngine.ini或项目配置文件中,可以针对特定日志类别设置:
    [Core.Log] LogMyGame=VeryVerbose LogTemp=Warning // 只显示Warning及以上级别的LogTemp日志
  3. 运行时控制台命令:在游戏内按~(波浪号)打开控制台,输入:
    Log LogMyGame VeryVerbose
    这会将LogMyGame类别的日志级别临时设置为VeryVerbose

6.2 日志文件与循环

在非编辑器环境下(打包的游戏),日志默认会写入Saved/Logs/YourProject.log。对于长时间运行的服务器或客户端,日志文件可能会非常大。虚幻引擎内置了日志循环功能。

  • 你可以在DefaultEngine.ini中配置LogFiles
    [Core.Log] LogTimes=UTC LogTimes=Local LogTimes=Counter LogTimes=ProgramTime LogFiles=5 // 保留5个日志文件循环
    这会在日志文件达到一定大小后,重命名为YourProject.log.1,YourProject.log.2等,最多保留指定数量。

6.3 生产环境下的日志策略

对于要上线的项目,日志策略需要仔细设计:

  • 减少Verbose日志:在发布版本中,默认关闭VerboseVeryVerbose以提升性能并减少日志体积。
  • 结构化日志:考虑将关键信息(时间戳、玩家ID、事件类型、关键参数)以更结构化的格式(如JSON的一行)记录,便于后续用日志分析工具处理。
  • 使用专门的日志分类:不要全用LogTemp。为GameplayNetworkAIInventory等系统创建独立的日志类别,方便按模块过滤和开关。
  • 敏感信息脱敏绝对不要在日志中记录玩家的密码、Token、个人身份信息等敏感数据。
  • 错误上报:可以结合UE_LOG(Error, ...)和游戏内的错误上报系统,将关键错误自动发送到服务器端收集分析。

7. 常见问题排查清单

当你写的日志没有按预期输出时,按这个顺序检查:

  1. 编译了吗?修改C++代码后,必须重新编译(在VS里生成解决方案),否则更改不会生效。
  2. 代码执行到了吗?你放日志的函数真的被调用了吗?在函数入口处加一条最简单的UE_LOG(LogTemp, Log, TEXT(“Function Entered”))确认。
  3. 日志级别过滤了吗?检查输出日志窗口顶部的过滤栏。是不是不小心过滤掉了Log级别?或者你的日志是Verbose级别,但运行配置没有开启详细日志?尝试用DisplayWarning级别输出看看。
  4. 屏幕消息没显示?
    • 首先检查if (GEngine)保护了吗?在打包版本中GEngine可能不可用。
    • 消息显示时间TimeToDisplay是不是设得太短(比如0.0)?设成2.0或5.0试试。
    • 消息被其他消息顶掉了?尝试使用一个独一无二的、大的Key值。
    • 游戏画面本身有UI覆盖了调试信息?尝试调整消息位置(这需要更复杂的画布绘制,非AddOnScreenDebugMessage默认功能)。
  5. 自定义日志类别不工作?确保在.cpp文件中使用了DEFINE_LOG_CATEGORY(LogCategoryName),并且这个.cpp文件被项目正确编译链接。有时在PCH(预编译头)中使用需要额外注意。
  6. 打包后没有日志文件?确保打包时没有完全剥离调试信息(Development 或 Debug 构建通常有日志,Shipping构建默认日志很少)。并且通过命令行参数-log启动打包后的程序。

把日志用好,是UEC++开发者从“能写代码”到“能高效解决问题”的关键一步。它不仅仅是printf,而是你嵌入在游戏里的、随时可以回放的“黑匣子”。先从在每一个你觉得不确定的地方打一条Log开始,慢慢你会形成自己的日志规范,这会让你在复杂的游戏开发中始终保持清晰的思路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询