Debugging techniques
Pick the tool that fits the question:
| Question | Tool |
|---|---|
| What value / did this run? | UE_LOG / ensure — see ue-logging-and-assertions |
| Where in space is this thing? | DrawDebug* helpers |
| What happened over the last N seconds? | Visual Logger |
| What is the AI/gameplay system thinking right now? | Gameplay Debugger |
| Crash / step through logic? | Native debugger (VS/Rider) |
| Frame time, draw calls, physics cost? | ue-profiling-and-optimization |
When to use this skill
- Behavior is wrong and you need values, positions, or state visible during play.
- Visualizing traces, detection ranges, AI targets, nav paths, or spawn points.
- An intermittent bug whose cause unfolds over several seconds — record with the Visual Logger and scrub the timeline.
- Stepping through C++ to find a crash or logic error; attaching a debugger and reading a callstack.
- Adding a custom in-game debug overlay for a new system (Gameplay Debugger custom category).
Logging and assertions — start here
For "what is the value / did this path run" questions, a UE_LOG line is fastest. For
invariant violations, prefer ensure over check in gameplay code so the editor survives and
surfaces the problem without a crash. Full coverage — log categories, verbosity, UE_LOG,
check, ensure, checkf, ensureMsgf — is in ue-logging-and-assertions.
// Non-fatal invariant — logs callstack, editor keeps running, fires once per session by default
if (!ensure(MyComponent != nullptr))
return;
// Fatal invariant — crashes intentionally when the assumption is broken in dev builds
check(GetWorld() != nullptr);
checkf(Health >= 0.f, TEXT("Health underflowed to %.1f on %s"), Health, *GetName());
Source: Runtime/Core/Public/Misc/AssertionMacros.h — check:229, checkf:259, ensure
block begins at line 368 with DO_ENSURE guard; ensure fires once per call-site per session,
ensureAlways fires every time.
Debug drawing (visualize in the world)
DrawDebug* functions render geometry directly into the viewport — lines, spheres, boxes,
capsules, text labels, arrows, cones, and more. They require no actor or component; call them
from any code that has a UWorld*.
#include "DrawDebugHelpers.h"
// Line — show a trace or path segment
DrawDebugLine(GetWorld(), TraceStart, TraceEnd, FColor::Red,
/*bPersistentLines=*/false, /*LifeTime=*/2.f, /*DepthPriority=*/0, /*Thickness=*/1.f);
// Sphere — visualize a detection radius or spawn point
DrawDebugSphere(GetWorld(), GetActorLocation(), DetectionRadius,
/*Segments=*/16, FColor::Green, false, 0.f);
// Text label at a world position — tag an actor with live data
DrawDebugString(GetWorld(), GetActorLocation() + FVector(0,0,80),
FString::Printf(TEXT("HP: %d"), Health),
/*TestBaseActor=*/nullptr, FColor::White, /*Duration=*/0.f);
// Capsule — match a character's collision shape
DrawDebugCapsule(GetWorld(), GetActorLocation(), CapsuleHalfHeight, CapsuleRadius,
FQuat::Identity, FColor::Yellow, false, 1.f);
// Directional arrow — show a velocity or AI direction
DrawDebugDirectionalArrow(GetWorld(), Tail, Head, /*ArrowSize=*/20.f,
FColor::Cyan, false, 1.f);
Key parameters shared by most functions:
bPersistentLines— iftrue, stays untilFlushPersistentDebugLinesis called.LifeTime— seconds the shape is visible;-1means one frame;0.fon most shapes means "use duration", which defaults to one frame from the game's perspective.DepthPriority— higher priority draws on top; useSDPG_Foreground(1) to prevent geometry from occluding the shape.Thickness— line thickness in screen pixels for wire shapes.
All functions are guarded by #if ENABLE_DRAW_DEBUG (which evaluates to false in Shipping),
so they are automatically stripped from release builds — no manual #if guards needed.
Source: Runtime/Engine/Public/DrawDebugHelpers.h — DrawDebugLine:22, DrawDebugPoint:24,
DrawDebugSphere:45, DrawDebugBox:28, DrawDebugCapsule:57, DrawDebugString:52,
DrawDebugDirectionalArrow:26, DrawDebugCone:49, DrawDebugCylinder:47,
DrawDebugCapsule:57, DrawDebugSolidBox:68, FlushPersistentDebugLines:20.
Full shape catalog and persistent-line patterns: references/draw-debug-and-console.md.
On-screen debug messages
For transient HUD-free readouts — values that update each frame without spamming the log:
// Keyed message: same key overwrites the previous value each frame
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(
/*Key=*/1, // unique int key; INDEX_NONE appends
/*TimeToDisplay=*/0.f, // 0 = one frame, positive = linger
FColor::Yellow,
FString::Printf(TEXT("Speed: %.1f"), Speed)
);
}
Use Key values unique per message so they overwrite rather than stack. INDEX_NONE (-1)
appends a new line every call — useful for single-shot notifications, noisy if called every tick.
GEngine->ClearOnScreenDebugMessages() removes all messages at once.
Source: Runtime/Engine/Classes/Engine/Engine.h — AddOnScreenDebugMessage (uint64 key):2197,
AddOnScreenDebugMessage (int32 key):2200, ClearOnScreenDebugMessages:2206,
RemoveOnScreenDebugMessage:2209.
Visual Logger (events over time)
The Visual Logger records text and spatial shapes per-actor with timestamps. After recording you
can scrub a timeline in the Visual Logger window and see exactly what each actor saw, decided,
and drew at each moment — invaluable for AI and intermittent bugs that plain logging cannot
capture. For spatial/temporal bugs, prefer UE_VLOG* over flooding the output log with
UE_LOG.
#include "VisualLogger/VisualLogger.h"
// Text entry — attached to 'this' actor's timeline row
UE_VLOG(this, LogMyAI, Verbose, TEXT("Acquired target %s at dist %.1f"),
*Target->GetName(), Distance);
// Sphere at a world position — mark a last-known position
UE_VLOG_LOCATION(this, LogMyAI, Verbose, LastKnownPos, /*Radius=*/30.f,
FColor::Yellow, TEXT("LKP"));
// Segment between two points — visualize a planned move
UE_VLOG_SEGMENT(this, LogMyAI, Verbose, Start, Goal, FColor::Cyan, TEXT("Path"));
// Wire sphere — perception radius at this frame
UE_VLOG_WIRESPHERE(this, LogMyAI, Verbose, GetActorLocation(), PerceptionRadius,
FColor::Green, TEXT("Hear radius"));
// Conditional variant — only logs if the condition is true (avoids branching boilerplate)
UE_CVLOG(bTargetVisible, this, LogMyAI, Verbose, TEXT("Target visible"));
All UE_VLOG* macros are no-ops unless ENABLE_VISUAL_LOG is defined (true in non-Shipping
builds). The macros short-circuit on FVisualLogger::IsRecording() so there is no overhead
when the Visual Logger is inactive. UE_VLOG_UELOG emits to both the Visual Logger and the
standard output log simultaneously.
Source: Runtime/Engine/Public/VisualLogger/VisualLogger.h — macro block starts at line 43;
FVisualLogger class at line 223; SetIsRecording:581, IsRecording (static inline):575,
SetIsRecordingToFile:693, FVisualLogger::Get():567.
Full macro catalog, redirection, and file recording: references/visual-logger.md.
Gameplay Debugger (live categories in PIE)
The Gameplay Debugger is an in-game overlay activated by the apostrophe key (') in PIE. Built-in
categories cover the AI subsystem (Behavior Trees, EQS, perception, navigation), abilities, and
more. You can add custom categories to visualize your own systems alongside the built-in data.
Registering a custom category
// In your module's StartupModule():
#if WITH_GAMEPLAY_DEBUGGER
IGameplayDebugger& GDB = IGameplayDebugger::Get();
GDB.RegisterCategory(
TEXT("MySystem"),
IGameplayDebugger::FOnGetCategory::CreateStatic(
&FMySystemDebuggerCategory::MakeInstance),
EGameplayDebuggerCategoryState::EnabledInGameAndSimulate
);
GDB.NotifyCategoriesChanged();
#endif
// In ShutdownModule():
#if WITH_GAMEPLAY_DEBUGGER
if (IGameplayDebugger::IsAvailable())
{
IGameplayDebugger::Get().UnregisterCategory(TEXT("MySystem"));
}
#endif
Implementing the category
// MySystemDebuggerCategory.h
#if WITH_GAMEPLAY_DEBUGGER
#include "GameplayDebuggerCategory.h"
class FMySystemDebuggerCategory : public FGameplayDebuggerCategory
{
public:
static TSharedRef<FGameplayDebuggerCategory> MakeInstance()
{
return MakeShared<FMySystemDebuggerCategory>();
}
// [AUTH] — runs on server/standalone, collects data to replicate
virtual void CollectData(APlayerController* OwnerPC, AActor* DebugActor) override;
// [LOCAL] — runs on client, draws the collected data
virtual void DrawData(APlayerController* OwnerPC,
FGameplayDebuggerCanvasContext& CanvasContext) override;
};
#endif
In CollectData, call AddTextLine(TEXT("{yellow}Key: {white}Value")) and AddShape(...) to
populate replicated data. In DrawData, the lines and shapes added by CollectData render
automatically before your custom draw code runs.
WITH_GAMEPLAY_DEBUGGER is not defined in Shipping builds by default; guard all category code
with it. Add SetupGameplayDebuggerSupport(Target) to your Build.cs to link correctly.
Source: Runtime/GameplayDebugger/Public/GameplayDebugger.h — IGameplayDebugger interface,
RegisterCategory:78, UnregisterCategory:79, NotifyCategoriesChanged:80.
Runtime/GameplayDebugger/Public/GameplayDebuggerCategory.h — FGameplayDebuggerCategory:48,
CollectData:56, DrawData:59, AddTextLine:68, AddShape:71.
Full custom-category walkthrough: references/gameplay-debugger.md.
Native debugger (C++ breakpoints and inspection)
Attach Visual Studio or Rider to the Unreal Editor process (or launch with -debug). Unreal
ships .natvis files so the watch window renders FString, FName, TArray, TMap, and
FVector as readable values rather than raw memory.
Practical habits:
- Break on
ensurefailures: in VS, add a conditional breakpoint onFDebug::EnsureFailedor use "Break on all exceptions" for C++ in the Exception Settings. - For crash callstacks, read from the top down; the first frame in project code is usually where the bad pointer or bad assumption lives.
checkNoEntry()marks code paths that should be unreachable; hitting one in the debugger means the control flow is wrong, not the data.
Live Coding
Live Coding (Ctrl+Alt+F11) recompiles function bodies without restarting the editor. It is
safe for changing logic inside existing functions. It cannot handle:
- Adding or removing
UPROPERTY/UFUNCTIONmembers. - Changing class layout (new member variables, changed base classes).
- Changes to header files that affect other translation units.
After any of these, restart the editor for a full hot reload or use Unreal Build Tool. The common failure mode is that Live Coding "succeeds" on a structural change but the old CDO or Blueprint instance retains the stale layout, causing silent data corruption or crashes on the next frame. When in doubt after a crash following Live Coding, do a full rebuild.
Console commands and cvars
Open the console with the tilde key (~). Useful dev commands:
| Command | What it shows |
|---|---|
stat fps |
Frame rate overlay |
stat unit |
Game/render/GPU thread timing |
stat game |
Per-system game-thread costs |
stat ai |
AI tick costs |
showdebug ai |
AI state overlay (overlaps with Gameplay Debugger) |
showdebug input |
Current input action states |
showdebug collision |
Collision channel visualization |
displayall ClassName PropertyName |
Live broadcast of a UPROPERTY value for all instances |
dumpticks |
Lists all ticking actors and their tick group |
r.VisualizeOccludedPrimitives 1 |
Highlights culled primitives |
ToggleDebugCamera |
Detaches the camera from the pawn for free-fly inspection |
Slomo 0.1 |
Slows time by 10x (via UCheatManager) |
t.MaxFPS 10 |
Caps frame rate for slow-motion inspection |
CVars follow the pattern r.* (rendering), s.* (streaming), p.* (physics), t.*
(time), ai.* (AI). Use DumpConsoleCommands or start typing in the console to search.
Exec functions on any UObject in the input chain (pawn, controller, game mode, cheat
manager) can be marked UFUNCTION(Exec) and invoked from the console by name. UCheatManager
(on APlayerController) is the canonical home for dev-only cheats.
// In your PlayerController or CheatManager subclass:
UFUNCTION(Exec)
void ToggleMyDebugVis()
{
bShowMyDebugOverlay = !bShowMyDebugOverlay;
}
Source: Runtime/Engine/Classes/GameFramework/CheatManager.h — UCheatManager:98,
UFUNCTION(exec) cheat examples: FreezeFrame:160, Fly:172, God:184, Slomo:188.
Full stat commands, cvar authoring, and displayall:
references/draw-debug-and-console.md.
Decision guide
Bug type → Tool
──────────────────────────────────────────────────────
Wrong value / missing execution → UE_LOG / ensure
Wrong position / shape / range → DrawDebug* or UE_VLOG_LOCATION
AI makes wrong decision → Gameplay Debugger + Visual Logger
Intermittent / race / sequence → Visual Logger (scrub timeline)
Crash / access violation → Native debugger + callstack
Perf regression (fps/ms) → ue-profiling-and-optimization
Gotchas
DrawDebug*andGEngine->AddOnScreenDebugMessagein Shipping — both are stripped. They are dev tools; never rely on them for gameplay feedback.- Live Coding after structural changes — silent staleness or immediate crash; do a full hot-reload restart instead.
UE_LOGspam every tick instead of the Visual Logger — produces GBs of output; useUE_VLOG*for spatial/temporal information.checkwhenensurewas wanted — crashes the editor and loses unsaved work; preferensurein gameplay code for non-fatal invariants.UE_VLOG*with no owner — passthis(anAActororUObject); a null owner silently drops the entry.- Forgetting
#if WITH_GAMEPLAY_DEBUGGER— linker error in Shipping if the GameplayDebugger module is excluded; always guard category code. DrawDebugStringwith a non-nullTestBaseActor— the string position is relative to that actor, which can be confusing if the actor moves; passnullptrfor absolute positions.LifeTime = -1.f— inDrawDebugLine/DrawDebugSphere,-1.fmeans "use the default duration" (one frame), NOT "persist forever". UsebPersistentLines = truefor persistence, then callFlushPersistentDebugLinesto clear.
References & source material
Engine source (UE 5.8, under Engine/Source/):
Runtime/Engine/Public/DrawDebugHelpers.h—DrawDebugLine:22,DrawDebugPoint:24,DrawDebugSphere:45,DrawDebugBox:28,DrawDebugCapsule:57,DrawDebugString:52,DrawDebugDirectionalArrow:26,DrawDebugCone:49,DrawDebugSolidBox:68;ENABLE_DRAW_DEBUGmacro:14.Runtime/Engine/Classes/Engine/Engine.h—AddOnScreenDebugMessage(uint64):2197,AddOnScreenDebugMessage(int32):2200,ClearOnScreenDebugMessages:2206,RemoveOnScreenDebugMessage:2209.Runtime/Engine/Public/VisualLogger/VisualLogger.h—UE_VLOGmacro:43,UE_VLOG_LOCATION:55,UE_VLOG_SEGMENT:49,UE_VLOG_SPHERE:58,UE_VLOG_UELOG:46;FVisualLogger:223,SetIsRecording:581,IsRecording:575,FVisualLogger::Get():567.Runtime/GameplayDebugger/Public/GameplayDebugger.h—IGameplayDebugger:50,RegisterCategory:78,UnregisterCategory:79,NotifyCategoriesChanged:80.Runtime/GameplayDebugger/Public/GameplayDebuggerCategory.h—FGameplayDebuggerCategory:48,CollectData:56,DrawData:59,AddTextLine:68,AddShape:71.Runtime/Core/Public/Misc/AssertionMacros.h—check:229,checkf:259,ensureblock:368 (see alsoue-logging-and-assertions).Runtime/Engine/Classes/GameFramework/CheatManager.h—UCheatManager:98, exampleUFUNCTION(exec)cheat bodies:160–217.
Official docs (UE 5.8):
- Visual Logger — https://dev.epicgames.com/documentation/unreal-engine/visual-logger-in-unreal-engine
- Gameplay Debugger — https://dev.epicgames.com/documentation/unreal-engine/using-the-gameplay-debugger-in-unreal-engine
- Console Variables — https://dev.epicgames.com/documentation/unreal-engine/console-variables-cplusplus-in-unreal-engine
- Live Coding — https://dev.epicgames.com/documentation/unreal-engine/using-live-coding-to-recompile-unreal-engine-applications-at-runtime
Related skills: ue-logging-and-assertions, ue-ai-and-navigation, ue-profiling-and-optimization.
Deep-dive references in this skill:
- references/visual-logger.md — full
UE_VLOG*macro catalog, redirection, file recording, Rewind Debugger integration. - references/gameplay-debugger.md — custom category
walkthrough, replication, key bindings,
Build.cssetup. - references/draw-debug-and-console.md — full shape catalog, persistent lines, on-screen messages, stat/console command reference.