Skip to main content

🎚️ 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 BlueprintReadOnly and 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;
PolishEffect
Category = "A|B"Nested, collapsible groups in Details
ToolTip = "..."Hover help (a code comment above the property also becomes a tooltip)
ClampMin/MaxHard value limits (typed values clamped)
EditConditionShow/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.

  1. Amount — tuned per instance, BP-writable, clamped 1–999, with a tooltip.
  2. PickupType — set only on the Blueprint default, BP-readable.
  3. bConsumed — runtime state C++ owns, BP read-only.
  4. Add a meta = (ExposeOnSpawn) to Amount so a spawner can set it on the spawn node.
  5. Provide a BlueprintCallable Consume() that sets bConsumed safely 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 of SpawnActorDeferred.
  • Polish with nested Category, ToolTip (or code comments), ClampMin/Max, EditCondition, and Units.
  • Guard invariant-critical state as BlueprintReadOnly + a BlueprintCallable setter that validates and notifies.
  • PostEditChangeProperty (wrapped in #if WITH_EDITOR) reacts to editor edits; use GET_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.