📦 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
USTRUCTa Blueprint type with usable pins - Make a
UENUMa Blueprint dropdown with friendly names - Pass structs and enums as function parameters and return values
- Expose
TSubclassOfclass-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;
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 takesTSubclassOfclass references,FWeaponStatsconfig structs, andEWeaponTypeenums — 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, bool | Raw pointers to non-UObjects |
FString, FName, FText | std:: types (std::vector, std::string) |
UObject* / AActor* / TObjectPtr | USTRUCT/UENUM without BlueprintType |
USTRUCT/UENUM with BlueprintType | Templates other than the supported containers |
TArray/TMap/TSet of the above | Un-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.
- Create
UENUM(BlueprintType) ERarity : uint8with Common/Rare/Epic/Legendary and display names. - Create
USTRUCT(BlueprintType) FLootEntrywith: aTSubclassOf<AActor> ItemClass, anERarity Rarity, and afloat DropChance(BP-writable each). - Add a
BlueprintCallablefunctionvoid GrantLoot(const FLootEntry& Entry). - Which member would fail to cross the boundary if you used
std::stringfor a name instead ofFName?
✅ 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)+BlueprintReadWritemembers makes a struct a full Blueprint type with break/make pins.UENUM(BlueprintType)requiresenum class ... : uint8;UMETA(DisplayName)sets friendly dropdown labels.- Pass structs (as
const&) and enums throughUFUNCTIONs; 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.