π 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_LOGwith 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, andverify - 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);
| Verbosity | Use for | Note |
|---|---|---|
Fatal | Unrecoverable β crashes the game intentionally | Always compiled in |
Error | Something is wrong and visible | Red in the log |
Warning | Suspicious but non-fatal | Yellow β easy to spot |
Display | Important normal info | Shown by default |
Log | General developer logging | The everyday level |
Verbose / VeryVerbose | Deep detail | Off unless enabled |
π Format specifiers you'll use constantly
%dβ integer (int32) Β·%f/%.1fβ float Β·%sβ a string- For
%s, dereference anFStringwith*:*MyStringor*GetName(). Forgetting the*is the most common logging mistake. %dforboolprints 0/1; or use%swith 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:
| Macro | If condition is false⦠| In shipping builds |
|---|---|---|
check(x) | Halts execution (crash) immediately | Compiled out (no cost) |
ensure(x) | Logs + breaks in debugger, then continues | Still reports once |
verify(x) | Like check, but the expression still runs in shipping | Expression 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 yourprintf; dereferenceFStringargs with*for%s.- Define a custom log category (
DECLARE_/DEFINE_LOG_CATEGORY) to filter your game's output and tune verbosity. GEngine->AddOnScreenDebugMessage(guarded byif (GEngine)) shows feedback in the viewport.- Assertions:
checkhalts on invariant violations (dev only),ensurereports once and continues,verifykeeps 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.