关于 ue-testing-debugging
面向 Unreal Engine 5.8 开发者的技能,覆盖日志、断言、自动化与功能测试、控制台命令和性能分析。它也介绍调试可视化及不同构建配置下的相关行为。
主要功能
- 01配置日志类别、级别与结构化日志
- 02选择断言方式并了解构建差异
- 03编写自动化、CQTest 和功能测试
- 04使用控制台命令与控制台变量
- 05分析性能并使用调试可视化
适用场景
- 01为 UE 模块记录和筛查运行信息
- 02为游戏逻辑编写自动化测试
- 03排查性能问题或可视化运行状态
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ quodsoler/ unreal-engine-skills --skill ue-testing-debugging安装后在 Claude Code 中输入/ue-testing-debugging确认已加载。
附带文件references/automation-test-patterns.mdreferences/profiling-commands.md
详细介绍
UE 测试、日志记录与性能分析
目标引擎:UE 5.8。以下 API 已根据 5.8 头文件验证;旧版形式列于“已弃用 — 请勿使用”下。
本技能负责本仓库的日志记录、断言、自动化/功能测试、控制台命令和性能分析。日志记录、断言、统计、Trace 和 CVar 位于 Core(Logging/、Misc/AssertionMacros.h、Stats/Stats.h、ProfilingDebugging/、HAL/IConsoleManager.h)。调试绘制、Visual Logger 和 AHUD::ShowDebug 位于 Engine。测试框架属于开发者模块 AutomationController、CQTest、FunctionalTesting 和 AutomationDriver;Gameplay Debugger 是运行时模块 GameplayDebugger。
上下文
如果 .agents/ue-project-context.md 存在,请阅读它(模块名称、约定、已启用插件、GAS/网络设置)。如果该文件缺失,不要因此停止。根据请求和代码库确定相关领域;只有在两种合理解读会产生不同代码时才提问。
| 请求涉及…… | 前往 |
|---|---|
UE_LOG、类别、详细级别、结构化日志记录 | 日志记录 |
check、ensure、verify、错误状态时崩溃 | 断言 |
单元测试、IMPLEMENT_SIMPLE_AUTOMATION_TEST、规格测试、潜伏命令 | 自动化测试 |
TEST_CLASS、ASSERT_THAT、夹具式测试 | CQTest |
放置在地图中的测试、AFunctionalTest | 功能测试 |
Insights、stat、周期计数器、CSV、内存、卡顿 | 性能分析 |
DrawDebug*、UE_VLOG、Gameplay Debugger、ShowDebug | 调试可视化 |
Exec 函数、FAutoConsoleCommand、CVar | 控制台命令和 CVar |
日志记录
声明类别
#include "Logging/LogMacros.h"
// MyGameLogging.h — DECLARE_LOG_CATEGORY_EXTERN(Name, DefaultVerbosity, CompileTimeVerbosity)
DECLARE_LOG_CATEGORY_EXTERN(LogMyGame, Log, All);
DEFINE_LOG_CATEGORY(LogMyGame); // MyGameLogging.cpp
| 宏 | 位置 | 作用范围 |
|---|---|---|
DECLARE_LOG_CATEGORY_EXTERN(Name, Default, CompileTime) | 头文件 | 所属模块及所有包含该头文件的代码 |
DEFINE_LOG_CATEGORY(Name) | 一个 .cpp 文件 | 与 EXTERN 形式配对 |
DEFINE_LOG_CATEGORY_STATIC(Name, Default, CompileTime) | 一个 .cpp 文件 | 仅限该翻译单元 |
DECLARE_LOG_CATEGORY_CLASS(Name, Default, CompileTime) | 类体 | 类静态类别;与 DEFINE_LOG_CATEGORY_CLASS(Class, Name) 配对 |
DefaultVerbosity 必须 <= CompileTimeVerbosity,否则类别宏内部的 static_assert 会触发。高于 CompileTimeVerbosity 的内容会被编译器删除,因此 DECLARE_LOG_CATEGORY_EXTERN(LogMyGame, Log, Log) 会移除所有 Verbose/VeryVerbose 调用点。单体构建可通过定义 COMPILED_IN_MINIMUM_VERBOSITY 一次性限制所有类别。
详细级别
| 级别 | 控制台 | 文件 | 说明 |
|---|---|---|---|
Fatal | 是 | 是 | 导致进程崩溃,即使禁用了日志记录也一样 |
Error | 是 | 是 | 会被命令程序和编辑器收集;导致命令程序失败 |
Warning | 是 | 是 | 可以提升为错误 |
Display | 是 | 是 | 用户必须看到该行时使用 |
Log | 否 | 是 | 仅写入文件 — 开发消息的默认级别 |
Verbose | 否 | 是 | 仅当类别提升至 Verbose 后才会记录 |
VeryVerbose | 否 | 是 | 仅当类别提升至 VeryVerbose 后才会记录 |
ELogVerbosity::All 是 VeryVerbose 的别名,通常用作 CompileTimeVerbosity 参数。
输出日志
UE_LOG(LogMyGame, Display, TEXT("Player %s spawned"), *PlayerName);
UE_LOG(LogMyGame, Warning, TEXT("Inventory full, dropped %s"), *ItemName);
UE_LOG(LogMyGame, Error, TEXT("Failed to load asset %s"), *AssetPath);
UE_LOG(LogMyGame, Fatal, TEXT("Save data unrecoverable")); // crashes
// Condition and arguments are evaluated only when the category is active.
UE_CLOG(Health <= 0.f, LogMyGame, Warning, TEXT("%s has zero health"), *GetName());
将开销较大的报告逻辑包在 if (UE_LOG_ACTIVE(LogMyGame, Verbose)) { … } 中,这样跳过的是实际工作,而不仅仅是格式化。
结构化日志记录
UE_LOGFMT 会输出可保留在 Insights 和日志分析中的命名字段;使用 {Name} 占位符,而不是 %s。
#include "Logging/StructuredLog.h"
// Named fields: order is irrelevant, extra fields are allowed.
UE_LOGFMT(LogMyGame, Warning, "Loading '{Name}' failed with error {Error}",
("Name", AssetName), ("Error", ErrorCode), ("Flags", LoadFlags));
// Positional fields: the values must match the placeholders exactly.
UE_LOGFMT(LogMyGame, Display, "Spawned {Count} actors in {Seconds}s", SpawnCount, Elapsed);
UE_CLOGFMT(bFailed, LogMyGame, Error, "Retry {Attempt} failed", ("Attempt", Attempt));
字段名称必须匹配 [A-Za-z0-9_]+,且每个事件中的名称必须唯一;值通过 SerializeForLog 或 operator<<(FCbWriter&, T) 序列化。UE_LOGFMT 最多接受 16 个字段;超出时使用 UE_LOGFMT_EX 搭配 UE_LOGFMT_FIELD / UE_LOGFMT_VALUE。
Shipping 和 Test 构建
在 Test 和 Shipping 中,NO_LOGGING 均为 !USE_LOGGING_IN_SHIPPING,在 Debug 和 Development 中则为 0。启用 NO_LOGGING 时,UE_LOG/UE_CLOG/UE_LOGFMT 会收敛为仅处理 Fatal 的路径:所有非致命调用都会消失,包括其参数。在 MyGame.Target.cs 中设置 bUseLoggingInShipping = true;,即可定义 USE_LOGGING_IN_SHIPPING=1 并恢复日志记录。
在运行时控制详细级别
-LogCmds="LogMyGame Verbose, LogNet Warning" # command line, applied at boot
Log list [Filter] # every category with its current verbosity
Log LogMyGame Verbose # raise one category
Log global none # silence everything
Log reset # restore defaults
在代码中使用 UE_SET_LOG_VERBOSITY(LogMyGame, Verbose);在配置中使用 DefaultEngine.ini 中的 [Core.Log]。
断言
所有定义都在 Misc/AssertionMacros.h 中。根据出错的一方选择:程序员错误使用 check,值得报告且可恢复的意外情况使用 ensure,用户输入或数据输入使用普通的 if。
check(Component != nullptr);
checkf(Index >= 0 && Index < Items.Num(), TEXT("Index %d out of [0,%d)"), Index, Items.Num());
checkNoEntry(); // unreachable branch
checkNoReentry(); // must not be entered twice
checkSlow(ValidateExpensiveInvariant()); // Debug only
verify(Manager->Initialize()); // expression ALWAYS evaluated
if (ensure(Component != nullptr))
{
Component->Activate(); // ensure() returns the condition
}
ensureMsgf(Health > 0.f, TEXT("%s has health %.1f"), *GetName(), Health);
ensureAlways(bInitialized); // reports every failure, not just the first
ensure/ensureMsgf 在每个会话中每个调用位置仅报告一次;ensureAlways/ensureAlwaysMsgf 则每次都会报告。四者都会返回条件值,因此可以与 if 组合使用。
| 构建 | DO_CHECK | DO_ENSURE | DO_GUARD_SLOW | NO_LOGGING |
|---|---|---|---|---|
| Debug | 1 | 1 | 1 | 0 |
| Development | 1 | 1 | 0 | 0 |
| Test 和 Shipping | USE_CHECKS_IN_SHIPPING | USE_ENSURES_IN_SHIPPING | 0 | !USE_LOGGING_IN_SHIPPING |
USE_CHECKS_IN_SHIPPING 默认为 0,由目标规则中的 bUseChecksInShipping 设置,因此默认情况下,Test 构建也会移除 check 和 ensure。当 DO_CHECK 为 0 时,check(expr) 会退化为 CA_ASSUME(expr),且不会对表达式求值;verify(expr) 仍会对表达式求值,只是不再中止程序。
自动化测试
测试源文件位于被测模块的 Private/Tests 文件夹中,或位于专用测试模块中。每个测试文件都要用 #if WITH_DEV_AUTOMATION_TESTS(或 WITH_AUTOMATION_TESTS = WITH_DEV_AUTOMATION_TESTS || WITH_PERF_AUTOMATION_TESTS)包裹——仅使用 IMPLEMENT_* 宏仍会编译类;只有在 WITH_AUTOMATION_WORKER 为 0 时才会跳过注册(Misc/AutomationTest.h:4366);UBT 会在除 Test 和 Shipping 以外的所有配置中启用这两个宏,可通过 bForceCompileDevelopmentAutomationTests、bForceCompilePerformanceAutomationTests 和 bForceDisableAutomationTests 覆盖(UEBuildTarget.cs:6309-6325)。模块和目标的配置方式、完整的模式库以及运行测试的命令行请参阅 references/automation-test-patterns.md。
// Source/MyGame/Private/Tests/MyInventoryTest.cpp — whole file inside #if WITH_DEV_AUTOMATION_TESTS
#include "Misc/AutomationTest.h"
#include "MyInventoryComponent.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FMyInventoryAddTest, "MyGame.Inventory.AddItem",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter)
bool FMyInventoryAddTest::RunTest(const FString& Parameters)
{
UMyInventoryComponent* Inv = NewObject<UMyInventoryComponent>(GetTransientPackage());
UE_RETURN_ON_ERROR(Inv != nullptr, TEXT("Inventory created"));
Inv->AddItem(FName("Sword"), 1);
TestEqual(TEXT("Count after add"), Inv->GetItemCount(FName("Sword")), 1);
TestTrue(TEXT("Has sword"), Inv->HasItem(FName("Sword")));
TestFalse(TEXT("No axe"), Inv->HasItem(FName("Axe")));
return true;
}
IMPLEMENT_COMPLEX_AUTOMATION_TEST 会添加 virtual void GetTests(TArray<FString>& OutBeautifiedNames, TArray<FString>& OutTestCommands) const,并针对每个条目运行一次 RunTest。IMPLEMENT_CUSTOM_SIMPLE_AUTOMATION_TEST(TClass, TBaseClass, PrettyName, TFlags) 会声明一个简单测试,其基类是你从 FAutomationTestBase 派生出的共享夹具。
标志
EAutomationTestFlags 是带有 ENUM_CLASS_FLAGS 的 enum class,因此可使用 | 组合。两个 static_assert 会强制要求至少一个上下文标志和恰好一个筛选标志。
| 组 | 值 |
|---|---|
| 上下文(至少需要 1 个) | EditorContext、ClientContext、ServerContext、CommandletContext、ProgramContext |
| 筛选(恰好 1 个) | SmokeFilter、EngineFilter、ProductFilter、PerfFilter、StressFilter、NegativeFilter |
| 优先级 | CriticalPriority、HighPriority、MediumPriority、LowPriority |
| 其他 | NonNullRHI、RequiresUser、Disabled、SupportsAutoRTFM |
EAutomationTestFlags_ApplicationContextMask 可用一个标记涵盖所有上下文标志。
测试中的断言
TestEqual(TEXT("Label"), Actual, Expected); // int32/int64/SIZE_T/FName/FText/FString/FStringView…
TestEqual(TEXT("Approx"), ActualF, ExpectedF, 0.001f); // float/double/FVector/FRotator/FTransform take a tolerance
TestTrue(TEXT("Label"), bCondition); // also TestFalse, TestNotEqual
TestNotNull(TEXT("Label"), Ptr); // also TestNull, TestSamePtr, TestNotSamePtr
TestValid(TEXT("Label"), SharedOrWeakPtr); // also TestInvalid
AddError(FString::Printf(TEXT("Unexpected damage %d"), Damage)); // also AddWarning, AddInfo
AddErrorIfFalse(bOk, TEXT("Setup failed"));
UE_RETURN_ON_ERROR(Ptr != nullptr, TEXT("Ptr must not be null")); // adds error, returns false
// Expect (and swallow) a log line the code under test is supposed to emit.
AddExpectedMessage(TEXT("Inventory is full"), ELogVerbosity::Warning,
EAutomationExpectedMessageFlags::Contains);
除非使用 …Plain 变体(AddExpectedMessagePlain、AddExpectedErrorPlain),否则模式会被视为正则表达式。AddExpectedError(Pattern, MatchType, Occurrences, bIsRegex) 是匹配警告和错误的简写形式(它会传递 ELogVerbosity::Warning,因此包含该级别);不带详细级别参数的 AddExpectedMessage 重载会匹配所有严重级别。
潜在命令
RunTest 会在潜在命令运行前返回。先将它们加入队列,然后在步骤完成时让 Update() 返回 true。在 return true; 之后加入队列属于不可达代码。
DEFINE_LATENT_AUTOMATION_COMMAND_ONE_PARAMETER(FMyWaitSecondsCommand, float, Duration);
bool FMyWaitSecondsCommand::Update()
{
return GetCurrentRunTime() >= Duration;
}
ADD_LATENT_AUTOMATION_COMMAND(FMyWaitSecondsCommand(2.0f)); // inside RunTest, before `return true;`
存在从 DEFINE_LATENT_AUTOMATION_COMMAND 到 ..._FIVE_PARAMETER 的宏。若要在等待结束后进行断言,请派生自 IAutomationLatentCommand 并捕获 FAutomationTestBase*。
规格测试
BEGIN_DEFINE_SPEC(TClass, PrettyName, TFlags) / 成员声明 / END_DEFINE_SPEC(TClass) 用于声明一个 BDD 风格测试,其主体为 void TClass::Define()。在 Define() 中使用 Describe、It、LatentIt、BeforeEach、LatentBeforeEach、AfterEach(以及 xIt/xDescribe 来禁用代码块)。如果规格测试不需要成员,可用 DEFINE_SPEC(TClass, PrettyName, TFlags) 替代这对宏。references/automation-test-patterns.md 中有完整示例。
CQTest
CQTest(模块 CQTest)是基于 fixture 的前端,底层使用同一框架。Assert 是一个 FNoDiscardAsserter,因此所有断言都通过 ASSERT_THAT 执行,失败时会从方法返回。
#include "CQTest.h"
TEST_CLASS(FMyInventoryTests, "MyGame.Inventory")
{
UMyInventoryComponent* Inventory = nullptr;
BEFORE_EACH() { Inventory = NewObject<UMyInventoryComponent>(GetTransientPackage()); }
TEST_METHOD(AddItem_WhenEmpty_StoresOne)
{
Inventory->AddItem(FName("Sword"), 1);
ASSERT_THAT(AreEqual(1, Inventory->GetItemCount(FName("Sword"))));
}
TEST_METHOD_WITH_TAGS(AddItem_WhenFull_Rejects, "[Slow][Inventory]")
{
Inventory->SetCapacity(0);
ASSERT_THAT(IsFalse(Inventory->AddItem(FName("Sword"), 1)));
}
};
TEST_CLASS(ClassName, TestDir) 使用 FNoDiscardAsserter 和 DefaultFlags(EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::ProductFilter)。变体包括:TEST_CLASS_WITH_FLAGS(ClassName, TestDir, Flags)、TEST_CLASS_WITH_TAGS、TEST_CLASS_WITH_BASE、TEST_CLASS_WITH_ASSERTS。生命周期宏为 BEFORE_ALL()/AFTER_ALL()(静态方法,每个 fixture 执行一次)和 BEFORE_EACH()/AFTER_EACH()(重写 Setup()/TearDown())。断言器辅助方法:IsTrue、IsFalse、IsNull、IsNotNull、AreEqual、AreNotEqual、AreEqualIgnoreCase、AreNotEqualIgnoreCase、IsNear(Expected, Actual, Epsilon)——每个方法还都接受一个末尾的失败消息参数。
世界、Actor 和异步辅助工具(TestCommandBuilder、FSpawnHelper、FActorTestSpawner、FMapTestSpawner、TObjectBuilder、UTestGameInstance)详见 references/automation-test-patterns.md。
功能测试
AFunctionalTest(模块 FunctionalTesting)是放置在测试地图中的一个 Actor。PrepareTest、StartTest、IsReady_Implementation 和 OnTimeout 是 受保护的虚方法;FinishTest 和 Assert* 辅助方法是公有的。
// MyFunctionalTest.h
#pragma once
#include "CoreMinimal.h"
#include "FunctionalTest.h"
#include "MyFunctionalTest.generated.h"
UCLASS()
class MYGAME_API AMyFunctionalTest : public AFunctionalTest
{
GENERATED_BODY()
protected:
// PrepareTest() sets TimeLimit / PreparationTimeLimit; IsReady_Implementation()
// gates StartTest(); StartTest() asserts and calls FinishTest().
virtual void PrepareTest() override;
virtual bool IsReady_Implementation() override;
virtual void StartTest() override;
private:
UPROPERTY(EditInstanceOnly, Category = "MyGame")
TObjectPtr<AActor> TargetActor;
};
每个重写方法都先调用 Super::。FinishTest(EFunctionalTestResult TestResult, const FString& Message) 会结束运行;EFunctionalTestResult 的取值为 Default、Invalid、Error、Running、Failed、Succeeded,其中 Default 会让记录的断言决定结果。断言包括 AssertTrue/AssertFalse(bool Condition, const FString& Message, const UObject* ContextObject = nullptr)、AssertIsValid(UObject*, const FString&, const UObject* = nullptr)、AssertEqual_Int/Float/Bool/Name/Object/Vector/Rotator/Transform/Quat 和 AssertValue_Int/Float/Double/DateTime(带有 EComparisonMethod)。此外还有 TimeLimit、PreparationTimeLimit、SetTimeLimit(float, EFunctionalTestResult)、LogMessage(const FString&) 以及 StartStep/FinishStep。完整的 .cpp 示例见 references/automation-test-patterns.md。
使用蓝图节点 UFunctionalTestingManager::RunAllFunctionalTests(WorldContextObject, bNewLog, bRunLooped, FailedTestsReproString) 运行当前世界中的所有功能测试(该类为 MinimalAPI,且该函数没有 FUNCTIONALTESTING_API,因此从其他模块的 C++ 代码中调用时无法链接),也可以从命令行运行:
UnrealEditor-Cmd.exe MyGame.uproject -ExecCmds="Automation RunTests MyGame.Functional" -unattended -nopause -testexit="Automation Test Queue Empty" -log
Automation 还接受 List、RunTest、RunFilter <Filter>、RunAll、SetFilter、SetPriority、SetMinimumPriority、Quit 和 SoftQuit;用 ; 将它们串联。
性能分析
完整的命令表、CSV 和 LLM 工作流以及瓶颈排查流程见 references/profiling-commands.md。
Unreal Insights
# Capture at launch
UnrealEditor.exe MyGame.uproject -trace=cpu,frame,bookmark,counters,gpu,loadtime,log -tracehost=127.0.0.1
UnrealEditor.exe MyGame.uproject -trace=cpu,frame -tracefile=MyCapture.utrace
# Or from the console, mid-session
Trace.File D:/Captures/Run1.utrace cpu,frame,bookmark # or Trace.Send 127.0.0.1 cpu,frame
Trace.Enable counters
Trace.Bookmark WaveStarted
Trace.Stop # also Trace.Pause, Trace.Resume, Trace.Status
通道名称去掉 Channel 后缀并改为小写:cpu、gpu、frame、bookmark、counters、log、loadtime、task、memalloc、callstack、module、metadata、net、rhicommands、rendercommands、slate、assetmetadata、iostore、animation、region、screenshot。
代码插桩
#include "Stats/Stats.h" // DECLARE_*_STAT, SCOPE_CYCLE_COUNTER
#include "HAL/PlatformMisc.h" // SCOPED_NAMED_EVENT
#include "ProfilingDebugging/CpuProfilerTrace.h" // TRACE_CPUPROFILER_EVENT_SCOPE
#include "ProfilingDebugging/MiscTrace.h" // TRACE_BOOKMARK
#include "ProfilingDebugging/CountersTrace.h" // TRACE_*_COUNTER, TRACE_INT_VALUE
#include "ProfilingDebugging/CsvProfiler.h" // CSV_*
#include "ProfilingDebugging/ScopedTimers.h" // FScopedDurationTimer
DECLARE_STATS_GROUP(TEXT("MyGame"), STATGROUP_MyGame, STATCAT_Advanced);
DECLARE_CYCLE_STAT(TEXT("MyGame Tick"), STAT_MyGameTick, STATGROUP_MyGame);
CSV_DEFINE_CATEGORY(MyGame, true);
TRACE_DECLARE_INT_COUNTER(MyGameActiveEnemies, TEXT("MyGame/ActiveEnemies"));
void UMySubsystem::Tick(float DeltaTime)
{
SCOPE_CYCLE_COUNTER(STAT_MyGameTick); // stat system + Insights
TRACE_CPUPROFILER_EVENT_SCOPE(UMySubsystem::Tick); // Insights only, very cheap
CSV_SCOPED_TIMING_STAT(MyGame, SubsystemTick); // one CSV column
SCOPED_NAMED_EVENT(MyGame_Tick, FColor::Orange); // also visible to PIX/Razor
TRACE_COUNTER_SET(MyGameActiveEnemies, ActiveEnemyCount);
CSV_CUSTOM_STAT(MyGame, Enemies, ActiveEnemyCount, ECsvCustomStatOp::Set);
}
void UMySubsystem::BeginWave(int32 WaveNumber)
{
TRACE_BOOKMARK(TEXT("WaveStarted_%d"), WaveNumber); // vertical marker in Insights
CSV_EVENT(MyGame, TEXT("WaveStarted"));
double LoadSeconds = 0.0;
{
FScopedDurationTimer Timer(LoadSeconds); // accumulates into LoadSeconds
LoadWaveAssets(WaveNumber);
}
UE_LOGFMT(LogMyGame, Display, "Wave {Wave} loaded in {Seconds}s",
("Wave", WaveNumber), ("Seconds", LoadSeconds));
}
QUICK_SCOPE_CYCLE_COUNTER(STAT_MyAdHocScope) 会内联声明一个用于一次性测量的统计项;TRACE_INT_VALUE(TEXT("MyGame/Spawned"), Count) 和 TRACE_FLOAT_VALUE 无需声明即可发出计数器;SCOPED_NAMED_EVENT_TEXT("Name", FColor::Yellow) 接受字面值,而 SCOPED_NAMED_EVENT_F 会对其进行格式化。
stat 命令
首先运行 stat unit:它会将帧时间拆分为 Game、Draw、RHIT 和 GPU。然后用 stat game、stat scenerendering、stat initviews、stat slate、stat threading、stat streaming、stat net 深入查看占主导地位的线程,或者用 stat MyGame 查看自己的组。stat unitgraph 会绘制这些数值随时间变化的图表,stat fps 会显示帧率,stat hitches 会标记卡顿尖峰,stat namedevents 会将统计项名称作为分析器事件发出,供外部工具使用。
内存
stat memory / stat memoryplatform # engine categories / OS physical and virtual
memreport -full # dump to Saved/Profiling/MemReports
obj list class=Texture2D # loaded objects of a class, with sizes
使用 -LLM 和 -trace=memalloc,callstack,module 启动,以使用 Memory Insights;使用 LLM_SCOPE(ELLMTag::EngineMisc) 为分配划定作用域,或通过 LLM_DEFINE_TAG/LLM_DECLARE_TAG 声明自定义标签,并使用 LLM_SCOPE_BYTAG 进入该作用域。在 Windows 上,非编辑器构建默认使用 MallocBinned3;可通过 -binnedmalloc2、-binnedmalloc、-ansimalloc 或 -stompmalloc 覆盖。
卡顿捕获与 UObject 计数
snapshothitches -start 会启用自动跟踪快照:每当统计系统检测到卡顿时便会捕获快照;snapshothitches -stop 会将其停用。此功能需要启用 STATS,且不能用于 Shipping 构建;启用期间会禁用 Screenshot 通道,以防过期截图进入快照尾部。
启用 CSV 分析后,使用 CSV_TRACK_UOBJECT_COUNT=1 编译会逐帧记录存活的 UObject 数量,作为类别 ObjectCount 中的 CSV 统计项 Total;该数值来自 UObjectStats::GetUObjectCount()。可利用此功能在长时间会话中检测对象泄漏,而无需进行完整的内存捕获。
调试可视化
调试绘制
DrawDebugHelpers.h 中的所有内容都受 ENABLE_DRAW_DEBUG 控制,其值为 UE_ENABLE_DEBUG_DRAWING = (!(UE_BUILD_SHIPPING || UE_BUILD_TEST) || WITH_EDITOR)。在 Test 或 Shipping 游戏构建中,它们会变为空的内联存根(DrawDebugHelpers.h:180-185;定义 SHIPPING_DRAW_DEBUG_ERROR=1 会移除这些存根,使残留调用无法通过编译),但参数仍会求值——请用 #if ENABLE_DRAW_DEBUG 包裹开销较大的调用位置。
#include "DrawDebugHelpers.h"
// LifeTime: -1 (default) or 0 draws one frame; > 0 is seconds.
// bPersistentLines = true keeps the shape until FlushPersistentDebugLines.
UWorld* World = GetWorld();
DrawDebugLine(World, Start, End, FColor::Red, false, 2.0f, 0, 2.0f);
DrawDebugPoint(World, Location, 8.0f, FColor::White, false, 2.0f, 0);
DrawDebugDirectionalArrow(World, Start, End, 40.0f, FColor::Cyan, false, 2.0f, 0, 2.0f);
DrawDebugBox(World, Center, Extent, FQuat::Identity, FColor::Blue, false, 2.0f, 0, 1.0f);
DrawDebugSphere(World, Center, 100.0f, 12, FColor::Green, false, 2.0f, 0, 1.0f);
DrawDebugCapsule(World, Center, 88.0f, 34.0f, FQuat::Identity, FColor::Yellow, false, 2.0f, 0, 1.0f);
DrawDebugString(World, Location, TEXT("Label"), nullptr, FColor::White, 2.0f, true, 1.0f);
FlushPersistentDebugLines(World); // and FlushDebugStrings(World)
面向 Blueprint 的 UKismetSystemLibrary::DrawDebugLine/Arrow/Sphere/Capsule/String 会自行进行条件判断,接受 FLinearColor 和 EDrawDebugSceneDepthPriorityGroup;此外还有 PrintString(WorldContextObject, InString, bPrintToScreen, bPrintToLog, TextColor, Duration, Key)。
Visual Logger
ENABLE_VISUAL_LOG 的值为 PLATFORM_DESKTOP && !NO_LOGGING && UE_ENABLE_DEBUG_DRAWING。通过 窗口 > Visual Logger 打开时间线。
#include "VisualLogger/VisualLogger.h"
UE_VLOG(this, LogMyGame, Log, TEXT("State: %s"), *StateName);
UE_VLOG_UELOG(this, LogMyGame, Warning, TEXT("Lost target %s"), *TargetName); // also hits UE_LOG
UE_VLOG_LOCATION(this, LogMyGame, Verbose, GetActorLocation(), 20.0f, FColor::Green, TEXT("Here"));
UE_VLOG_SEGMENT(this, LogMyGame, Log, PathStart, PathEnd, FColor::Red, TEXT("Path"));
UE_VLOG_SPHERE(this, LogMyGame, Verbose, PatrolPoint, 50.0f, FColor::Blue, TEXT("Patrol"));
UE_VLOG_ARROW(this, LogMyGame, Log, GetActorLocation(), AimPoint, FColor::Yellow, TEXT("Aim")); // also _BOX, _CAPSULE
UE_CVLOG(bIsAlerted, this, LogMyGame, Log, TEXT("Alerted")); // also UE_CVLOG_SEGMENT, UE_CVLOG_LOCATION
FVisualLogger::Get().SetIsRecording(true); // and SetIsRecordingToFile / SetIsRecordingToTrace
每个宏都会先检查 FVisualLogger::IsRecording(),因此关闭记录时,参数不会产生任何开销。
Gameplay Debugger 和 ShowDebug
在 PIE 中按 '。在模块的 StartupModule 中注册一个类别(将 "GameplayDebugger" 添加到 PrivateDependencyModuleNames;注册操作和类别类都用 #if WITH_GAMEPLAY_DEBUGGER 包裹。默认情况下,该宏在 Test/Shipping 游戏构建中为 0 — GameplayDebuggerCategory.h:10-11、TargetRules.cs:1125):
#include "GameplayDebugger.h" // in FMyGameModule::StartupModule()
if (IGameplayDebugger::IsAvailable())
{
IGameplayDebugger& Debugger = IGameplayDebugger::Get();
Debugger.RegisterCategory(TEXT("MyGame"),
IGameplayDebugger::FOnGetCategory::CreateStatic(&FMyDebuggerCategory::MakeInstance),
EGameplayDebuggerCategoryState::EnabledInGame);
Debugger.NotifyCategoriesChanged();
}
FMyDebuggerCategory 派生自 FGameplayDebuggerCategory,并重写 virtual void CollectData(APlayerController* OwnerPC, AActor* DebugActor) 和 virtual void DrawData(APlayerController* OwnerPC, FGameplayDebuggerCanvasContext& CanvasContext),通过 AddTextLine 和 AddShape 为它们提供数据。在 ShutdownModule 中调用 UnregisterCategory(TEXT("MyGame"))。
AHUD::ShowDebug(FName DebugType) 可切换指定的调试页面(ShowDebug AI、ShowDebug Physics);ShowDebugToggleSubCategory(FName) 可切换子页面,ShowDebugForReticleTargetToggle(TSubclassOf<AActor>) 则将其目标切换为准星所指向的对象。重写 virtual void ShowDebugInfo(float& YL, float& YPos) 或绑定 AHUD::OnShowDebugInfo,以实现自己的页面。独立的 display、displayall 和 displayclear 控制台命令可将对象或类的指定属性打印到 HUD 上。
控制台命令和 CVar
// In the AMyPlayerController class body. Exec works on PlayerController, Pawn, HUD,
// GameMode, GameState, CheatManager (or a UCheatManagerExtension) and GameInstance.
UFUNCTION(Exec)
void ToggleGodMode();
// MyGameDebug.cpp — console objects register at static init
#include "HAL/IConsoleManager.h"
static FAutoConsoleCommand GMyDumpStatsCmd(
TEXT("MyGame.DumpStats"), TEXT("Dump gameplay stats"),
FConsoleCommandDelegate::CreateStatic(&FMyStatsDumper::Dump));
// World + args; prefer this for anything that touches gameplay in PIE.
static FAutoConsoleCommandWithWorldAndArgs GMySpawnItemCmd(
TEXT("MyGame.SpawnItem"), TEXT("Usage: MyGame.SpawnItem <Name>"),
FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(&FMySpawner::SpawnFromConsole));
static TAutoConsoleVariable<float> CVarMyDamageScale(
TEXT("MyGame.DamageScale"), 1.0f, TEXT("Scales all outgoing damage."), ECVF_Cheat);
// FAutoConsoleVariableRef binds a console name to an existing int32/float/bool/FString/FName.
static float GMyDrawDistance = 5000.0f;
static FAutoConsoleVariableRef CVarMyDrawDistance(
TEXT("MyGame.DrawDistance"), GMyDrawDistance, TEXT("Gameplay draw distance."), ECVF_Default);
使用 GetValueOnGameThread()、GetValueOnRenderThread() 或 GetValueOnAnyThread() 读取 CVar。常用标志:ECVF_Default、ECVF_Cheat(在 Shipping 目标中禁用)、ECVF_ReadOnly、ECVF_Scalability、ECVF_RenderThreadSafe。当命令的生命周期与某个对象绑定时,请在运行时使用 IConsoleManager::Get().RegisterConsoleCommand(Name, Help, Delegate) 注册,并使用 IConsoleManager::Get().UnregisterConsoleObject(Cmd) 释放返回的 IConsoleCommand*。
已弃用 — 请勿使用
| 不要输出 | 在 5.8 中使用 | 来源 |
|---|---|---|
UE_TRACE_BOOKMARK(...) | TRACE_BOOKMARK(Format, ...) | ProfilingDebugging/MiscTrace.h — 不存在 UE_TRACE_BOOKMARK |
Trace.Start [Channels] | Trace.File [Path] [Channels] | TraceAuxiliary.cpp 中说明了“'Trace.Start' is being deprecated in favor of 'Trace.File'” |
stat startfile / stat stopfile / stat startfileraw(.uestats) | Trace.File + UnrealInsights.exe | 在 Stats/StatsFile.h 中受 UE_ENABLE_STATS_FILE_DEPRECATED_IN_5_8(默认值为 0)控制 |
stat gpu | ProfileGPU、DumpGPU 或按队列划分的组 stat GPU0_Graphics0 | RHI/Private/GPUProfiler.cpp 中的 GPU 组由 GPU%d_%s%d 构建为 STATGROUP_%s |
未限定作用域的标志名称,如 EditorContext | 为其指定作用域:EAutomationTestFlags::EditorContext | EAutomationTestFlags 在 Misc/AutomationTest.h 中是 enum class |
将 EAutomationExpectedErrorFlags::… 作为独立类型使用 | EAutomationExpectedMessageFlags::… | 前者现在是后者的命名空间别名 |
UE_LOG_REF、UE_LOG_CLINKAGE | UE_LOG | 在 Logging/LogMacros.h 中标记为“DO NOT USE … will be deprecated” |
-csvstatfile=Out.csv | -csvCaptureFrames=N 加上 CsvProfile StartFile=<name> | CsvProfiler.cpp 会解析 csvCaptureFrames=、csvCategories=、csvMetadata=、csvGpuStats、csvExecCmds= 等,但不解析 csvstatfile |
常见错误
以为 Test 构建会保留 check/ensure: 在 Test 和 Shipping 中,DO_CHECK 和 DO_ENSURE 分别对应 USE_CHECKS_IN_SHIPPING / USE_ENSURES_IN_SHIPPING,且两者默认值均为 0。如果表达式必须始终执行,请使用 verify(),或设置 bUseChecksInShipping。
在日志或断言中执行逻辑: UE_LOG(LogMyGame, Log, TEXT("%d"), AdvanceCounter()); 在启用 NO_LOGGING 后就不再递增计数器,而 check(Init()) 在 DO_CHECK 为 0 后就不再初始化。先将结果赋给局部变量,再对其记录日志或进行断言。
使用 Log 却期待控制台输出: Display 会将内容输出到控制台;Log 只会写入日志文件。“我的日志行怎么没出现”几乎总是因为使用了 Log 级别的消息。
缺少筛选标志: 每个 IMPLEMENT_*_AUTOMATION_TEST、DEFINE_SPEC 和 BEGIN_DEFINE_SPEC 都必须且只能指定 SmokeFilter/EngineFilter/ProductFilter/PerfFilter/StressFilter/NegativeFilter 中的一个,并至少指定一个上下文标志,否则 static_assert 会导致编译失败。
丢弃 CQTest 断言结果: 断言器方法带有 [[nodiscard]],因此 Assert.IsTrue(x); 会发出警告,并在失败后继续运行。务必写成 ASSERT_THAT(IsTrue(x));。
在 Shipping 或 Test 游戏构建中使用 DrawDebug*: 这些调用会变成空操作存根,但其参数(追踪、字符串格式化)仍会执行。请将整个代码块包在 #if ENABLE_DRAW_DEBUG 中。
跳过 Super::StartTest(): AFunctionalTest::StartTest 会触发 Blueprint ReceiveStartTest 事件,因此省略 Super:: 会导致同一测试 Actor 中由 Blueprint 编写的所有步骤都被静默禁用。
相关技能
ue-cpp-foundations— UObject 基础知识、委托、容器、UFUNCTION/UPROPERTY 说明符表ue-module-build-system— Build.cs 和 Target.cs 配置、bBuildDeveloperTools、测试模块设置、插件布局ue-editor-tools— 承载测试和调试 UI 的编辑器实用工具、命令let 和 Slate 工具ue-physics-collision— 可使用DrawDebug*可视化结果的追踪和扫描查询ue-ai-navigation— 行为树、EQS 和 AI 端 Gameplay Debugger 类别ue-async-threading— 潜在命令和task追踪通道可观测的任务图和异步工作ue-gameplay-framework— GameMode、PlayerController、HUD 和 CheatManager,即Exec函数的承载对象ue-materials-rendering— 材质实例、参数集合、渲染目标和后期处理