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

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:

  1. Right-click anywhere in your Event Graph
  2. Search for "Print String"
  3. Connect it to any execution pin in your logic flow
  4. 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:

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:

cpp
void UEngine::AddOnScreenDebugMessage(
    uint64 Key,
    float TimeToDisplay,
    FColor DisplayColor,
    const FString& DebugMessage,
    bool bNewerOnTop = true,
    const FVector2D& TextScale = FVector2D::UnitVector
)

Parameter breakdown:

Critical safety pattern—always use this:

cpp
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:

cpp
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:

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:

cpp
void AMyActor::BeginPlay()
{
    Super::BeginPlay();
    if (GEngine)
    {
        GEngine->AddOnScreenDebugMessage(-1, 3.0f, FColor::Green,
            FString::Printf(TEXT("%s spawned successfully"), *GetName()));
    }
}

Monitoring state machines:

cpp
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:

cpp
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:

cpp
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:

When to avoid Print String:

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

cpp
UE_LOG(CategoryName, VerbosityLevel, TEXT("Message with %s"), *FStringVariable);

Three required parameters:

  1. Category Name: Organizational bucket for logs (e.g., LogTemp, LogAI, LogNetwork)
  2. Verbosity Level: Severity/importance (Fatal, Error, Warning, Display, Log, Verbose, VeryVerbose)
  3. 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):

cpp
DECLARE_LOG_CATEGORY_EXTERN(LogMyGame, Log, All);

In your source file (.cpp):

cpp
DEFINE_LOG_CATEGORY(LogMyGame);

Declaration variants you'll encounter:

Understanding declaration parameters:

cpp
DECLARE_LOG_CATEGORY_EXTERN(CategoryName, DefaultVerbosity, CompileTimeVerbosity);

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:

Real examples:

cpp
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:

cpp
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:

cpp
#include "Logging/StructuredLog.h"

UE_LOGFMT(LogTemp, Log, "Simple message without TEXT macro");

Using positional parameters:

cpp
UE_LOGFMT(LogTemp, Warning, "Loading '{0}' failed with error {1}", PackageName, ErrorCode);

Using named parameters (my preference):

cpp
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):

cpp
DECLARE_LOG_CATEGORY_EXTERN(LogMyGame, Log, Warning); // Only Warning and above compiled

2. Engine.ini or DefaultEngine.ini configuration:

ini
[Core.Log]
LogOnline=Verbose
LogMyGame=Display

3. Command-line arguments:

plaintext
-LogCmds="LogMyGame Verbose, LogTemp Off"

4. Runtime console command:

plaintext
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:

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:

Setting Up Custom Categories (Complete Example)

Here's how I typically set up logging for a new project:

MyProjectLogging.h:

cpp
#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:

cpp
#include "MyProjectLogging.h"

DEFINE_LOG_CATEGORY(LogGameplay);
DEFINE_LOG_CATEGORY(LogUI);
DEFINE_LOG_CATEGORY(LogAI);

Usage in your game code:

cpp
#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:

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:

cpp
#include "DrawDebugHelpers.h"

DrawDebugLine - Straight lines between two points (perfect for raycasts and directions):

cpp
DrawDebugLine(GetWorld(), StartLocation, EndLocation, FColor::Green, false, 2.0f, 0, 2.0f);

DrawDebugSphere - Wireframe spheres (collision radii, detection ranges):

cpp
DrawDebugSphere(GetWorld(), GetActorLocation(), 100.0f, 12, FColor::Red, false, 2.0f);

DrawDebugBox - Axis-aligned or oriented boxes (bounding volumes):

cpp
DrawDebugBox(GetWorld(), BoxCenter, BoxExtent, FColor::Yellow, false, 2.0f);

DrawDebugCapsule - Capsule shapes (character collision):

cpp
DrawDebugCapsule(GetWorld(), Center, HalfHeight, Radius, Rotation, FColor::Blue, false, 2.0f);

DrawDebugCone - Cone shapes (vision cones, attack ranges):

cpp
DrawDebugCone(GetWorld(), Origin, Direction, Length, AngleRadians, AngleRadians, 12, FColor::Yellow, false, 0.5f);

DrawDebugArrow - Directional arrows (movement direction, forces):

cpp
DrawDebugArrow(GetWorld(), Start, End, ArrowSize, FColor::Orange, false, 2.0f);

DrawDebugString - 3D text at world locations:

cpp
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:

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):

Persistent drawing:

Real-World Visual Debugging Examples

Visualizing raycasts:

cpp
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:

cpp
DrawDebugSphere(GetWorld(), AILocation, DetectionRadius, 16, FColor::Cyan, false, 0.1f);

