Skip to main content

πŸ” Lesson 2.6: Logging, Assertions & Debugging C++

You can't fix what you can't see. This lesson gives you the everyday toolkit for observing and hardening C++: structured logging, on-screen messages, assertions that catch bugs at their source, and debug drawing to make invisible logic visible.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Use UE_LOG with the right verbosity and format specifiers
  • Declare and use a custom log category for your game
  • Print on-screen debug messages during play
  • Choose between check, ensure, and verify
  • Visualize logic with debug drawing (lines, spheres, points)

Estimated Time: 45 minutes

Engine Version: Unreal Engine 5.8

In This Lesson

UE_LOG & Verbosity

UE_LOG is Unreal's printf. It writes to the Output Log and the log file, tagged with a category and a verbosity.

// UE_LOG(Category, Verbosity, Format, Args...)
UE_LOG(LogTemp, Warning, TEXT("Player %s took %.1f damage"), *GetName(), Damage);
VerbosityUse forNote
FatalUnrecoverable β€” crashes the game intentionallyAlways compiled in
ErrorSomething is wrong and visibleRed in the log
WarningSuspicious but non-fatalYellow β€” easy to spot
DisplayImportant normal infoShown by default
LogGeneral developer loggingThe everyday level
Verbose / VeryVerboseDeep detailOff unless enabled

πŸ“– Format specifiers you'll use constantly

  • %d β€” integer (int32) Β· %f / %.1f β€” float Β· %s β€” a string
  • For %s, dereference an FString with *: *MyString or *GetName(). Forgetting the * is the most common logging mistake.
  • %d for bool prints 0/1; or use %s with a ternary for readability.

⚠️ Fatal really crashes

UE_LOG(..., Fatal, ...) deliberately terminates the game with the message. Use it only for truly unrecoverable states. For "this shouldn't happen but let's keep going," you want ensure (below), not Fatal.

Custom Log Categories

LogTemp is fine for quick tests, but real projects define their own categories so you can filter your game's logs away from the engine's firehose.

Declare the category in a header (often a shared one) and define it once in a .cpp:

// MyGameLog.h β€” declare the category
#pragma once
#include "CoreMinimal.h"

// Args: category name, default verbosity, compiled-in max verbosity
DECLARE_LOG_CATEGORY_EXTERN(LogMyGame, Log, All);
// MyGameLog.cpp β€” define it exactly once
#include "MyGameLog.h"

DEFINE_LOG_CATEGORY(LogMyGame);
// Anywhere that includes MyGameLog.h:
UE_LOG(LogMyGame, Log, TEXT("Wave %d started with %d enemies"), WaveNumber, Count);

βœ… Pro Tip

Type your category name (LogMyGame) into the Output Log's filter box to see only your messages. You can also set per-category verbosity at runtime with the console: Log LogMyGame Verbose. Custom categories turn the log from noise into a signal.

On-Screen Messages

Sometimes you want feedback right in the viewport without hunting the log. GEngine->AddOnScreenDebugMessage prints to the screen during play.

if (GEngine)
{
    // Args: key (-1 = always add new), time on screen (s), color, message
    GEngine->AddOnScreenDebugMessage(
        -1, 5.0f, FColor::Green,
        FString::Printf(TEXT("Ammo: %d"), CurrentAmmo));
}

⚠️ Always guard GEngine

GEngine can be null in some contexts, so wrap the call in if (GEngine). Using a non-negative key instead of -1 updates the same line each frame rather than stacking new messages β€” handy for per-frame values.

check, ensure, verify

Assertions catch "this must be true" conditions at the exact point they break, instead of letting a bad value propagate into a confusing crash later. Unreal has three families with different behavior:

MacroIf condition is false…In shipping builds
check(x)Halts execution (crash) immediatelyCompiled out (no cost)
ensure(x)Logs + breaks in debugger, then continuesStill reports once
verify(x)Like check, but the expression still runs in shippingExpression runs; check removed
// check: an invariant that must hold β€” if not, we WANT to stop now.
check(MaxHealth > 0);   // a zero/negative max health is a design bug

