🎚️ Lesson 6.2: Exposing Properties to Blueprint
Functions are how Blueprint acts; properties are how designers configure. This lesson goes deeper than Lesson 2.3 on turning C++ member variables into well-organized, safe, discoverable knobs — the difference between a C++ class designers love and one they dread.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Combine edit and Blueprint-access specifiers deliberately for each property
- Use
meta = (ExposeOnSpawn)so a property can be set at spawn - Polish the inspector with categories, tooltips, and edit conditions
- Guard C++-owned state with
BlueprintReadOnlyand getters - React to editor edits with
PostEditChangeProperty
Estimated Time: 45 minutes
Engine Version: Unreal Engine 5.8
In This Lesson
The Two Axes, Applied
Recall the two independent axes from Lesson 2.3: editor access (EditAnywhere / VisibleAnywhere / Edit*Only) and Blueprint access (BlueprintReadWrite / BlueprintReadOnly). Here's how to combine them purposefully for a real class:
// Designer tunes per-instance; BP logic can also change it at runtime:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
float Damage = 25.0f;
// A type-wide constant: set on the Blueprint default only, BP can read it:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Combat")
int32 MagazineSize = 30;
// Runtime state C++ owns; BP (e.g. HUD) reads but cannot set:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "State")
int32 CurrentAmmo = 30;
// A component reference — shown for inspection, not reassigned by hand:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
TObjectPtr<UStaticMeshComponent> Mesh;
📖 Design the surface, don't just expose everything
Marking every field EditAnywhere, BlueprintReadWrite is a smell — it invites designers to break invariants and clutters the inspector. Ask per field: who should change this, and when? Tunable → EditAnywhere. Type constant → EditDefaultsOnly. C++-owned state → VisibleAnywhere, BlueprintReadOnly. A deliberate surface is a gift to your designers.
ExposeOnSpawn
When a Blueprint spawns your actor via "Spawn Actor from Class," it can set exposed properties at spawn time — pins appear right on the spawn node. Mark a property with meta = (ExposeOnSpawn = true).
// A projectile whose damage and speed are set when a weapon spawns it:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Projectile",
meta = (ExposeOnSpawn = true))
float Damage = 10.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Projectile",
meta = (ExposeOnSpawn = true))
float Speed = 2000.0f;
✅ The Blueprint mirror of SpawnActorDeferred
Remember SpawnActorDeferred from Lesson 3.4 — set properties before BeginPlay? ExposeOnSpawn is the Blueprint equivalent: the spawn node exposes those properties as input pins, and they're applied before the actor begins play. So a designer can spawn a projectile and set its damage right on the node — no C++ change to the spawner. C++ and Blueprint solving the same problem, each in their idiom.
Inspector Polish
Small touches make a C++ class feel professional in the editor. Building on the meta options from Lesson 2.3:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat|Ranged",
meta = (ClampMin = "0.0", ClampMax = "1000.0", UIMin = "0.0", UIMax = "500.0",
ToolTip = "Base damage per hit before modifiers."))
float Damage = 25.0f;
// A dependent property that only appears when relevant:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat")
bool bUsesSpread = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat",
meta = (EditCondition = "bUsesSpread", ClampMin = "0.0"))
float SpreadAngle = 2.0f;
| Polish | Effect |
|---|---|
Category = "A|B" | Nested, collapsible groups in Details |
ToolTip = "..." | Hover help (a code comment above the property also becomes a tooltip) |
ClampMin/Max | Hard value limits (typed values clamped) |
EditCondition | Show/enable a property only when another is set |
Units = "cm" | Displays a unit and enables unit conversion |
✅ Comments become tooltips
A // ... comment directly above a UPROPERTY is picked up by UHT as its tooltip — so documenting your code documents your inspector at the same time. Two birds, one comment.
Guarding State
Some values are dangerous to let Blueprint set directly — health, ammo, an internal index — because a bad write breaks an invariant. Expose them read-only and provide a BlueprintCallable setter that enforces the rules.
// Read-only to Blueprint — no direct writes.
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "State")
float CurrentHealth = 100.0f;
// The ONLY sanctioned way for Blueprint to change health — it clamps & notifies.
UFUNCTION(BlueprintCallable, Category = "State")
void SetHealth(float NewHealth)
{
CurrentHealth = FMath::Clamp(NewHealth, 0.0f, MaxHealth);
OnHealthChanged.Broadcast(CurrentHealth, MaxHealth); // event from Lesson 5.4
}
📖 Read-only property + Callable setter = encapsulation
This pairing gives Blueprint access through your rules: it can read the value freely and change it only via a function that clamps, validates, and fires events. It's the interop version of a private field with a public setter — designers get flexibility, you keep your invariants. Compare this to a bare BlueprintReadWrite float CurrentHealth, where any graph could set it to -999 and skip your death event.
Reacting to Edits
Sometimes you want C++ to respond the moment a designer changes a property in the editor — e.g. rebuild something when a size value changes. Override PostEditChangeProperty (editor-only).
#if WITH_EDITOR
void AMyActor::PostEditChangeProperty(FPropertyChangedEvent& Event)
{
Super::PostEditChangeProperty(Event);
// Which property changed?
const FName Name = (Event.Property != nullptr) ? Event.Property->GetFName() : NAME_None;
if (Name == GET_MEMBER_NAME_CHECKED(AMyActor, GridSize))
{
RebuildGrid(); // react to the designer's edit immediately
}
}
#endif
⚠️ Editor-only — wrap in WITH_EDITOR
PostEditChangeProperty exists only in editor builds, so guard it with #if WITH_EDITOR or a packaged build won't compile. Use GET_MEMBER_NAME_CHECKED to compare property names — it fails to compile if you typo the member name, unlike a raw string. This hook is for editor-time responsiveness, not runtime logic.
Hands-on Exercise & Quiz
🏋️ Exercise: A polished pickup
Objective: Give a pickup a designer-friendly property surface.
Amount— tuned per instance, BP-writable, clamped 1–999, with a tooltip.PickupType— set only on the Blueprint default, BP-readable.bConsumed— runtime state C++ owns, BP read-only.- Add a
meta = (ExposeOnSpawn)toAmountso a spawner can set it on the spawn node. - Provide a
BlueprintCallable Consume()that setsbConsumedsafely rather than exposing it writable.
✅ Reference
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pickup",
meta = (ClampMin = "1", ClampMax = "999", ExposeOnSpawn = true,
ToolTip = "How much this pickup grants."))
int32 Amount = 25;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Pickup")
EPickupKind PickupType = EPickupKind::Health;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "State")
bool bConsumed = false;
UFUNCTION(BlueprintCallable, Category = "Pickup")
void Consume() { bConsumed = true; /* ...effects, Destroy... */ }
🎯 Quick Quiz
Question 1: A value Blueprint should read but never set directly is best marked:
Question 2: What does meta = (ExposeOnSpawn = true) do?
Question 3: To let Blueprint change protected state safely, you should:
Summary
🎉 Key Takeaways
- Combine the editor-access and Blueprint-access axes deliberately per property — design the surface, don't expose everything.
meta = (ExposeOnSpawn = true)adds set-at-spawn pins on the Blueprint Spawn node — the BP mirror ofSpawnActorDeferred.- Polish with nested
Category,ToolTip(or code comments),ClampMin/Max,EditCondition, andUnits. - Guard invariant-critical state as
BlueprintReadOnly+ aBlueprintCallablesetter that validates and notifies. PostEditChangeProperty(wrapped in#if WITH_EDITOR) reacts to editor edits; useGET_MEMBER_NAME_CHECKED.
📚 Additional Resources
🚀 What's Next?
Functions and properties cross the boundary. But what about your own types — the USTRUCTs and UENUMs from Module 2? Next we make them first-class Blueprint citizens so you can pass rich data across the boundary, not just primitives.
🎉 Lesson complete!
Your inspectors shine. Now let's expose custom data types.