🪞 Lesson 2.2: The Reflection System — UCLASS, USTRUCT, UENUM
Reflection is the single most important idea in Unreal C++. It's how a compiled language gains the ability to inspect itself at runtime — and it's the reason the editor, Blueprint, saving, and networking can all touch your code. Get this, and the whole engine clicks into place.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Define reflection and explain why C++ needs help to get it
- Describe what the Unreal Header Tool (UHT) generates and when
- Use
UCLASS,USTRUCT,UENUM, andGENERATED_BODYcorrectly - Explain what powers
Cast<T>and the runtime type system - List the four engine systems reflection unlocks
Estimated Time: 60 minutes
Engine Version: Unreal Engine 5.8
In This Lesson
What Reflection Means
Reflection is a program's ability to examine its own structure at runtime: to ask "what class is this object? what properties does it have? what functions can I call by name?" Languages like C# and Java have this built in. Standard C++ does not — once compiled, a C++ type's names and structure are erased; there's just memory and machine code.
Unreal needs reflection badly. The editor must draw a property panel from a class it didn't know about at compile time. A save file must serialize objects by walking their properties. Blueprint must call C++ functions by name. Networking must know which properties to replicate. None of that is possible without runtime type information.
📖 The core idea
Unreal adds reflection to C++ with a build step. You annotate your code with macros (UCLASS, UPROPERTY…), and a tool reads those annotations before compilation and generates extra C++ that records your type's structure. That generated data is the reflection system.
The Unreal Header Tool
The Unreal Header Tool (UHT) runs as part of every build, before the C++ compiler. It scans your headers for reflection macros and generates a companion file — the .generated.h you always include last — plus a .gen.cpp that registers your types.
with UCLASS/UPROPERTY"] --> B["UHT scans headers"] B --> C["Generates MyActor.generated.h
+ registration code"] C --> D["C++ compiler compiles
your code + generated code"] D --> E["Runtime has full type info
(UClass objects)"]
⚠️ This explains a whole class of errors
If you write GENERATED_BODY() but forget to #include "MyActor.generated.h", or put that include in the wrong place, UHT can't wire things up and you get confusing errors about the macro. The generated header is UHT's output for that file — it must be included, and included last.
At runtime, the result is that every reflected class has a corresponding UClass object describing it — its name, parent, properties, and functions. You can retrieve it with UMyActor::StaticClass(), and the engine uses it constantly under the hood.
UCLASS, USTRUCT, UENUM
Three macros mark the three kinds of reflected type. Each pairs with a body macro.
UCLASS — reflected classes (usually UObject-derived)
UCLASS()
class MYPROJECT_API AEnemy : public AActor
{
GENERATED_BODY() // required inside every UCLASS
public:
UPROPERTY(EditAnywhere, Category = "Enemy")
float MaxHealth = 100.0f;
};
USTRUCT — reflected plain-data structs
USTRUCT(BlueprintType) // BlueprintType: usable as a Blueprint variable/pin
struct FDamageInfo
{
GENERATED_BODY() // structs use the SAME body macro
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float Amount = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FName DamageType;
};
📖 UCLASS vs USTRUCT — the practical difference
A UCLASS (a UObject) is a first-class engine citizen: garbage-collected, networkable, referenced by pointer, can have functions exposed to Blueprint. A USTRUCT is a lightweight value type — copied around, not garbage-collected, no UFUNCTIONs. Use structs for bundles of data (a hit result, a stat block); use classes for things with identity and behavior.
UENUM — reflected enumerations
UENUM(BlueprintType)
enum class EWeaponType : uint8 // uint8 base is required for BlueprintType enums
{
Pistol UMETA(DisplayName = "Pistol"),
Rifle UMETA(DisplayName = "Rifle"),
Shotgun UMETA(DisplayName = "Shotgun")
};
✅ Pro Tip
Use enum class (scoped enums) with a uint8 base for anything exposed to Blueprint. UMETA(DisplayName = ...) controls the friendly name shown in editor dropdowns. We build data-driven systems on these in Module 7.
Cast and the Type System
Because reflection records each object's real class, Unreal can offer a safe runtime cast — Cast<T>. It converts a base pointer to a derived type only if the object truly is that type; otherwise it returns nullptr. No crash, no undefined behavior.
AActor* HitActor = /* something we got from a trace or overlap */;
// Is this actor actually an AEnemy? Cast tells us safely.
if (AEnemy* Enemy = Cast<AEnemy>(HitActor))
{
// Inside this block, Enemy is a valid AEnemy* — use its API freely.
Enemy->TakeDamage(25.0f);
}
// If HitActor wasn't an AEnemy, Cast returned nullptr and we skip the block.
⚠️ Cast, don't C-style cast
Never use a C-style cast ((AEnemy*)HitActor) on UObjects — it blindly reinterprets memory and will crash or corrupt if the type is wrong. Cast<T> consults the reflection system and is the only correct tool. This is a rule you'll apply in nearly every gameplay lesson.
The same type info powers IsA(), StaticClass(), class-based spawning, and the "class picker" dropdowns you see in the editor — all of it reading the UClass data UHT generated.
What Reflection Unlocks
Everything distinctive about Unreal C++ traces back to reflection. Four big systems depend on it:
(UClass, UProperty, UFunction)"] --> Editor["Editor: auto-generated
Details panels"] R --> BP["Blueprint: call C++ by name,
C++ nodes"] R --> Ser["Serialization: save/load,
copy/paste, undo"] R --> Net["Networking: replicate
marked properties"]
| System | What reflection provides | Course module |
|---|---|---|
| Editor UI | Details panel widgets from UPROPERTY | 2, 6 |
| Blueprint | C++ exposed as nodes via UFUNCTION | 6 |
| Serialization | Save/load, copy, undo walk properties | 7 |
| Networking | Replicate properties & call RPCs | 10 |
💬 The payoff mindset: when you write UPROPERTY(EditAnywhere, Replicated), you're not "configuring a variable" — you're registering it with the editor and the network system simultaneously. Reflection is the shared substrate.
Hands-on Exercise & Quiz
🏋️ Exercise: Model a small system
Objective: Choose the right reflected type for each piece and write the declarations.
Design the data for a pickup system: a kind of pickup (health, ammo, coin), a bundle describing one pickup's effect (a type and an amount), and the actor that sits in the world. Write the UENUM, USTRUCT, and UCLASS skeletons.
💡 Hint
Kind → UENUM(BlueprintType) enum class : uint8. Bundle of data → USTRUCT(BlueprintType) with two UPROPERTYs. World object → UCLASS deriving from AActor.
✅ Sample solution
UENUM(BlueprintType)
enum class EPickupKind : uint8
{
Health, Ammo, Coin
};
USTRUCT(BlueprintType)
struct FPickupEffect
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite)
EPickupKind Kind = EPickupKind::Health;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 Amount = 25;
};
UCLASS()
class MYPROJECT_API APickup : public AActor
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pickup")
FPickupEffect Effect;
};
🎯 Quick Quiz
Question 1: Why does standard C++ need UHT to get reflection?
Question 2: You need a lightweight, copied value type bundling a few fields, usable in Blueprint. Use:
Question 3: Which converts AActor* to AEnemy* safely?
Summary
🎉 Key Takeaways
- Reflection is runtime self-knowledge of types; standard C++ lacks it, so Unreal generates it.
- The Unreal Header Tool (UHT) runs before the compiler, reads your macros, and emits the
.generated.hplus registration code. UCLASS(UObjects — identity, GC, Blueprint),USTRUCT(value-type data bundles), andUENUM(enums) mark reflected types; each needsGENERATED_BODY().Cast<T>uses reflection for a safe runtime downcast (nullptr on mismatch) — never C-style cast a UObject.- Reflection is the shared substrate for the editor UI, Blueprint, serialization, and networking.
📚 Additional Resources
🚀 What's Next?
You've seen the macros; next we go deep on the two that shape your everyday code the most — the specifiers inside UPROPERTY and UFUNCTION that decide exactly how your members surface to the editor, Blueprint, and beyond.
🎉 Lesson complete!
The engine's nervous system is no longer a mystery.