// ensure: "shouldn't happen, but let's survive and get told about it."
if (ensure(Weapon != nullptr))
{
    Weapon->Fire();     // only runs if the ensure passed
}

// checkf / ensureMsgf: same, with a formatted message
checkf(Ammo >= 0, TEXT("Ammo went negative: %d"), Ammo);

πŸ“– Which one when?

  • check β€” a condition whose violation means the code is fundamentally broken and continuing is meaningless. Development-only.
  • ensure β€” a condition you expect to hold but want to recover from; it reports once and continues, so a designer's bad data doesn't hard-crash the editor.
  • verify β€” when the checked expression has a side effect you need even in shipping (e.g. it performs an operation and returns success).

βœ… Pro Tip

Favor ensure in gameplay code and the editor β€” it surfaces the bug in the log and debugger without taking down the whole session, which keeps you productive. Reserve check for genuine invariants where continuing would be nonsense.

Debug Drawing

Much game logic is spatial β€” a trace, a radius, a direction. Debug drawing renders temporary shapes in the world so you can see what your math is doing. Include DrawDebugHelpers.h.

#include "DrawDebugHelpers.h"

// A line from the actor forward 500 units, red, lasting 2 seconds:
const FVector Start = GetActorLocation();
const FVector End   = Start + GetActorForwardVector() * 500.0f;
DrawDebugLine(GetWorld(), Start, End, FColor::Red, false, 2.0f);

// A sphere showing an interaction radius:
DrawDebugSphere(GetWorld(), Start, 200.0f, 16, FColor::Green, false, 2.0f);

// A labeled point:
DrawDebugPoint(GetWorld(), End, 12.0f, FColor::Blue, false, 2.0f);
πŸ’‘ You'll rely on this in Module 5. When we do traces and overlaps, debug drawing is how you confirm the trace actually goes where you think. It turns "why isn't this hitting?" into an obvious picture. These draws are development aids β€” strip or guard them out of shipping builds.

Hands-on Exercise & Quiz

πŸ‹οΈ Exercise: Instrument a function

Objective: Add the right diagnostics to a damage function.

Given void ApplyDamage(float Amount, AActor* Target), add: (1) an ensure that Target is valid before use, (2) a check that Amount is non-negative, (3) a log line under a custom category recording who took how much, and (4) a debug sphere at the target's location.

βœ… Sample solution
void AWeapon::ApplyDamage(float Amount, AActor* Target)
{
    check(Amount >= 0.0f);
    if (!ensure(IsValid(Target))) { return; }

    UE_LOG(LogMyGame, Log, TEXT("%s took %.1f damage"),
        *Target->GetName(), Amount);

    DrawDebugSphere(GetWorld(), Target->GetActorLocation(),
        50.0f, 12, FColor::Red, false, 1.0f);

    // ... apply the damage ...
}

🎯 Quick Quiz

Question 1: To print an FString with %s in UE_LOG, you must:

Question 2: You want to catch a "shouldn't happen" case but keep the editor running. Use:

Question 3: Why define a custom log category like LogMyGame?

Summary

πŸŽ‰ Key Takeaways

  • UE_LOG(Category, Verbosity, TEXT("..."), ...) is your printf; dereference FString args with * for %s.
  • Define a custom log category (DECLARE_/DEFINE_LOG_CATEGORY) to filter your game's output and tune verbosity.
  • GEngine->AddOnScreenDebugMessage (guarded by if (GEngine)) shows feedback in the viewport.
  • Assertions: check halts on invariant violations (dev only), ensure reports once and continues, verify keeps the expression in shipping.
  • Debug drawing (DrawDebugLine/Sphere/Point) makes spatial logic visible β€” essential for the traces in Module 5.

πŸ“š Additional Resources

πŸš€ What's Next?

Module 2 is complete β€” you understand reflection, types, memory, and debugging, the foundation everything else stands on. Module 3 puts it to work: the gameplay framework, writing real Actors and Components in C++, and their lifecycles.

πŸŽ‰ Module 2 complete!

Foundations locked in. Time to build the things players actually see.