Skip to main content

📦 Lesson 6.3: Exposing Structs, Enums & Data

Primitives cross the boundary easily, but real gameplay data comes in bundles — a damage event, a stat block, a weapon config. Making your USTRUCTs and UENUMs first-class Blueprint types lets you pass rich, self-documenting data between C++ and Blueprint cleanly.

🎯 Learning Objectives

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

  • Make a USTRUCT a Blueprint type with usable pins
  • Make a UENUM a Blueprint dropdown with friendly names
  • Pass structs and enums as function parameters and return values
  • Expose TSubclassOf class-picker properties to Blueprint
  • Recognize which types can and cannot cross the boundary

Estimated Time: 60 minutes

Engine Version: Unreal Engine 5.8

In This Lesson

BlueprintType Structs

You declared USTRUCTs in Lesson 2.2. Adding BlueprintType and marking members with Blueprint access turns a struct into a type Blueprint fully understands — you can make variables of it, pass it on pins, and break/make it in graphs.

USTRUCT(BlueprintType)   // ← makes it usable as a Blueprint type
struct FWeaponStats
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon")
    float Damage = 25.0f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon")
    float FireRate = 5.0f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon")
    int32 MagazineSize = 30;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon")
    FName WeaponName = TEXT("Rifle");
};

📖 Two levels of exposure

BlueprintType on the USTRUCT makes the struct usable in Blueprint; BlueprintReadWrite on each member makes those fields accessible in a "Break/Make Struct" node. Omit member specifiers and Blueprint sees the struct type but can't read its fields — usually not what you want. A fully-exposed struct shows up as neat, named pins.

✅ Structs bundle related data

Instead of a function with eight loose float parameters, pass one FWeaponStats. It's self-documenting, easy to extend (add a field without changing signatures), and reads cleanly on both sides of the boundary. This is the same "bundle of data" role from Lesson 2.2 — now Blueprint-visible.

BlueprintType Enums

A UENUM(BlueprintType) becomes a dropdown in the editor and a typed pin in Blueprint. It must use enum class ... : uint8, and UMETA(DisplayName) controls the friendly labels.

UENUM(BlueprintType)
enum class EWeaponType : uint8
{
    Pistol   UMETA(DisplayName = "Pistol"),
    Rifle    UMETA(DisplayName = "Assault Rifle"),
    Shotgun  UMETA(DisplayName = "Shotgun"),
    Sniper   UMETA(DisplayName = "Sniper Rifle")
};

⚠️ The : uint8 is mandatory for BlueprintType

Blueprint-exposed enums must have a uint8 underlying type — enum class EWeaponType : uint8. Without it, UHT rejects the BlueprintType enum. Also prefer scoped enum class over old-style enum to avoid name collisions (you write EWeaponType::Rifle, not a bare Rifle).

Now an enum property gives designers a clean dropdown:

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon")
EWeaponType Type = EWeaponType::Rifle;   // shows as a dropdown of display names

Passing Data Across

With both marked BlueprintType, they flow through UFUNCTIONs as parameters and return values — full rich data crossing the boundary.

// Return a struct to Blueprint (BlueprintPure getter):
UFUNCTION(BlueprintPure, Category = "Weapon")
FWeaponStats GetStats() const { return Stats; }

// Take a struct and an enum as parameters from Blueprint:
UFUNCTION(BlueprintCallable, Category = "Weapon")
void Configure(const FWeaponStats& NewStats, EWeaponType NewType);

// Return an array of structs — Blueprint gets a proper array of struct pins:
UFUNCTION(BlueprintCallable, Category = "Weapon")
TArray<FWeaponStats> GetAllVariants() const;
graph LR BP["Blueprint graph"] -->|"Configure(FWeaponStats, EWeaponType)"| CPP["C++ mechanics"] CPP -->|"GetStats() returns FWeaponStats"| BP
Figure 1: Rich data flowing both ways. Because the struct and enum are BlueprintType, Blueprint draws proper pins for them — no primitive-by-primitive plumbing.

✅ Const-ref for struct params

Note const FWeaponStats& for the parameter — the same efficient const-reference pass from Lesson 1.4. Blueprint handles the reference transparently; on the C++ side you avoid copying the whole struct. It's the idiomatic way to accept a struct across the boundary.

TSubclassOf Pickers

