How I Debug My Unreal Engine 5 Games (And How You Can Too)
- Print String is your quickest friend for immediate visual feedback during gameplay, but use keys properly to avoid flooding your screen (especially in Tick events) - UELOG creates persistent, fi...
Key Takeaways
- Print String is your quickest friend for immediate visual feedback during gameplay, but use keys properly to avoid flooding your screen (especially in Tick events)
- UE_LOG creates persistent, filterable logs that survive your play session—perfect for tracking down bugs you can't reproduce live
- DrawDebugHelpers lets you visualize spatial problems like raycasts, collision volumes, and AI behavior directly in your 3D viewport
- Visual Logger records timeline-based gameplay data so you can scrub frame-by-frame to catch single-frame bugs that traditional debugging misses
- Blueprint debugging gives you breakpoints and watch values with real-time execution flow visualization—no C++ debugger needed
- Most debugging tools automatically compile out in Shipping builds, giving you zero performance overhead in production
- Always wrap
GEnginecalls in null checks to prevent crashes during editor startup or on dedicated servers - Custom log categories help you organize debugging output by system (LogGameplay, LogAI, LogNetwork) for easier filtering and analysis
Ready to Start Building Your First Game?
Now that you've learned about unreal engine 5 debugging tools, it's time to put this knowledge into practice. Whether you're working with Print String for quick tests or setting up sophisticated Visual Logger workflows, these debugging skills will serve you throughout your entire game development journey.
Want to go from the basics to building professional game experiences? Check out our comprehensive course that takes you from zero to shipping your first complete game: Start Your Game Development Journey
The Debugging Problem That Burned Half My Day
Here's the thing—I remember sitting in front of Unreal Engine for the first time, watching my character teleport randomly across the level. No error messages. No warnings. Just... chaos. I spent hours adding Print Strings everywhere, trying to figure out which function was misbehaving. Turned out I was using the default key (-1), and my screen was flooded with hundreds of overlapping messages per second. I couldn't see anything useful.
Game debugging isn't like debugging a web app where you can easily reproduce issues in a controlled environment. You're dealing with physics, AI, rendering, player input, and gameplay logic—all happening in real-time at 60+ frames per second. A bug might only appear in a single frame, or only when two specific systems interact in a particular order.
The good news? Unreal Engine 5 gives you an incredible toolkit for tracking down these problems. From simple on-screen messages to timeline-based recording systems that let you scrub through gameplay frame-by-frame, these tools have saved me countless hours of frustration. Let me show you how to use them effectively.
Why Unreal Engine 5 Debugging is Different From Regular Programming
Been there—debugging games is nothing like debugging traditional software. In a web application, you can pause execution, inspect variables, and reproduce issues in a controlled environment. Easy.
Game development? You're dealing with complex interactions between gameplay logic, physics simulations, AI decision-making, rendering systems, and player input—all happening simultaneously in real-time. A bug might only show up when the player jumps while firing a weapon during a specific animation frame. Good luck reproducing that consistently.
This is where Unreal Engine 5's debugging ecosystem becomes essential. UE5 provides three primary approaches to tackle these challenges:
Print String and on-screen messages give you immediate visual feedback right in your game viewport. Perfect for tracking values that change rapidly during active gameplay.
Text logging systems like UE_LOG create persistent, filterable debug records that you can review after your play session. These logs save to files and the Output Log window, letting you search and filter by category.
Visual debugging tools help you analyze spatial and temporal problems. DrawDebugHelpers draws shapes in 3D space to visualize raycasts and collision volumes, while Visual Logger records timeline-based data you can scrub through frame-by-frame.
Understanding when and how to use each tool is critical for efficient debugging workflows. Let me show you how each one works.
Your First Debugging Tool: Print String On-Screen Messages
What Print String Actually Does
Print String is the most accessible debugging tool in unreal engine 5 debugging, especially if you're working with Blueprints. It displays text messages directly on your game viewport and writes them to the Output Log window during runtime. This immediate visual feedback makes it invaluable for tracking variable values, verifying function execution order, and validating logic flow while you're actively playing.
I use Print String constantly during early prototyping. When I need to know "did this function actually fire?" or "what's the current value of this variable right now?", Print String gives me the answer instantly without switching windows.
Setting Up Print String in Blueprints
To use print string unreal engine in Blueprints:
- Right-click anywhere in your Event Graph
- Search for "Print String"
- Connect it to any execution pin in your logic flow
- Type your text directly or connect variables to the "In String" input
That's it. Next time that execution path runs, you'll see your message on screen.
Understanding the Six Parameters
The Print String node has six configurable parameters that control how and where your message appears:
In String: This is your primary text input. You can type text directly or connect variables and function outputs. Whatever you put here displays on screen or logs to the Output window.
Print to Screen (Boolean): Controls whether the message appears overlaid on your game viewport. Default is true. When enabled, text appears in the upper-left corner of the screen. I usually keep this enabled during development, but disable it when I only want log file output.
Print to Log (Boolean): Determines whether the message writes to the Output Log window. Default is true. This provides a persistent record you can review after on-screen messages fade. I recommend leaving this enabled—you'll often need to reference messages after they disappear from the viewport.
Text Color (Linear Color): Customizes the color of on-screen messages. Super useful for distinguishing message types at a glance—I use red for errors, yellow for warnings, green for success states. Note: This only affects the on-screen display, not the Output Log color.
Duration (Float): Specifies how many seconds the message remains visible on screen. Default is 2.0 seconds. Set to 0 for one-frame display, or higher values for longer persistence. For frequently updated values in Tick events, I use 0.0-0.5 seconds to avoid stacking messages.
Key (String): This is critical for performance and readability. The key acts as a unique identifier:
- Empty or default: Each call creates a new line on screen, causing flooding if called repeatedly
- Specific string value: Messages with the same key replace the previous message instead of stacking
- Essential for displaying frequently updated values without visual clutter
Let me show you why the Key parameter matters so much.
The C++ Way: AddOnScreenDebugMessage
If you're working in C++, the equivalent function is GEngine->AddOnScreenDebugMessage(). Here's the full signature:
void UEngine::AddOnScreenDebugMessage(
uint64 Key,
float TimeToDisplay,
FColor DisplayColor,
const FString& DebugMessage,
bool bNewerOnTop = true,
const FVector2D& TextScale = FVector2D::UnitVector
)
Parameter breakdown:
- Key (uint64): Unique identifier. Use -1 for new messages (warning: this floods your screen), or positive integers to replace messages with the same key
- TimeToDisplay (float): Duration in seconds (e.g., 5.0f)
- DisplayColor (FColor): Text color using predefined colors like FColor::Red, FColor::Cyan, or custom FColor(R, G, B)
- DebugMessage (FString): The text to display. Use the TEXT() macro for string literals
- bNewerOnTop (bool): Optional parameter that determines if newer messages appear above or below older ones
- TextScale (FVector2D): Optional parameter to control text size
Critical safety pattern—always use this:
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, TEXT("Player took damage!"));
}
I learned this the hard way. The GEngine pointer can be null during editor startup, in dedicated servers, or during early initialization. Accessing it without validation causes immediate crashes. Always wrap your GEngine calls in a null check.
Required header: You'll need to include #include "Engine/Engine.h" at the top of your file.
Displaying Variables in C++
Here's how to display dynamic values:
if (GEngine)
{
FVector Location = GetActorLocation();
GEngine->AddOnScreenDebugMessage(
0, // Key 0 replaces previous message
0.1f, // Short duration for frequent updates
FColor::Yellow,
FString::Printf(TEXT("Player Location: %s"), *Location.ToString())
);
}
Notice the *Location.ToString() syntax—you need to dereference FStrings with the asterisk operator when using them with Printf-style formatting.
Advanced Print String Techniques
Output Log color tagging in Blueprints:
You can automatically color messages in the Output Log by prefixing your string with special keywords:
- "Warning: Low health" appears in yellow in the Output Log
- "Error: Failed to load" appears in red in the Output Log
This only affects log output, not the on-screen display color (which is controlled by the Text Color parameter).
Combining text and variables in Blueprints:
Use the Append node to concatenate static text with variable values. You can add multiple pins to combine several strings into a formatted message. For more complex displays, use the Format Text node with placeholders like {VariableName} for clean, readable debug messages.
Real-World Code Examples
Tracking event triggers:
void AMyActor::BeginPlay()
{
Super::BeginPlay();
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 3.0f, FColor::Green,
FString::Printf(TEXT("%s spawned successfully"), *GetName()));
}
}
Monitoring state machines:
void AMyCharacter::SetState(EPlayerState NewState)
{
CurrentState = NewState;
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(0, 2.0f, FColor::Cyan,
FString::Printf(TEXT("State: %s"), *UEnum::GetValueAsString(NewState)));
}
}
Debugging collision detection:
void AMyActor::OnOverlapBegin(AActor* OtherActor)
{
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(1, 1.0f, FColor::Orange,
FString::Printf(TEXT("Collision with: %s"), *OtherActor->GetName()));
}
}
Monitoring values every frame in Tick:
void AMyCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (GEngine && bShowDebugInfo)
{
// Use different keys to prevent overlap
GEngine->AddOnScreenDebugMessage(0, 0.0f, FColor::White,
FString::Printf(TEXT("Speed: %.2f"), GetVelocity().Size()));
GEngine->AddOnScreenDebugMessage(1, 0.0f, FColor::Yellow,
FString::Printf(TEXT("Health: %.0f"), Health));
}
}
Notice how I use different keys (0 and 1) to prevent the messages from overlapping, and I use a very short duration (0.0f) since these update every frame.
When to Actually Use Print String
Best situations for Print String:
- Quick visual feedback during active gameplay testing
- Seeing values change in real-time without switching windows
- Showing information to team members during playtesting sessions
- Prototyping and rapid iteration
- One-time events like button presses or item collection
When to avoid Print String:
- High-frequency logging (every frame without proper key management)
- Production or shipping builds (it's automatically disabled anyway)
- Detailed debugging requiring filtering by category
- Persistent records for post-session analysis (use UE_LOG instead)
The Professional's Choice: UE_LOG Text Logging
Why UE_LOG is Different
UE_LOG is Unreal Engine's primary text logging mechanism for C++ development. While Print String gives you immediate visual feedback, UE_LOG writes formatted messages to log files and the Output Log window for persistent, filterable debug records. Unlike Print String, UE_LOG is designed for comprehensive logging with minimal performance overhead and sophisticated organization through categories and verbosity levels.
Once I started working on larger projects, I realized Print String wasn't enough. I needed logs I could search through hours after a bug occurred, filter by system, and review without replaying the entire session. That's where UE_LOG became essential.
The Basic Syntax
UE_LOG(CategoryName, VerbosityLevel, TEXT("Message with %s"), *FStringVariable);
Three required parameters:
- Category Name: Organizational bucket for logs (e.g., LogTemp, LogAI, LogNetwork)
- Verbosity Level: Severity/importance (Fatal, Error, Warning, Display, Log, Verbose, VeryVerbose)
- Format String: Printf-style formatted message wrapped in the TEXT() macro
Organizing Logs with Categories
Log categories organize output into manageable streams. LogTemp is the built-in temporary category for quick debugging, but production code benefits from custom categories.
I always create custom categories for different systems in my projects. It makes filtering logs infinitely easier when you're trying to track down a networking bug versus an AI pathfinding issue.
Creating custom categories:
In your header file (.h):
DECLARE_LOG_CATEGORY_EXTERN(LogMyGame, Log, All);
In your source file (.cpp):
DEFINE_LOG_CATEGORY(LogMyGame);
Declaration variants you'll encounter:
- DECLARE_LOG_CATEGORY_EXTERN / DEFINE_LOG_CATEGORY: For categories shared across multiple files (most common approach)
- DEFINE_LOG_CATEGORY_STATIC: For single .cpp file usage only
- DECLARE_LOG_CATEGORY_CLASS / DEFINE_LOG_CATEGORY_CLASS: For class-scoped categories
Understanding declaration parameters:
DECLARE_LOG_CATEGORY_EXTERN(CategoryName, DefaultVerbosity, CompileTimeVerbosity);
- DefaultVerbosity: The runtime default verbosity level
- CompileTimeVerbosity: Maximum compiled verbosity (typically set to "All"). Logs more verbose than this are completely removed at compile time, giving you zero performance overhead
The Seven Verbosity Levels
UE_LOG uses seven verbosity levels in the ELogVerbosity enum, ordered by severity:
Fatal (1): Crashes the application after logging, even with logging disabled. Reserved for truly unrecoverable errors. Appears in red.
Error (2): Serious problems requiring immediate attention. Printed to console and log files in red text.
Warning (3): Potential issues or deprecated usage. Printed in yellow text.
Display (4): General informational messages shown in console and log files. Uses the default color.
Log (5): Detailed information written only to log files, not the in-game console. This is ideal for frequent logging without console spam.
Verbose (6): Detailed tracing information for debugging specific systems. Log files only.
VeryVerbose (7): Extremely detailed logging for deep debugging sessions. Log files only, typically disabled by default.
Hierarchical filtering works like this: Setting a category to "Warning" suppresses Display, Log, Verbose, and VeryVerbose messages while allowing Warning, Error, and Fatal messages through.
Printf Format Specifiers for UE_LOG
UE_LOG uses C-style printf formatting:
- %s: String (TCHAR*). FStrings require the dereference operator with
*:*FStringVar - %d: Signed 32-bit integer (int32, int)
- %f: Floating-point number (float, double)
- %.2f: Float with 2 decimal precision
- %u: Unsigned integer
Real examples:
int32 Health = 100;
float Speed = 5.75f;
FString PlayerName = TEXT("Hero");
FVector Location = FVector(1.0f, 2.0f, 3.0f);
UE_LOG(LogTemp, Warning, TEXT("Health: %d"), Health);
UE_LOG(LogTemp, Display, TEXT("Speed: %.2f"), Speed);
UE_LOG(LogTemp, Log, TEXT("Player: %s"), *PlayerName);
UE_LOG(LogTemp, Verbose, TEXT("Location: %s"), *Location.ToString());
UE_LOG(LogTemp, Error, TEXT("Player %s at %s with health %d"),
*PlayerName, *Location.ToString(), Health);
Handling booleans:
UE_LOG(LogTemp, Log, TEXT("IsAlive: %s"), (bIsAlive ? TEXT("true") : TEXT("false")));
Working with Unreal types:
FVector, FRotator, FName, and other Unreal types provide ToString() methods that return FStrings. You must dereference these with the * operator when using them in UE_LOG.
The Modern Alternative: UE_LOGFMT (UE5.2+)
If you're using Unreal Engine 5.2 or later, UE_LOGFMT provides cleaner syntax with automatic type handling:
#include "Logging/StructuredLog.h"
UE_LOGFMT(LogTemp, Log, "Simple message without TEXT macro");
Using positional parameters:
UE_LOGFMT(LogTemp, Warning, "Loading '{0}' failed with error {1}", PackageName, ErrorCode);
Using named parameters (my preference):
UE_LOGFMT(LogTemp, Error, "Player {Name} died at {Location} with {Health} health",
("Name", PlayerName),
("Location", Location),
("Health", Health));
UE_LOGFMT automatically handles FString, bool, and numeric types without format specifiers or dereference operators. Despite the advantages, UE_LOG remains widely used due to legacy codebases and developer familiarity. I still use UE_LOG in most projects because it's what everyone on the team knows.
Controlling Verbosity at Multiple Levels
You can configure verbosity at multiple levels, with later configurations overriding earlier ones:
1. Compile-time (in the declaration):
DECLARE_LOG_CATEGORY_EXTERN(LogMyGame, Log, Warning); // Only Warning and above compiled
2. Engine.ini or DefaultEngine.ini configuration:
[Core.Log]
LogOnline=Verbose
LogMyGame=Display
3. Command-line arguments:
-LogCmds="LogMyGame Verbose, LogTemp Off"
4. Runtime console command:
Log LogMyGame Verbose
This flexibility lets you debug specific systems without rebuilding.
Where to Actually View Your Logs
Output Log Window:
Access it via Window → Developer Tools → Output Log in the editor. Features include:
- Category filtering: Toggle visibility per category
- Search functionality: Text-based filtering
- Verbosity color coding: Errors (red), Warnings (yellow), Display (white)
- Real-time updates during Play-in-Editor (PIE)
Log files on disk:
Logs are automatically saved to ProjectFolder/Saved/Logs/ as timestamped .txt files. These files contain more detail than the Output Log window, particularly Verbose and VeryVerbose messages that don't display in the in-game console.
Console commands for log management:
log list: Lists all categories and current verbosity levelsLog [Category] [Verbosity]: Changes category verbosity at runtimeshowlog: Opens the log window (useful in packaged builds if console is enabled)
Setting Up Custom Categories (Complete Example)
Here's how I typically set up logging for a new project:
MyProjectLogging.h:
#pragma once
#include "CoreMinimal.h"
#include "Logging/LogMacros.h"
DECLARE_LOG_CATEGORY_EXTERN(LogGameplay, Log, All);
DECLARE_LOG_CATEGORY_EXTERN(LogUI, Log, All);
DECLARE_LOG_CATEGORY_EXTERN(LogAI, Log, All);
MyProjectLogging.cpp:
#include "MyProjectLogging.h"
DEFINE_LOG_CATEGORY(LogGameplay);
DEFINE_LOG_CATEGORY(LogUI);
DEFINE_LOG_CATEGORY(LogAI);
Usage in your game code:
#include "MyProjectLogging.h"
UE_LOG(LogGameplay, Warning, TEXT("Player took damage!"));
UE_LOG(LogUI, Display, TEXT("Menu opened"));
UE_LOG(LogAI, Verbose, TEXT("Pathfinding: %d nodes"), NodeCount);
When UE_LOG Makes Sense
Best suited for:
- Detailed debugging requiring filtering by category or verbosity
- Persistent records for post-session analysis
- Production and shipping builds (with appropriate verbosity settings)
- Systems that don't require immediate visual feedback
- High-frequency logging (compiles out in shipping builds with proper configuration)
Seeing is Believing: Visual Debugging with DrawDebugHelpers
Why Visual Debugging Changes Everything
DrawDebugHelpers is a C++ library that draws geometric primitives directly in your 3D viewport for debugging spatial relationships, collision detection, AI pathfinding, raycasts, and physics. Unlike text logging, visual debugging shows you exactly where things are happening in world space.
I can't count how many times I've spent 30 minutes staring at log output trying to figure out why a raycast wasn't hitting, only to draw it with DrawDebugLine and immediately see the problem—I had the start and end positions swapped. Visual debugging makes spatial problems obvious.
The Essential DrawDebug Functions
Required header:
#include "DrawDebugHelpers.h"
DrawDebugLine - Straight lines between two points (perfect for raycasts and directions):
DrawDebugLine(GetWorld(), StartLocation, EndLocation, FColor::Green, false, 2.0f, 0, 2.0f);
DrawDebugSphere - Wireframe spheres (collision radii, detection ranges):
DrawDebugSphere(GetWorld(), GetActorLocation(), 100.0f, 12, FColor::Red, false, 2.0f);
DrawDebugBox - Axis-aligned or oriented boxes (bounding volumes):
DrawDebugBox(GetWorld(), BoxCenter, BoxExtent, FColor::Yellow, false, 2.0f);
DrawDebugCapsule - Capsule shapes (character collision):
DrawDebugCapsule(GetWorld(), Center, HalfHeight, Radius, Rotation, FColor::Blue, false, 2.0f);
DrawDebugCone - Cone shapes (vision cones, attack ranges):
DrawDebugCone(GetWorld(), Origin, Direction, Length, AngleRadians, AngleRadians, 12, FColor::Yellow, false, 0.5f);
DrawDebugArrow - Directional arrows (movement direction, forces):
DrawDebugArrow(GetWorld(), Start, End, ArrowSize, FColor::Orange, false, 2.0f);
DrawDebugString - 3D text at world locations:
DrawDebugString(GetWorld(), TextLocation, TEXT("Debug Info"), nullptr, FColor::White, 2.0f);
Additional functions include DrawDebugCylinder, DrawDebugCamera, DrawDebugFrustum, DrawDebugPoint, DrawDebugPlane, DrawDebugSolidBox, and DrawDebugSolidPlane.
Understanding the Common Parameters
All DrawDebug functions follow a consistent parameter pattern:
const UWorld InWorld* - World context object, typically GetWorld()
Location/Position Parameters - FVector values defining positioning (LineStart, LineEnd, Center)
Size Parameters - Floats or FVectors defining dimensions (Radius, Extent, Length, Thickness)
FColor Color - RGB color values for visual distinction. Use predefined colors like FColor::Red or FColor::Cyan, or create custom colors with FColor(R, G, B)
bool bPersistentLines - Whether the shape persists between frames. Default is false (one frame only)
float LifeTime - Duration in seconds. Default is -1 (infinite if persistent). Examples:
- LifeTime = -1 with bPersistentLines = true: Infinite persistence
- LifeTime = 5.0f with bPersistentLines = true: Visible for 5 seconds
- bPersistentLines = false: One frame regardless of LifeTime
uint8 DepthPriority - Rendering depth priority. Default is 0
float Thickness - Line width in pixels. Default is 0
Persistent vs Temporary Drawing
Temporary drawing (default behavior):
- bPersistentLines = false
- Draws for one frame only
- Must be called every frame to remain visible
- Ideal for real-time continuous updates like active raycasts
- No performance accumulation
Persistent drawing:
- bPersistentLines = true
- Remains visible for LifeTime duration
- Useful for marking paths, recording movement history, or creating persistent markers
- Can impact performance if many shapes accumulate
- Clear accumulated shapes with
FlushPersistentDebugLines(GetWorld())
Real-World Visual Debugging Examples
Visualizing raycasts:
FHitResult HitResult;
bool bHit = GetWorld()->LineTraceSingleByChannel(HitResult, Start, End, ECC_Visibility);
FColor LineColor = bHit ? FColor::Red : FColor::Green;
DrawDebugLine(GetWorld(), Start, End, LineColor, false, 2.0f, 0, 2.0f);
if (bHit)
{
DrawDebugSphere(GetWorld(), HitResult.ImpactPoint, 10.0f, 8, FColor::Red, false, 2.0f);
}
This is one of my most-used patterns. Green line means the raycast found nothing, red line with a sphere means it hit something at that exact point.
AI detection radius visualization:
DrawDebugSphere(GetWorld(), AILocation, DetectionRadius, 16, FColor::Cyan, false, 0.1f);
Debugging collision volumes:
DrawDebugBox(GetWorld(), BoxCenter, BoxExtent, Rotation, FColor::Yellow, true, 5.0f, 0, 3.0f);
Visualizing AI pathfinding:
for (int i = 0; i < PathPoints.Num() - 1; i++)
{
DrawDebugArrow(GetWorld(), PathPoints[i], PathPoints[i+1],
50.0f, FColor::Orange, true, 10.0f, 0, 5.0f);
}
AI vision cone debugging:
DrawDebugCone(GetWorld(), AILocation, AIForwardVector, ConeLength,
FMath::DegreesToRadians(ConeAngle), FMath::DegreesToRadians(ConeAngle),
12, FColor::Purple, false, 0.5f, 0, 2.0f);
Blueprint Access to DrawDebug
Blueprint users can access DrawDebugHelpers functions through UKismetSystemLibrary:
- Draw Debug Box
- Draw Debug Line
- Draw Debug Sphere
- Draw Debug Arrow
- Draw Debug Capsule
- Draw Debug Cone
- Flush Persistent Debug Lines
Blueprint nodes provide the same parameters as the C++ functions.
My Color Coding System
While Epic doesn't provide official standards, I follow these community conventions (and they've served me well):
- Red: Errors, collisions detected, blocked paths, failed conditions
- Green: Success states, clear paths, available positions
- Blue: Information, neutral states, player-related data
- Yellow/Orange: Warnings, intermediate states
- White: Default/neutral information
- Cyan: AI-related visualization
- Magenta: Special markers, temporary indicators
Pro tip: Assign consistent colors per system. For example, all pathfinding debug lines = cyan, all physics debug shapes = yellow. This lets you identify debug information at a glance when multiple systems are drawing simultaneously.
The Best Part: Zero Cost in Production
DrawDebugHelpers are automatically disabled in Shipping and Test builds via this preprocessor define:
#define ENABLE_DRAW_DEBUG !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
All DrawDebug function calls are compiled out entirely in production builds, resulting in zero performance overhead. You can safely leave DrawDebug calls in your codebase without worrying about release performance.
Where to Actually See Your Debug Output
The Output Log Window
Access it via: Window → Developer Tools → Output Log
The Output Log is your primary logging window that displays real-time messages from both the engine and your game code during development. All UE_LOG messages appear here, along with system messages from engine subsystems.
Key features I use constantly:
Category filtering: Each log message belongs to a category (LogTemp, LogBlueprint, LogAI, etc.). You can toggle category visibility using the Categories dropdown to reduce noise and focus on the systems you're actually debugging. This is essential when you have multiple systems logging simultaneously.
Verbosity color coding:
- Fatal: Red with crash
- Error: Red
- Warning: Yellow
- Display: White/default color
- Log, Verbose, VeryVerbose: Default color (these are written to files, not the console)
Search functionality: Use the search bar to find specific strings in log output. This supports text-based filtering for quickly locating relevant messages when you have thousands of log entries.
Persistent display: Messages remain in the Output Log until you manually clear them or close the editor. This lets you review historical data from your entire session.
The Message Log Window
Access it via: Window → Developer Tools → Message Log
The Message Log is a specialized window focused on blueprint debugging, displaying compilation errors, asset configuration issues, and level design problems.
How Message Log differs from Output Log:
Blueprint-focused: It primarily shows Blueprint compilation errors and warnings with detailed context about what went wrong and where. Much more helpful than generic error codes.
Interactive navigation: Messages contain clickable hyperlinks that take you directly to the source of the problem—the specific Blueprint node, asset, or level actor. This dramatically speeds up debugging compared to manually searching for issues.
Auto-clearing behavior: By default, the Message Log clears its display each time you press Play. This ensures you only see issues relevant to your current session, not leftover messages from hours ago.
Organized by category: Messages are grouped by type (Blueprint errors, asset issues, level warnings) for easier navigation.
Designer-friendly: It's designed to provide quick feedback to non-programmers working with visual scripting and level design, using clear language and direct navigation rather than cryptic technical errors.
Log Files on Your Hard Drive
All log data is automatically saved to persistent text files that you can review later or share with team members.
Location: YourProject/Saved/Logs/
Primary file: YourProject.log (named after your project)
Content: These files contain more detailed information than the Output Log window, including Verbose and VeryVerbose messages that don't display in the in-game console. I often grep through these files when hunting for intermittent bugs.
Platform-specific locations for packaged builds:
- Windows Development builds: Same Saved/Logs directory in the packaged game folder
- Windows crash logs:
%USERPROFILE%\AppData\Local\Microsoft\Windows\WER\ReportQueue - Shipping builds: Logging is completely disabled by default (no log files generated)
Blueprint Debugging (No C++ Required)
Why Blueprint Debugging Feels Different
blueprint debugging provides visual, interactive debugging capabilities tailored specifically for visual scripting. Unlike traditional code debugging, Blueprint debugging emphasizes real-time execution flow visualization and accessible tools designed for non-programmers.
If you're coming from traditional programming, Blueprint debugging will feel surprisingly intuitive. If Blueprints are your first exposure to programming, you're lucky—this is one of the most accessible debugging experiences in game development.
The Breakpoint System
Breakpoints pause Blueprint execution at specific nodes, allowing you to inspect variable values and understand execution flow.
Setting breakpoints:
- Right-click any Blueprint node
- Select "Add Breakpoint"
- A solid red octagon appears in the upper-left corner of the node
When execution reaches a breakpoint during Play In Editor (PIE) or Simulate In Editor (SIE), the simulation pauses and the Blueprint Editor automatically navigates to that node. You can then inspect the current state of all your variables.
Breakpoint states you'll see:
- Active (solid red octagon): Execution will pause when this node is reached
- Disabled (outlined red octagon): Temporarily inactive without removal
- Invalid (yellow octagon with exclamation mark): Requires Blueprint recompilation to function
Managing breakpoints: Right-click the node or use the Blueprint Debugger window (accessible from the Tools menu or Debug menu) to enable, disable, or remove breakpoints. All breakpoints for a Blueprint are listed centrally for easy management.
Important limitation: Breakpoints only function during PIE/SIE sessions. They do not work in packaged builds and are completely compiled out of Shipping builds.
Watch Values and Variable Inspection
Watching pin values:
- Right-click any pin's name in the Blueprint Graph
- Select "Watch this value"
- The pin displays its current value after node execution
To stop watching, right-click and select "Stop watching this value". I use this constantly to track how values change as execution flows through the graph.
Interactive tooltips: When paused at a breakpoint, hover over any pin to see its current value in a tooltip. Note: Pins whose nodes haven't executed yet show "no debugging information available" because pin values only update after node execution.
The Data Flow Tab (UE5): The Data Flow tab provides consolidated viewing of all watched data with object filtering. You can:
- List all instances of a chosen Blueprint class
- Inspect any properties from that Blueprint
- View a consolidated call stack populated with current data
- Jump between different Blueprints to inspect property values
- Expand Arrays, Sets, Maps, and other complex data structures
Debug Object selection (critical step): When multiple instances of the same Blueprint exist in your level, you must select which instance to debug. Click the Debug Object dropdown in the Blueprint Editor toolbar and select the specific instance. Without this selection, breakpoints may not trigger. This confused me for weeks when I first started with Blueprints.
Stepping Through Your Blueprint Code
Blueprint debugging provides execution controls similar to traditional debuggers:
F10 - Step Over: Advances execution to the next node in the current graph without entering function calls. Use this to move through your logic line-by-line.
F11 - Step Into: Steps into function calls, allowing you to debug custom functions or macros. Great when you need to understand what's happening inside a function.
F7 - Compile: Compiles the current Blueprint. Useful when making changes during a debugging session.
Resume button: Continues execution until the next breakpoint or end of execution. Note: There's no dedicated keyboard shortcut—you need to use the toolbar button.
Active Wire visualization: When playing in the editor with the Blueprint open in another window, you'll see pulsating "Active Wires" showing which execution paths are currently running in real-time. This is excellent for quick logic verification without even setting breakpoints.
The Blueprint Debugger Window
Access via: Tools menu or Debug menu in the Blueprint Editor
The Blueprint Debugger window contains three main tabs:
Breakpoints Tab: Lists all breakpoints in the current Blueprint with options to quickly enable, disable, or navigate to each one. Super useful when you have breakpoints scattered across multiple graphs.
Execution Trace Stack: Shows a chronological list of executed nodes with the most recent at the top. This updates in real-time as you step through the graph. It's invaluable for understanding the code path that led to your current breakpoint.
Call Stack: Displays execution flow between Blueprint and C++ functions, showing you how different systems interact. This helps when debugging interactions between Blueprint and native code.
Blueprint vs C++ Debugging Reality Check
Accessibility: Blueprint debugging is significantly more accessible with visual error representations, logic flow visualization, and integrated tools. C++ debugging requires IDE integration, debugging symbols, and more technical knowledge.
Error messages: Blueprints provide user-friendly error messages for common mistakes like "Accessed None trying to read..." C++ errors can be cryptic, often requiring deep engine knowledge to interpret. Blueprint errors are generally easier to trace due to built-in safety mechanisms.
Real-time features: Blueprints excel at real-time debugging with immediate visual feedback. You can see data flowing through pins and observe node execution without recompiling. C++ debugging lacks this immediate visual feedback and requires recompilation for most changes.
Depth vs accessibility trade-off: While Blueprints offer excellent real-time debugging and accessibility, they lack the depth of C++ debugging tools. C++ debuggers can examine memory directly, track pointer references, analyze performance at the instruction level, and use advanced features like conditional breakpoints with complex expressions.
Visual Studio Integration (new in 2022 17.14+): Visual Studio 2022 17.14 and later offers unified blueprint debugging + C++ debugging with integrated call stacks showing both Blueprint frames and C++ frames together. You can view Blueprint node pins and values in the local variables window alongside C++ variables, effectively bridging the gap between the two systems.
Common Debugging Scenarios and Solutions
Breakpoint not triggering:
- Ensure Debug Object is selected in the dropdown (this is the #1 cause)
- Verify the execution path actually reaches the breakpoint node
- Check that you're in PIE/SIE mode—breakpoints don't work in packaged builds
Variables not showing values:
- Your breakpoint may be placed before the node that sets the variable
- The node may not have executed yet in the current execution path
- Use the Execution Trace stack to verify execution order
Yellow breakpoint icons:
- This indicates an invalid breakpoint location
- Solution: Compile the Blueprint (press F7)
- If it persists, hover over the icon for a specific error message
Debugging complex data structures:
- Use the Data Flow tab for expanding Arrays, Maps, and Sets
- Use Print String nodes at strategic points to output specific values to the screen
- Consider using the Debug Object filter to focus on specific instances when multiple exist
The Secret Weapon: Visual Logger for Timeline Recording
What Makes Visual Logger Special
The visual logger is a sophisticated debugging tool that records visual representations of gameplay state over time. Unlike traditional logging that only shows text output, Visual Logger captures 3D shapes, text logs, and actor state snapshots, allowing frame-by-frame analysis after gameplay sessions complete.
I originally thought Visual Logger was just for AI debugging (which it's amazing at), but it's valuable for any gameplay system involving spatial or temporal complexity. When you need to understand "what was happening in frame 347 when the bug occurred?", Visual Logger is your answer.
Opening the Visual Logger
Access via: Window → Developer Tools → Visual Logger
Alternative method: Console command vislog
Window components:
- Actor List: Shows all objects that logged data during the session
- Timeline Scrubber: Navigate through recorded frames
- Status View: Displays actor snapshots with persistent state data
- Text Window: Shows debug messages
- 3D Viewport Overlay: Displays visual debug shapes in the game viewport
Enabling Recording (Critical Step)
Critical: Visual Logger does not record by default. Recording must be manually enabled before your play session:
In the editor: Click the "Enable VisLog Recording" button in the Visual Logger window
Programmatically in C++:
FVisualLogger::Get().SetIsRecording(true);
This design prevents performance overhead when you're not actively debugging. I appreciate this—it means Visual Logger isn't constantly eating memory during normal development.
The UE_VLOG Macro Family
Basic text logging:
UE_VLOG(ActorPointer, LogCategoryName, Verbosity, TEXT("Message format %d"), Variable);
Parameters:
- ActorPointer: The UObject or AActor associated with this log entry
- LogCategoryName: Log category for filtering (e.g., LogTemp, LogAI)
- Verbosity: Log level (Log, Warning, Error, Verbose, VeryVerbose)
- Format String: Printf-style formatted message
Example:
UE_VLOG(this, LogFPSCharacter, Log, TEXT("Projectile fired at speed: %f"), ProjectileSpeed);
UE_VLOG_UELOG variant: Duplicates the message to both Visual Logger and the standard Output Log. Useful when you want the message in both places:
UE_VLOG_UELOG(this, LogGameplay, Warning, TEXT("Health critical: %d"), Health);
Automatic compilation: UE_VLOG macros automatically compile out in shipping builds (controlled by the ENABLE_VISUAL_LOG macro). No need for preprocessor checks—they're safe to leave in your codebase, just like DrawDebugHelpers.
Drawing Debug Shapes for Visual Logger
UE_VLOG_SEGMENT - Line segments for raycasts, pathfinding, trajectories:
UE_VLOG_SEGMENT(this, LogAI, Log, StartLocation, EndLocation, FColor::Green, TEXT("Raycast to target"));
UE_VLOG_BOX - Bounding boxes for collision debugging:
UE_VLOG_BOX(this, LogCollision, Log, Box, FColor::Blue, TEXT("Collision bounds"));
UE_VLOG_CONE - Vision cones, attack ranges:
UE_VLOG_CONE(this, LogAI, Log, Origin, Direction, Length, AngleRadians, FColor::Yellow, TEXT("Vision cone"));
UE_VLOG_CAPSULE - Character collision shapes:
UE_VLOG_CAPSULE(this, LogCharacter, Log, Center, HalfHeight, Radius, Rotation, FColor::Red, TEXT("Character capsule"));
Each shape macro requires: the owning actor, log category, verbosity level, shape-defining parameters (location, size, orientation), color, and description text.
Timeline Recording and Playback Workflow
The power of timeline debugging:
When recording is enabled, Visual Logger captures a snapshot of all logged data each frame. After stopping play, you can scrub through this timeline to analyze what happened at any point. This is game-changing for temporal bugs.
The scrubbing interface:
The timeline is a horizontal bar with tick marks for each frame containing logged data. Click and drag to scrub through time. As you scrub:
- Actor List updates to show which actors were logging at that frame
- Status View displays the selected actor's snapshot data for that specific frame
- Text Window shows text logs from that specific frame only
- 3D Viewport renders debug shapes as they existed at that frame
My workflow for investigating bugs:
- Enable recording before reproducing the bug
- After the bug occurs, stop playing
- Scrub back to the approximate time of the issue
- Select the relevant actor from the Actor List
- Examine the Status view to see variable values at that moment
- Scrub frame-by-frame around the problem moment to identify exactly when state changed unexpectedly
This is especially powerful for single-frame bugs or timing issues that are nearly impossible to catch with traditional debugging. I've found bugs in one afternoon with Visual Logger that I'd been chasing for days with Print String.
Saving and Loading .vlog Files
Saving sessions: After recording a session, use the save button in the Visual Logger window. The .vlog file contains all actor snapshots, text logs, and shape data from the recording.
QA workflow (highly recommended): Configure your QA team to always record Visual Logger sessions when testing. When bugs occur, they attach the .vlog file to bug reports. You can then load the file in your editor and analyze exactly what happened, even if you can't reproduce the bug locally. This dramatically reduces time spent on vague bug reports like "the enemy sometimes doesn't shoot."
Loading files: Open the Visual Logger window and use the load option. The entire recording becomes available for scrubbing and analysis as if you just recorded it.
GrabDebugSnapshot: Custom Actor Data
For advanced usage, actors can implement the IVisualLoggerDebugSnapshotInterface to provide custom data in the Status view.
Header declaration:
#pragma once
#include "VisualLogger/VisualLoggerDebugSnapshotInterface.h"
UCLASS()
class AMyCharacter : public ACharacter, public IVisualLoggerDebugSnapshotInterface
{
GENERATED_BODY()
#if ENABLE_VISUAL_LOG
virtual void GrabDebugSnapshot(FVisualLogEntry* Snapshot) const override;
#endif
private:
int32 HealthPoints;
float StaminaLevel;
};
Implementation:
#if ENABLE_VISUAL_LOG
void AMyCharacter::GrabDebugSnapshot(FVisualLogEntry* Snapshot) const
{
Super::GrabDebugSnapshot(Snapshot);
// Create custom category in Status view
const int32 CategoryIndex = Snapshot->Status.AddZeroed();
FVisualLogStatusCategory& CustomCategory = Snapshot->Status[CategoryIndex];
CustomCategory.Category = TEXT("Character Stats");
// Add custom data fields
CustomCategory.Add(TEXT("Health"), FString::Printf(TEXT("%d"), HealthPoints));
CustomCategory.Add(TEXT("Stamina"), FString::Printf(TEXT("%.2f"), StaminaLevel));
// Add text log entry
Snapshot->AddText(TEXT("Character snapshot captured"), TEXT("MyLog"), ELogVerbosity::Log);
}
#endif
Key points:
- Wrap the implementation in
#if ENABLE_VISUAL_LOGfor shipping build compilation - Call
Super::GrabDebugSnapshot(Snapshot)if your parent class implements it - GrabDebugSnapshot runs automatically on the first Visual Logger call each frame
- The data appears in the Status view and is included in timeline recordings
AI Debugging Use Cases (Where Visual Logger Shines)
Behavior Tree analysis: Visual Logger integrates with Behavior Tree debugging. When logging from tasks or decorators, you can see which nodes executed each frame, why decisions were made, and what blackboard values influenced behavior.
Environment Query System (EQS): Visual Logger automatically captures EQS test results when AI performs environment queries. You can see which locations were tested, their scores, and why the AI chose a particular position. This is amazing for understanding why your AI is making seemingly random decisions.
AI Perception: When debugging sight, hearing, or other senses, Visual Logger displays perception ranges, successful detections, and sensory events. Log perception events with UE_VLOG and draw vision cones with UE_VLOG_CONE.
Pathfinding: Use UE_VLOG_SEGMENT to draw the AI's calculated path. Log navigation events when paths are recalculated or blocked. Combine with timeline scrubbing to understand why an AI stopped moving or suddenly rerouted.
Temporal debugging (the killer feature): The most valuable AI debugging scenario is analyzing bugs that happen in a single frame or over a short sequence. For example, an AI might make a bad decision because a blackboard value changed for exactly one frame due to a race condition. With Visual Logger, you record the session, scrub to the problem moment, and examine frame-by-frame to see the exact sequence of state changes.
Beyond AI: Other Visual Logger Use Cases
Collision and physics: Log raycast results with UE_VLOG_SEGMENT, draw collision volumes with UE_VLOG_BOX or UE_VLOG_CAPSULE, and record when physics events trigger.
Projectile and attack systems: Draw projectile paths, log damage calculations, and visualize hit detection volumes. When players report "hits not registering," the .vlog file shows exactly where the projectile was each frame.
Character movement: Log velocity, acceleration, and ground detection. Draw the character's capsule and movement direction to identify why movement felt wrong at specific moments.
Networking debugging: Log replication events, authority checks, and network state. While Visual Logger doesn't automatically replicate, you can log server-side and client-side events to understand desync issues between clients and server.
Blueprint Integration (Limited but Useful)
Available in Blueprints:
- Enable VisLog Recording: Blueprint node available
- VisLog Box Shape: Blueprint node to draw boxes
Not available in Blueprints:
- Custom GrabDebugSnapshot implementation (cannot override in Blueprints directly)
- Most UE_VLOG shape macros
For full Visual Logger functionality, C++ implementation is recommended. However, Blueprint projects can still benefit from logging in C++ parent classes or plugins.
Performance Considerations
- Development builds only: Automatically disabled in Shipping builds via the ENABLE_VISUAL_LOG macro
- Recording overhead: Capturing snapshots every frame has CPU and memory cost
- Best practice: Only enable recording when actively debugging (which is why it's manual by default)
- Selective logging: Use log categories and verbosity levels to filter what gets recorded
When shipping QA builds with Visual Logger enabled, inform testers that recording has performance impact and should only be enabled when capturing specific bugs.
Console Commands Every Developer Should Know
The Power of Runtime Commands
Unreal Engine provides extensive console commands for runtime debugging and performance analysis. Access the console by pressing tilde (~) during gameplay or in Play In Editor (PIE) mode.
These commands have saved me countless times when I need to quickly check performance without setting up profiling tools or when I need to toggle debug visualization on the fly.
Essential Performance Commands
stat fps
Displays current framerate and frame time. Toggle shortcut: Ctrl+Shift+H. Shows the FPS number and millisecond timing for the last frame. Simple but essential.
stat unit (the most important command)
This is the most important performance diagnostic command. It displays timing breakdown for:
- Frame: Total frame time (this is your actual bottleneck)
- Game: CPU time for gameplay code (AI, physics, animation, Blueprints)
- Draw: CPU time preparing render commands for the GPU
- GPU: Time spent rendering on the graphics card
- RHIT: Rendering Hardware Interface Thread time
- DynRes: Dynamic Resolution system time
To identify bottlenecks, launch in a non-debug configuration and compare these values. The highest value indicates your bottleneck. If Game is highest, you have a gameplay code problem. If GPU is highest, you have a rendering problem.
stat unitgraph
Similar to stat unit but adds a real-time graph in the bottom-left corner plotting values over time. Excellent for spotting performance spikes. I keep this open during playtests to catch frame drops.
stat game
Displays detailed Game thread statistics including:
- AI performance breakdown
- Physics simulation timing
- Animation evaluation cost
- Blueprint execution time
- Memory allocation patterns
stat startfile / stat stopfile
Records detailed profiling data to files saved in YourProject/Saved/Profiling/UnrealStats. These .uestats files can be opened in Unreal Frontend or Session Frontend for deep analysis. Use this when you need to share profiling data with your team or analyze performance offline.
stat levels
Shows level streaming information, grouped under the persistent level. Useful for debugging which levels are currently loaded and their streaming states.
Additional stat commands worth knowing:
- stat memory
- stat scenerendering
- stat gpu
- stat streaming
- stat physics
Type "stat" in the console and use autocomplete to see all available options.
ShowDebug Commands
The showdebug command activates on-screen debug visualization for various engine systems:
showdebug [DebugType]
Common types:
- showdebug abilitysystem: Gameplay Ability System (GAS) debugger with real-time ability state
- showdebug: Without parameters or with an invalid type, this often lists available debug types in your project
Pro tip: Use autocomplete—type "showdebug " (with a space) and press Tab to cycle through available debug types specific to your project.
Logging Console Commands
log list
Lists all log categories and their current verbosity levels. Essential for understanding what's being logged and at what level.
Log [Category] [Verbosity]
Changes a category's verbosity at runtime without restarting:
Log LogTemp Verbose
Log LogGameplay Display
Log LogNetwork Off
showlog
Opens the log window. Useful in packaged builds if the console is enabled and you need to see log output.
Debug Camera
ToggleDebugCamera
Default hotkey: Semicolon (;). Activates a free-flying camera with overlay information. Shows a constant trace from the screen center displaying asset info under the cursor. Super useful for inspecting your level without possessing a character.
Keyboard shortcuts while active:
- F: Freeze rendering
- B: Toggle Buffer Visualization view modes
- V: Cycle through view modes (Unlit, Wireframe, Detail Lighting, Reflections, Collision Pawn, Collision Visibility)
Gameplay Debugger Tool
Enable via:
- Default hotkey: Apostrophe (') key
- Console command:
EnableGDTcheat command
Displays real-time data overlays during PIE, SIE, and standalone sessions. Shows gameplay-related debug information directly on the viewport, including:
- AI behavior trees and decision-making processes
- Navigation mesh data
- Perception system state
- Custom gameplay data via extensions
Keyboard shortcuts:
- Number keys: Toggle different debug categories
- Page Up/Down: Cycle through debug targets
Physics Debugging
p.chaos.debugdraw.enabled 1
Enables physics debug drawing. This is required before using many physics-specific debug commands. Once enabled, you can visualize physics bodies, constraints, and collision shapes.
Network Debugging
Net PktInfo
Shows packet information for network debugging, including packet size and frequency.
Stat Net
Displays network performance statistics including bandwidth usage, packet loss, and network timing. Essential for multiplayer debugging.
Build Configuration Availability
Development builds:
- All console commands available
- All stat commands functional
- All showdebug types accessible
- Highest debugging overhead
Test builds:
- Some commands available
- Reduced functionality compared to Development
- Lower overhead than Development
Shipping builds:
- Console completely disabled by default
- Stat commands compiled out
- No debug visualization
- Can be enabled with preprocessor defines (not recommended for production)
What Actually Ships to Players (Production Considerations)
Why Build Configurations Matter
Understanding which debugging tools work in different build configurations is critical for production debugging and final releases. Most debug features are intentionally disabled in Shipping builds for performance and security reasons.
I learned this lesson when I shipped an early prototype with Development build settings. Players were complaining about performance issues that I couldn't reproduce—turns out all my debug logging and visualization was still running in the background, eating CPU cycles.
The Four Build Configurations
Debug:
- Slowest configuration
- Both engine and game code unoptimized
- Full variable visibility in debugger
- Epic recommends against using Debug Editor for most work
- Only use when you need to step through engine source code with full symbol information
DebugGame (recommended for active development):
- Game code unoptimized (fully debuggable)
- Engine code optimized (runs fast)
- Reasonable editor performance
- Most debugging features available
- This is what I use 95% of the time during development
Development:
- Used for non-debugging activities and performance testing
- Both engine and game code optimized
- Some debugging information retained
- Ideal for playtesting and performance profiling
- Represents a realistic performance baseline
Shipping:
- Final production build
- Maximum optimization
- All debugging features disabled by default
- Minimal overhead
- This is what players receive
Logging in Shipping Builds
Default behavior:
Logging is completely disabled in Shipping builds by default. This results in:
- Absolutely zero runtime CPU cost from logging calls
- No string fragments that could be data-mined by players
- Enhanced security (no debug information leakage)
Enabling logging (optional, use carefully):
Edit your project's *.Target.cs file:
if (Configuration == UnrealTargetConfiguration.Shipping)
{
BuildEnvironment = TargetBuildEnvironment.Unique;
bUseLoggingInShipping = true;
}
Trade-off: Enabling logging introduces measurable performance overhead and potential security concerns. Only enable if production telemetry or debugging requirements outweigh these costs. I rarely enable this—instead, I use crash reporting and telemetry services.
Compile-Time Verbosity Optimization
The most powerful optimization is compile-time verbosity filtering:
DECLARE_LOG_CATEGORY_EXTERN(LogMyGame, Warning, Warning);
If a log statement is more verbose than the compile-time verbosity, it's not compiled into the game code at all. This means:
- Zero runtime overhead for verbose logging in production builds
- Complete elimination of detailed debug logging code
- String formatting and function calls never execute
Best practice: Set compile-time verbosity to Warning or Error for Shipping builds. This allows you to use Verbose and VeryVerbose logging freely during development without any production cost.
Debug Drawing in Production
Automatic disabling:
#define ENABLE_DRAW_DEBUG !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
All DrawDebugHelpers calls are compiled out entirely in Shipping and Test builds, resulting in zero performance overhead. You can safely leave DrawDebug calls in your codebase without affecting release builds. This is one of my favorite features—I never have to clean up debug visualization code.
Print String in Shipping Builds
Print String nodes and GEngine->AddOnScreenDebugMessage() are automatically disabled in Shipping builds. Blueprint Print String nodes marked "Development Only" are completely removed from compiled code.
Assertion Macros
check() family:
- Halts execution when the condition is false
- Completely disabled in Shipping builds by default
- Use for conditions that must always be true in development
check(Pointer != nullptr);
checkf(bIsValid, TEXT("Validation failed: %s"), *ErrorMessage);
verify() family:
- Behaves identically to check() in most builds
- Critical difference: Evaluates expressions even in Shipping builds
- Use when your expression has side effects that must execute
verify(Initialize() == true); // Initialize() runs even in Shipping builds
ensure() family:
- Non-fatal error handling
- Reports to crash reporter but continues execution
- Evaluates in all builds
- Reports only in Debug, Development, Test, and Shipping Editor builds
- Only reports once per session to avoid flooding crash reports
ensure(LoadConfigSuccess); // Logs failure but game continues
Best practice: Use ensure() for recoverable errors you want to track in production, verify() when expressions have side effects, and check() for truly fatal conditions during development.
Console Commands in Production
Default behavior:
The console is only available in Editor, Development, and Test builds—not Shipping builds.
CheatManager:
This is a specialized class for debugging commands that is:
- Automatically instanced in Development/Test builds
- Never instanced in Shipping builds
- Good practice: Concentrate sensitive debugging commands here for automatic disabling
Most cheat functions are wrapped in #if !UE_BUILD_SHIPPING preprocessor directives, meaning they're completely removed from compiled Shipping code.
Crash Reporting
Crash Report Client:
This is not included in packaged games by default. To enable:
- Go to Project Settings → Project → Packaging
- Enable "Include Crash Reporter"
Crash reports include:
- Unique crash GUID identifier
- Error type (crash, assert, ensure)
- Callstacks of the crashing thread and all threads
- Optional user comments
This can be customized to send crash data to your own servers rather than Epic Games, which is what most professional studios do.
Debug Symbols
Include Debug Files setting:
Enable this in Project Settings → Packaging → Include Debug Files.
This allows debugger attachment to Shipping builds for post-crash debugging with crash dumps. It may increase package size but is valuable for production debugging. I always enable this for QA builds, but sometimes disable it for public releases depending on package size concerns.
Third-Party Telemetry Integration
Production debugging often requires external services. Here are options I've used or seen used effectively:
Sentry: Crash alerts with detailed context, supports native UE crash reporter format.
Backtrace: First-class support for UE native crash reporting, available as a marketplace plugin.
BugSnag: Automatic crash detection for Android/iOS with immediate team notifications.
GameAnalytics: Full analytics integration for telemetry and crash data.
Unreal Insights: Epic's native telemetry capture and analysis suite. Works across all build configurations when enabled and is incredibly powerful for performance analysis.
Production Debugging Workflow
Development phase:
- Full logging enabled
- All debug tools available
- Verbose debugging as needed
- Focus on iteration speed
Test phase:
- Reduced logging (warnings and errors only)
- Prepare for production environment simulation
- Performance profiling in Development or Test builds
Shipping phase:
- Minimal logging (errors only or completely disabled)
- Crash reporter enabled
- Telemetry for critical issues only
- Maximum optimization
Production monitoring:
- Use ensure() for trackable issues
- Crash analytics for fatal errors
- Remote telemetry for specific systems
- User feedback integration
Performance Reality Check
Understanding the Cost of Debugging
Understanding the performance cost of debugging tools is critical for efficient development and avoiding performance issues in production. The good news: most debugging tools have zero impact in Shipping builds due to automatic compilation removal.
UE_LOG Performance
Shipping build overhead: Zero. Logging is completely disabled and stripped from compiled code by default in Shipping builds. No runtime CPU cost, no string fragments in memory, no condition checking.
Development build overhead: Measurable but generally acceptable. Even when not actively logging, the act of deciding whether to print can cause performance issues in systems with heavy logging. String formatting with FString::Printf has higher overhead than simple formatting.
Compile-time optimization (use this): Set compile-time verbosity to low levels (Warning, Error) for production builds. Logs more verbose than compile-time settings are completely removed from compiled code, eliminating all overhead.
// In production, this generates ZERO code if compile-time verbosity is Warning:
UE_LOG(LogTemp, Verbose, TEXT("Detailed debug info"));
Print String Performance
Significant overhead in development:
print string unreal engine has notably higher overhead than UE_LOG:
- Renders messages on the viewport in addition to logging
- Can cause severe FPS drops (I've seen reported frame times up to 45ms in extreme cases)
- Particularly expensive when the Output Log window is scrolled to the bottom (forces constant UI updates)
Screen flooding impact: Calling Print String every frame (e.g., in Event Tick) without proper key management creates hundreds of messages per second, causing massive performance degradation. I burned half a day debugging "performance issues" that were actually just Print String spam.
Shipping build: Automatically disabled. Zero overhead.
Best practice: Use UE_LOG instead of Print String for high-frequency logging. Reserve Print String for quick on-screen visualization during active development and prototyping.
DrawDebugHelpers Performance
Shipping and Test builds: Zero overhead. Automatically compiled out via the ENABLE_DRAW_DEBUG preprocessor define.
Development build: Performance cost depends on:
- Volume of shapes drawn (thousands vs dozens)
- Complexity of shapes (high-segment spheres cost more than simple lines)
- Persistence (accumulated shapes over time)
- CPU generation cost (game thread overhead)
- GPU rendering cost (drawing primitives)
Best practice: Use debug drawing freely during development but avoid extremely heavy use (thousands of shapes per frame) in performance-critical sections. Remember: production builds have zero cost, so don't over-optimize this during development.
Visual Logger Performance
Memory overhead: Visual Logger has significant memory usage. According to official Unreal documentation, "due to memory usage patterns, Visual Logger is not really feasible for consoles."
Design philosophy: The tool is designed to be efficient enough to be "on" all the time, though currently it requires manual start/stop of recording.
Best practice:
- Only enable recording when actively debugging
- Use log categories to filter what gets recorded
- Be aware that recording has both CPU and memory cost
- Stop recording when you've captured the data you need
Shipping build: Automatically disabled via the ENABLE_VISUAL_LOG macro. Zero overhead.
Blueprint Debugging Performance
blueprint debugging breakpoints and debugging have negligible performance impact in Development builds since they only activate when execution is paused.
Shipping build: Breakpoints are completely compiled out. Zero overhead.
String Formatting Overhead
FString: Higher overhead due to dynamic memory allocation. More expensive for frequent logging.
FName: Uses a shared name table with index-based lookup. Excellent performance with low memory overhead. Better choice for frequently-used identifiers.
Printf formatting: Formatting complex expressions every frame has measurable cost. Avoid expensive calculations in format strings for high-frequency logging.
File I/O Performance
Synchronous logging: By default, logging is often blocking—when you execute a log statement, the log gets written to disk:
- HDD access: 5,000-10,000 microseconds
- SSD access: 35-100 microseconds
Asynchronous logging: Reduces application pauses by decoupling logging from disk I/O. In benchmark tests I've seen, async logging reduced Real CPU time by 34.5% (from 108ms to 70.7ms in one particular stress test).
UE implementation: Official UE documentation doesn't detail whether native UE_LOG uses asynchronous or synchronous file I/O by default. Third-party plugins exist for UE5.6+ providing thread-safe, async file writing for performance-critical applications.
Build Configuration Overhead Comparison
Development: Highest CPU overhead. Full debugging capabilities. Use for active development.
Test: Lower overhead than Development. Some developer functionality retained. Good for pre-release testing.
Shipping: Lowest overhead. Maximum optimization. All debug tools compiled out. This is what players get.
Profiling consideration: Always profile in the configuration closest to your target release. Development builds have substantial performance differences compared to Shipping/Test. Don't make optimization decisions based on Development build profiling.
Stat Commands Overhead
Performance hit: Stat commands add a "small performance hit" according to Epic's documentation, but the overhead is generally considered very low for built-in stats.
Shipping builds: Disabled by default. Not available.
Best practice: Use stat commands conservatively. Only track actionable statistics. Useless stats "pollute your stats view" (Epic's words) and add unnecessary overhead.
My Performance Optimization Practices
1. Adjust verbosity by build configuration: Use verbose logging during development, reduce to warnings/errors in production builds.
2. Compile-time optimization: Set compile-time verbosity low to strip verbose logs from shipping builds automatically.
3. Conditional compilation for expensive debug code:
#if !UE_BUILD_SHIPPING
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Green, TEXT("Debug info"));
}
#endif
4. Prefer UE_LOG over Print String: Lower overhead, especially for frequent logging scenarios.
5. Use custom log categories: Organize logging with proper categories and verbosity levels for targeted filtering.
6. Profile in the correct configuration: Use Test or Shipping builds for performance profiling, not Development.
7. Trust default behavior: Most debug tools are automatically disabled in Shipping builds with zero overhead. Don't over-engineer their removal.
Mistakes I Made So You Don't Have To
Print String Flooding (The Classic Beginner Mistake)
The problem:
Calling Print String from Event Tick or high-frequency functions without proper key management creates an avalanche of debug messages, severely degrading performance and making the screen completely unreadable.
Symptoms:
- FPS drops from 60 to single digits
- Screen covered with overlapping text
- Output Log window scrolling so rapidly you can't read anything
- Editor becoming unresponsive
I did this in my first week with Unreal. Took me an embarrassingly long time to figure out why my simple movement script was running at 5 FPS.
Solutions:
- Use a positive integer key to replace messages instead of stacking them
- Add conditional logic to limit message frequency (only log every 10th frame)
- Move debug logic out of Tick events when possible
- Use only "Print to Log" (disable "Print to Screen") to reduce overhead
- Use timers with intervals of 0.1-0.5 seconds instead of Tick
- Consider UE_LOG for high-frequency debugging—it's much more efficient
Example fix:
// Bad: Floods screen every frame (60+ times per second)
void Tick(float DeltaTime)
{
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::White, TEXT("Tick"));
}
}
// Good: Replaces previous message instead of stacking
void Tick(float DeltaTime)
{
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(0, 0.0f, FColor::White,
FString::Printf(TEXT("Speed: %.2f"), GetVelocity().Size()));
}
}
GEngine Null Pointer Crashes
The problem:
Calling GEngine->AddOnScreenDebugMessage() without checking if GEngine exists. During editor startup, for actors with no instances in the default level, or on dedicated servers, GEngine can be null. Accessing it causes immediate crashes with cryptic error messages.
Symptoms:
- Crash on BeginPlay or during early initialization
- Crash report mentioning null pointer access or access violation
- Works inconsistently across different scenarios (works in PIE, crashes in standalone)
I hit this constantly when I first started using C++ with Unreal. The crash reports were confusing because GEngine was valid most of the time.
Solution (always do this):
// Bad: Will crash if GEngine is null
GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, TEXT("Debug message"));
// Good: Safe with null check
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, TEXT("Debug message"));
}
Even more subtle: GameViewport can be null:
// Even safer for viewport-specific operations
if (GEngine && GEngine->GameViewport)
{
// Safe to use viewport-specific functionality
}
Log Category Misconfiguration
The problem:
Custom log categories declared in one module but used in another without proper header includes. This works perfectly in-editor but fails catastrophically in packaged builds due to different include orders and module linking.
Symptoms:
- Unresolved external symbol errors during packaging
- Compilation succeeds in editor but fails when packaging
- UE_LOG statements appear to do nothing (silently fail)
Solutions:
- Declare log categories in a central header file that's always included
- Use forward declarations to reduce header dependencies
- Set CompileTimeVerbosity to All during development
- Ensure proper module dependencies in your Build.cs files
Example of proper setup:
// MyProjectLogging.h - Centralized log categories
#pragma once
#include "CoreMinimal.h"
#include "Logging/LogMacros.h"
DECLARE_LOG_CATEGORY_EXTERN(LogGameplay, Log, All);
DECLARE_LOG_CATEGORY_EXTERN(LogUI, Log, All);
// MyProjectLogging.cpp
#include "MyProjectLogging.h"
DEFINE_LOG_CATEGORY(LogGameplay);
DEFINE_LOG_CATEGORY(LogUI);
Multiplayer Debugging Pitfalls
Forgotten replication:
The most common networking mistake—forgetting to enable replication on actors. Your code is perfect, your logic is sound, but nothing works because you didn't check the "Replicates" checkbox in Actor settings or call SetReplicates(true).
Authority confusion:
Not using HasAuthority() checks leads to logic executing on the wrong machine. Print String messages appear on whichever instance executes the code, causing massive confusion about which client or server is actually logging.
Over-replication:
Marking every variable as Replicated causes network congestion and bandwidth issues. Only replicate what clients absolutely need to know.
Debug tool visibility:
Breakpoints pause only one instance (causing network timeouts for others), Print Strings appear only on the executing instance, and visual debug lines may only show on the server or specific clients depending on where you're drawing them.
Solutions:
- Always verify replication is enabled before debugging network code
- Use HasAuthority() checks and print the role with debug messages
- Test with multiple PIE instances early and continuously
- Minimize replicated data—only replicate what's necessary
- Use the Network Profiler to identify over-replication issues
IsValid() vs nullptr Confusion
The problem:
Developers overuse IsValid() when simple nullptr checks suffice, or use it incorrectly on uninitialized or dangling pointers.
Community consensus:
- Use nullptr checks (or simple
if (Pointer)) for basic null validation—it's faster and sufficient for most cases - Use IsValid() only when you need to check if an object is pending kill (marked for garbage collection)
- Avoid IsValid() on uninitialized or dangling pointers—it can cause undefined behavior
Example:
// Basic null check - sufficient for most cases
if (MyActor != nullptr)
{
MyActor->DoSomething();
}
// IsValid() - only when checking for pending kill status
if (IsValid(MyActor))
{
// Safe from garbage collection issues
MyActor->DoSomething();
}
Blueprint Breakpoint Confusion
The problem:
Developers expect breakpoints to work in packaged builds. Breakpoints are saved in .ini files and persist between editor sessions, creating confusion when they mysteriously don't work in packages.
Reality: Breakpoints only function during PIE/SIE sessions. They're completely compiled out of packaged builds, especially Shipping builds.
Solutions:
- Use breakpoints only for PIE/SIE debugging
- For packaged builds, enable "Include Debug Files" and use Visual Studio to attach a debugger (C++ only)
- Blueprint debugging is completely unavailable in packaged builds—plan accordingly
Debug Draw Persistent Lines Not Working
The problem:
Setting bPersistentLines = true and LifeTime = -1, but lines fade after one second anyway. This has been reported as a bug in certain engine versions (4.20.3 and some 5.x versions).
Solutions:
- Verify you're not in a Test or Shipping build (debug drawing is completely disabled)
- Try explicit positive LifeTime values instead of -1
- Call DrawDebug functions every frame as a workaround
- Check engine version release notes for known issues
- Consider using Visual Logger instead for persistent visualization
Build Configuration Misconceptions
The problem:
Developers use Debug Editor for normal work, causing "painfully slow" performance (Epic's exact words). Or they profile in Development configuration and expect results to match Shipping performance, leading to incorrect optimization decisions.
Solutions:
- Use DebugGame Editor for active development and debugging
- Use Development for playtesting and initial performance profiling
- Use Shipping for final production builds and true performance testing
- Avoid Debug Editor unless you specifically need to step through engine source code
- Always profile in the configuration closest to your release target
My Debugging Workflow (What Actually Works)
My General Unreal Engine 5 Debugging Approach
1. Start with the right tool for the job:
- Need immediate visual feedback during gameplay? Print String
- Need persistent logging for post-session analysis? UE_LOG
- Debugging spatial problems like raycasts or collision? DrawDebugHelpers
- Dealing with temporal or complex timing bugs? Visual Logger
- Want real-time variable inspection? Blueprint breakpoints
2. Build configuration awareness:
- Debug: Only when stepping through engine source code
- DebugGame: Active development and debugging (my default)
- Development: Playtesting and performance profiling
- Shipping: Final builds and true performance testing
3. Defensive programming habits:
- Always initialize pointers to nullptr
- Check pointers before dereferencing (especially GEngine)
- Use ensure() for recoverable errors you want to track
- Use check() for fatal conditions that should never occur
- Wrap expensive debug code in
#if !UE_BUILD_SHIPPINGwhen necessary
Print String Best Practices
1. Key parameter management:
- Never use default (-1) key in Tick or high-frequency functions
- Assign logical key ranges to organize messages:
- 0-9: Critical player information
- 10-19: Enemy AI data
- 20-29: System messages
- Use keys to replace messages, not stack them
2. Color coding system:
- Red: Errors, collisions detected, blocked paths
- Yellow/Orange: Warnings, things needing attention
- Green: Success states, clear paths, completed actions
- Blue: Informational messages, player data
- White: Default/neutral information
3. Duration settings:
- One-time events: 2-5 seconds
- Frequently updating values: 0.1-0.5 seconds with a fixed key
- Critical warnings: 5-10 seconds
4. When to avoid Print String:
- High-frequency logging (use UE_LOG instead)
- Production or shipping builds (it's disabled anyway)
- Systems requiring detailed post-session analysis
UE_LOG Best Practices
1. Custom categories by system:
- Create categories for different systems: LogGameplay, LogNetwork, LogUI, LogAI
- Declare in a central header file
- Define in the corresponding .cpp file
- Set compile-time verbosity appropriately for your target builds
2. Verbosity selection guide:
- Fatal: Unrecoverable errors only (crashes the game)
- Error: Serious problems requiring immediate attention
- Warning: Potential issues, deprecated usage warnings
- Display: Important gameplay events, major state changes
- Log: General debugging information
- Verbose: Detailed debugging in performance-sensitive code
- VeryVerbose: Deep debugging sessions only
3. String formatting tips:
- Use FString::Printf for complex formatting
- Remember to dereference FStrings with
*:*MyFString - Convert booleans manually:
(bValue ? TEXT("true") : TEXT("false")) - Use ToString() for Unreal types:
*Location.ToString()
4. Performance optimization:
- Set compile-time verbosity to Warning or Error for production builds
- Use Log/Verbose/VeryVerbose for frequent logging (skips console rendering)
- Avoid expensive formatting in Tick functions
- Trust the default shipping behavior for zero overhead
Visual Debugging Best Practices
1. DrawDebugHelpers usage:
- Use temporary drawing (bPersistentLines = false) for continuous updates
- Use persistent drawing (bPersistentLines = true) for marking paths and history
- Assign consistent colors per system for visual organization
- Clear persistent lines periodically with
FlushPersistentDebugLines(GetWorld()) - Remember: Zero overhead in Shipping/Test builds—use freely
2. Shape selection guide:
- Lines: Raycasts, directions, connections between objects
- Spheres: Detection ranges, collision radii, areas of effect
- Boxes: Bounding volumes, trigger zones, spatial regions
- Capsules: Character collision shapes
- Cones: Vision cones, attack ranges, directional effects
- Arrows: Movement direction, applied forces, velocity vectors
3. Color organization:
- Maintain consistent color schemes across your entire team
- Use darker/lighter shades for hierarchy or priority
- Adjust colors for your game environment (dark colors for bright games, bright colors for dark games)
Blueprint Debugging Best Practices
1. Breakpoint placement strategy:
- Place breakpoints at decision points (Branch nodes, Switch nodes)
- Avoid placing breakpoints on every single node—it slows debugging
- Use breakpoints strategically to understand logic flow, not to step through every operation
2. Debug Object selection (critical):
- Always select the specific instance from the Debug Object dropdown
- This is essential when multiple instances of the same Blueprint exist
- Without proper selection, breakpoints may not trigger at all
3. Watch values wisely:
- Watch only critical variables to reduce visual clutter
- Use the Data Flow tab for complex data structures like Arrays, Maps, and Sets
- Remember: Values are only available after the node has executed
4. Combining with Print String:
- Use Print String nodes before breakpoints to create a "breadcrumb trail"
- This helps identify where execution diverged from your expectations
- Mark Print String nodes with the "Development Only" option to ensure they're compiled out
Visual Logger Best Practices
1. Recording discipline:
- Only enable recording when actively debugging a specific issue
- Stop recording immediately after capturing the relevant session
- Save .vlog files for bug reports and share them with your team
2. Custom snapshot implementation:
- Implement GrabDebugSnapshot for persistent actor state tracking
- Always wrap in
#if ENABLE_VISUAL_LOGfor shipping build safety - Add meaningful category names and field names for clarity
3. Shape and text logging:
- Use UE_VLOG_SEGMENT for raycasts and pathfinding
- Use UE_VLOG_BOX for collision volumes and bounding boxes
- Use UE_VLOG_CONE for vision cones and awareness ranges
- Combine shapes with text logs to provide context
4. Timeline scrubbing workflow:
- Scrub to the approximate time of the issue
- Select the relevant actor from the Actor List
- Examine the Status view for variable values at that exact frame
- Step frame-by-frame to identify the exact moment state changed unexpectedly
Production Debugging Best Practices
1. Shipping build configuration:
- Disable logging by default for security and performance
- Enable Crash Reporter Client: Project Settings → Packaging → Include Crash Reporter
- Consider enabling debug symbols with the Include Debug Files option
- Use ensure() for trackable recoverable errors in production
2. Telemetry integration:
- Integrate third-party analytics services (Sentry, Backtrace, BugSnag)
- Configure custom crash report endpoints for your team
- Collect only the minimal necessary data for debugging
- Always respect player privacy and data regulations
3. Verbosity management by build:
- Development: Use Verbose/VeryVerbose freely
- Test: Reduce to Display/Warning levels
- Shipping: Error only, or completely disabled
- Set compile-time verbosity to eliminate verbose code in production automatically
4. Assertion strategy:
- check(): Fatal conditions that must never occur (disabled in Shipping)
- verify(): Expressions with side effects that must execute (evaluates in all builds)
- ensure(): Recoverable errors to track in production (reports once per session)
Team Workflow Best Practices
1. QA integration:
- Train QA team members to use Visual Logger for detailed bug reports
- Have QA attach .vlog files to bug tickets whenever possible
- Enable recording during bug reproduction attempts
- Document how to enable/disable debug features for non-programmers
2. Code standards:
- Establish team conventions for log categories and naming
- Define consistent color coding for debug drawing across all systems
- Document custom log categories in your project wiki or documentation
- Code review debug code for proper conditional compilation
3. Version control:
- Don't commit debug-specific nodes or code to production branches
- Use temporary branches for invasive debugging changes
- Document why specific debug code must remain in production if absolutely necessary
4. Performance profiling:
- Always profile in the configuration matching your target release
- Disable unnecessary debug features during performance profiling sessions
- Use stat commands and Unreal Insights for accurate performance data
- Document performance baselines per configuration for comparison
Wrapping Up: Debug Smarter, Not Harder
unreal engine 5 debugging doesn't have to be painful. With the right tools—Print String for quick visual checks, UE_LOG for persistent logging, DrawDebugHelpers for spatial visualization, Visual Logger for timeline analysis, and Blueprint breakpoints for interactive inspection—you can track down even the trickiest bugs efficiently.
Here's what I want you to remember: most of these debugging tools automatically compile out in Shipping builds, giving you zero performance overhead in production. This means you can debug freely during development without worrying about cleanup. Use Print String generously (with proper key management), scatter DrawDebugLines throughout your code, and log everything with UE_LOG. When you ship, it all disappears automatically.
The debugging skills you build now will serve you throughout your entire game development career. Every developer I know at KIXEYE and every studio I've consulted with uses these exact same tools daily. Master them, develop your own workflow, and you'll spend less time hunting bugs and more time building amazing games.
Common Questions
What is the difference between Print String and UE_LOG in Unreal Engine 5?
Print String displays messages directly on your game viewport and is primarily used in Blueprints for immediate visual feedback during gameplay. UE_LOG is a C++ logging system that writes persistent, filterable messages to log files and the Output Log window. Print String has higher performance overhead but provides instant visual feedback, while UE_LOG is more efficient for detailed debugging and post-session analysis. Both are automatically disabled in Shipping builds.
How do I use Print String without flooding my screen in Tick events?
Use the Key parameter with a specific integer value instead of the default -1. When you assign a key like 0 or 1, each new Print String call with that same key replaces the previous message instead of creating a new one. Also set the Duration to 0.0-0.5 seconds for frequently updated values. This prevents hundreds of messages from stacking on screen every second.
Why does my GEngine->AddOnScreenDebugMessage cause crashes?
GEngine can be null during editor startup, on dedicated servers, or during early initialization. Always wrap GEngine calls in a null check: if (GEngine) { GEngine->AddOnScreenDebugMessage(...); }. This simple check prevents access violation crashes. Even more robustly, check both GEngine and GameViewport: if (GEngine && GEngine->GameViewport).
How do I create custom log categories in UE5?
Declare the category in a header file with DECLARE_LOG_CATEGORY_EXTERN(LogMyGame, Log, All); and define it in a cpp file with DEFINE_LOG_CATEGORY(LogMyGame);. Then use it with UE_LOG(LogMyGame, Warning, TEXT("Your message"));. The second parameter (Log) is the default verbosity, and the third (All) is the compile-time maximum verbosity.
What does the ENABLE_DRAW_DEBUG macro do?
ENABLE_DRAW_DEBUG is a preprocessor define set to !(UE_BUILD_SHIPPING || UE_BUILD_TEST), which automatically disables all DrawDebugHelpers calls in Shipping and Test builds. This means DrawDebugLine, DrawDebugSphere, and all other visual debug functions compile out entirely in production, resulting in zero performance overhead. You can safely leave debug drawing code in your project.
How do I use Visual Logger to debug AI behavior?
First, enable recording by clicking "Enable VisLog Recording" in the Visual Logger window (Window → Developer Tools → Visual Logger). Use UE_VLOG macros in your AI code: UE_VLOG(this, LogAI, Log, TEXT("Pathfinding started")); for text and UE_VLOG_SEGMENT(this, LogAI, Log, Start, End, FColor::Cyan, TEXT("Path")); for visual shapes. After your play session, scrub through the timeline to analyze AI decisions frame-by-frame. You can save the session as a .vlog file for later analysis.
Why are my Blueprint breakpoints not working?
Check three things: First, ensure you've selected the correct Debug Object from the dropdown in the Blueprint Editor toolbar (required when multiple instances exist). Second, verify you're in Play In Editor (PIE) or Simulate mode—breakpoints don't work in packaged builds. Third, if the breakpoint icon is yellow, compile your Blueprint (F7). Breakpoints only trigger during editor play sessions, not in standalone games.
What verbosity level should I use for UE_LOG in production builds?
Set your compile-time verbosity to Warning or Error for production: DECLARE_LOG_CATEGORY_EXTERN(LogMyGame, Log, Warning);. This completely removes Display, Log, Verbose, and VeryVerbose statements from compiled code, giving zero overhead. During development, use Verbose and VeryVerbose freely—they'll be automatically stripped from Shipping builds.
How do I view debug shapes drawn with DrawDebugHelpers?
Debug shapes appear directly in your 3D viewport during Play In Editor or in standalone games (Development builds only). Make sure you're not in a Shipping or Test build where ENABLE_DRAW_DEBUG is disabled. If you still don't see shapes, check that bPersistentLines and LifeTime parameters are set correctly. For temporary drawing (bPersistentLines = false), you need to call the function every frame, often from Tick.
What's the difference between check(), verify(), and ensure() macros?
check() halts execution when the condition is false and is completely disabled in Shipping builds—use it for fatal conditions during development. verify() behaves like check() but always evaluates its expression even in Shipping builds—use it when the expression has side effects. ensure() reports errors to the crash reporter but continues execution, works in all builds, and reports only once per session—use it for recoverable errors you want to track in production.
How do I debug multiplayer games in Unreal Engine 5?
Enable network play in the editor by setting Number of Players to 2+ in the Play dropdown. Use HasAuthority() checks to identify server vs client execution. Add role information to your debug messages: UE_LOG(LogTemp, Warning, TEXT("Server: %s"), HasAuthority() ? TEXT("true") : TEXT("false"));. Remember that breakpoints pause only one instance and Print Strings appear only on the executing instance. Test continuously with multiple PIE instances to catch replication issues early.
Why is my game running slow with debug logging enabled?
Print String has significant overhead, especially when called every frame without proper key management or when the Output Log is scrolled to the bottom. UE_LOG with high verbosity levels also impacts performance in Development builds. Use compile-time verbosity filtering to reduce overhead, prefer UE_LOG over Print String for frequent logging, and remember that all debug tools are automatically disabled in Shipping builds with zero overhead.