Debugging collision volumes:

cpp
DrawDebugBox(GetWorld(), BoxCenter, BoxExtent, Rotation, FColor::Yellow, true, 5.0f, 0, 3.0f);

Visualizing AI pathfinding:

cpp
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:

cpp
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:

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):

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:

cpp
#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:

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:

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:

  1. Right-click any Blueprint node
  2. Select "Add Breakpoint"
  3. 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:

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:

  1. Right-click any pin's name in the Blueprint Graph
  2. Select "Watch this value"
  3. 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:

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:

Variables not showing values:

Yellow breakpoint icons:

Debugging complex data structures:

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:

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++:

cpp
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:

cpp
UE_VLOG(ActorPointer, LogCategoryName, Verbosity, TEXT("Message format %d"), Variable);

Parameters:

Example:

cpp
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:

cpp
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:

cpp
UE_VLOG_SEGMENT(this, LogAI, Log, StartLocation, EndLocation, FColor::Green, TEXT("Raycast to target"));

UE_VLOG_BOX - Bounding boxes for collision debugging:

cpp
UE_VLOG_BOX(this, LogCollision, Log, Box, FColor::Blue, TEXT("Collision bounds"));

UE_VLOG_CONE - Vision cones, attack ranges:

cpp
UE_VLOG_CONE(this, LogAI, Log, Origin, Direction, Length, AngleRadians, FColor::Yellow, TEXT("Vision cone"));

UE_VLOG_CAPSULE - Character collision shapes:

cpp
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:

My workflow for investigating bugs:

  1. Enable recording before reproducing the bug
  2. After the bug occurs, stop playing
  3. Scrub back to the approximate time of the issue
  4. Select the relevant actor from the Actor List
  5. Examine the Status view to see variable values at that moment
  6. 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:

cpp
#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:

cpp
#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:

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:

Not available in Blueprints:

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

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:

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:

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:

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:

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:

plaintext
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:

Gameplay Debugger Tool

Enable via:

Displays real-time data overlays during PIE, SIE, and standalone sessions. Shows gameplay-related debug information directly on the viewport, including:

Keyboard shortcuts:

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:

Test builds:

Shipping builds:

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:

DebugGame (recommended for active development):

Development:

Shipping:

Logging in Shipping Builds

Default behavior:

Logging is completely disabled in Shipping builds by default. This results in:

Enabling logging (optional, use carefully):

Edit your project's *.Target.cs file:

csharp
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:

cpp
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:

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:

cpp
#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 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:

cpp
check(Pointer != nullptr);
checkf(bIsValid, TEXT("Validation failed: %s"), *ErrorMessage);

verify() family:

cpp
verify(Initialize() == true); // Initialize() runs even in Shipping builds

ensure() family:

cpp
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:

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:

  1. Go to Project Settings → Project → Packaging
  2. Enable "Include Crash Reporter"

Crash reports include:

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:

Test phase:

Shipping phase:

Production monitoring:

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.

cpp
// In production, this generates ZERO code if compile-time verbosity is Warning:
UE_LOG(LogTemp, Verbose, TEXT("Detailed debug info"));

Significant overhead in development:

print string unreal engine has notably higher overhead than UE_LOG:

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:

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:

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:

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:

cpp
#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

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:

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:

Example fix:

cpp
// 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:

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):

cpp
// 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:

cpp
// 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:

Solutions:

Example of proper setup:

cpp
// 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:

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:

Example:

cpp
// 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:

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:

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:

My Debugging Workflow (What Actually Works)

My General Unreal Engine 5 Debugging Approach

1. Start with the right tool for the job:

2. Build configuration awareness:

3. Defensive programming habits:

1. Key parameter management:

2. Color coding system:

3. Duration settings:

4. When to avoid Print String:

UE_LOG Best Practices

1. Custom categories by system:

2. Verbosity selection guide:

3. String formatting tips:

4. Performance optimization:

Visual Debugging Best Practices

1. DrawDebugHelpers usage:

2. Shape selection guide:

3. Color organization:

Blueprint Debugging Best Practices

1. Breakpoint placement strategy:

2. Debug Object selection (critical):

3. Watch values wisely:

4. Combining with Print String:

Visual Logger Best Practices

1. Recording discipline:

2. Custom snapshot implementation:

3. Shape and text logging:

4. Timeline scrubbing workflow:

Production Debugging Best Practices

1. Shipping build configuration:

2. Telemetry integration:

3. Verbosity management by build:

4. Assertion strategy:

Team Workflow Best Practices

1. QA integration:

2. Code standards:

3. Version control:

4. Performance profiling:

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.