You met TSubclassOf for spawning in Lesson 3.4. As a UPROPERTY, it gives designers a filtered class picker in the editor and a class pin in Blueprint — the standard way to make "what to spawn / which type" data-driven.

// Designer picks BP_Rifle, BP_Shotgun... restricted to AWeaponBase subclasses.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Loadout")
TSubclassOf<AWeaponBase> StartingWeaponClass;

// A whole configurable list:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Loadout")
TArray<TSubclassOf<AWeaponBase>> UnlockableWeapons;
💡 The pattern crystallizing: a C++ system that takes TSubclassOf class references, FWeaponStats config structs, and EWeaponType enums — all set by designers in the editor — is the essence of data-driven design. The C++ provides the machinery; the data lives in Blueprint assets and the inspector. This is precisely what Lesson 6.4 assembles into the full base-class pattern, and what Module 7 formalizes with DataTables and DataAssets.

What Can Cross

A clear line on what's boundary-friendly and what isn't:

Crosses the boundary ✅Does NOT cross ❌
Primitives: int32, float, boolRaw pointers to non-UObjects
FString, FName, FTextstd:: types (std::vector, std::string)
UObject* / AActor* / TObjectPtrUSTRUCT/UENUM without BlueprintType
USTRUCT/UENUM with BlueprintTypeTemplates other than the supported containers
TArray/TMap/TSet of the aboveUn-reflected plain C++ classes
TSubclassOf<T>Function pointers / lambdas

⚠️ The error you'll see

Try to expose a non-reflection type on a UFUNCTION/UPROPERTY and UHT stops the build with a message like "Type is not supported by blueprint." The fix is always: make it reflection-friendly (add BlueprintType to your struct/enum, use an Unreal container instead of std::, or wrap it) — or keep it C++-only and expose a boundary-friendly summary instead.

Hands-on Exercise & Quiz

🏋️ Exercise: A loot table entry

Objective: Model a Blueprint-friendly data bundle.

  1. Create UENUM(BlueprintType) ERarity : uint8 with Common/Rare/Epic/Legendary and display names.
  2. Create USTRUCT(BlueprintType) FLootEntry with: a TSubclassOf<AActor> ItemClass, an ERarity Rarity, and a float DropChance (BP-writable each).
  3. Add a BlueprintCallable function void GrantLoot(const FLootEntry& Entry).
  4. Which member would fail to cross the boundary if you used std::string for a name instead of FName?
✅ Reference & answer
UENUM(BlueprintType)
enum class ERarity : uint8 {
    Common UMETA(DisplayName="Common"), Rare UMETA(DisplayName="Rare"),
    Epic UMETA(DisplayName="Epic"), Legendary UMETA(DisplayName="Legendary")
};

USTRUCT(BlueprintType)
struct FLootEntry {
    GENERATED_BODY()
    UPROPERTY(EditAnywhere, BlueprintReadWrite) TSubclassOf<AActor> ItemClass;
    UPROPERTY(EditAnywhere, BlueprintReadWrite) ERarity Rarity = ERarity::Common;
    UPROPERTY(EditAnywhere, BlueprintReadWrite) float DropChance = 0.1f;
};

A std::string name would fail — it's not reflection-friendly and UHT rejects it. Use FName or FString/FText.

🎯 Quick Quiz

Question 1: What makes a USTRUCT usable as a Blueprint type?

Question 2: A BlueprintType enum must have which underlying type?

Question 3: Which of these can NOT be exposed on a UFUNCTION?

Summary

🎉 Key Takeaways

  • USTRUCT(BlueprintType) + BlueprintReadWrite members makes a struct a full Blueprint type with break/make pins.
  • UENUM(BlueprintType) requires enum class ... : uint8; UMETA(DisplayName) sets friendly dropdown labels.
  • Pass structs (as const&) and enums through UFUNCTIONs; arrays of them work too.
  • TSubclassOf<T> properties give designers filtered class pickers — the backbone of data-driven design.
  • Only reflection-friendly types cross the boundary; std::/raw/un-reflected types don't — UHT enforces it.

📚 Additional Resources

🚀 What's Next?

You now have every interop tool — functions, properties, structs, enums, class pickers. The finale of the module assembles them into the pattern: a C++ base class of mechanics, extended by Blueprint subclasses of content. One AWeaponBase, many weapons.

🎉 Lesson complete!

Rich data crosses freely. Time to assemble the master pattern.