Chapter 21

UCLASS Specifiers & Data Ownership

Which UCLASS specifiers gate Blueprint visibility, why a Blueprint is a class and a Data Asset is an instance, and the ownership specifiers — EditInlineNew, Instanced, DefaultToInstanced — that decide whether a UObject property is shared or owned.

In this chapter

  1. Core UCLASS Specifiers
  2. Blueprint vs. Data Asset: Class vs. Instance
  3. Inheritance, Mutation Safety & Where Logic Lives
  4. Loading, Memory & Choosing Between Them
  5. Keeping Leaf Blueprints Data-Only
  6. Instanced Ownership
1

Core UCLASS Specifiers

Four specifiers control how a C++ class is exposed to Blueprints and the editor.

SpecifierControlsNotes
Blueprintable / NotBlueprintableWhether a Blueprint class can derive from this C++ classInherited by subclasses, which is why it's rarely written explicitly on Actor classes — AActor and UActorComponent already declare it. Use NotBlueprintable to cut off inheritance for a C++-only branch (managers, subsystems, pure data plumbing).
BlueprintType / NotBlueprintTypeWhether the class can be used as a value — variable type, function parameter, array element type, cast targetIndependent of Blueprintable. Best declared explicitly on each class meant to be usable, rather than relying on inheritance. NotBlueprintType hides a subclass from type pickers.
AbstractMakes the class non-instantiableWon't appear as a spawnable in Place Actors or class pickers; SpawnActor on it fails. Use for a base class that defines an interface but has no meaningful default behavior.
ConstMarks every property and function on the class as const for the reflection/scripting layerLegacy. Enforces nothing in C++ and is unrelated to BlueprintPure. Rarely seen outside a few engine internals; safe to ignore in modern code.

The four Blueprintable / BlueprintType combinations:

CombinationMeaning
Blueprintable, BlueprintTypeSubclass it and pass it around — the typical gameplay base class
Blueprintable onlyDesigners extend it, but it isn't a variable type
BlueprintType onlyDesigners reference instances but can't author subclasses — config/data objects
NeitherInvisible to Blueprint entirely
UCLASS(Abstract, Blueprintable, BlueprintType)
class UAbilityBase : public UObject
{
    GENERATED_BODY()
};
A Blueprint child of an abstract C++ class is concrete by default. The abstract-ness stops at the first Blueprint subclass unless the abstract option is also ticked in that Blueprint's own Class Settings.

2

Blueprint vs. Data Asset: Class vs. Instance

Everything in this chapter follows from one fact.

Blueprint

A class — a template for making things. Nothing exists until something spawns or instantiates it.

Data Asset

An instance — an actual object, saved to disk. It doesn't make more things; it is the thing.

This single difference drives referencing, inheritance, mutation safety, and loading behavior, covered section by section below.

Data-Only Blueprints

A Blueprint normally has two parts: a graph (logic) and default values (Details panel). A data-only Blueprint is a Blueprint where the graph was never touched — just a child class with changed defaults. Unreal detects this automatically and shows a stripped-down editor window (Details panel only, with an "Open Full Blueprint Editor" link). It is not a distinct asset type or a checkbox — it's a normal Blueprint that happens to be empty of logic, and the check is re-evaluated every time the editor opens it.

What flips it out of data-only status:

Special case: an Actor Blueprint with only the automatic DefaultSceneRoot still counts as data-only — the engine ignores that node since it wasn't added by the user. The check is stateless and reversible in both directions — delete the component/node/variable and it becomes data-only again.

Why it matters in practice. Data-only Blueprints take a fast compile/load path (no graph to compile), while a Blueprint with components has a Simple Construction Script that runs real per-spawn work. Negligible for a handful of assets; matters at scale (hundreds of weapon/item variants).

Where Default Values Actually Live: the CDO

Every class has a hidden singleton called the Class Default Object (CDO), auto-created at load (Chapter 6). Editing the Details panel of a data-only Blueprint edits its CDO. Reading a data-only Blueprint's values without spawning anything goes through the CDO:

const UMyWeapon* Defaults = GetDefault<UMyWeapon>(WeaponClass);
float Damage = Defaults->Damage;

A Data Asset has no such indirection — the object already exists:

float Damage = WeaponData->Damage;

This also shapes how the reference is stored:

TSubclassOf<AMyWeapon> WeaponClass;   // reference to a TYPE (Blueprint)
TObjectPtr<UWeaponData> WeaponData;   // reference to an OBJECT (Data Asset)

3

Inheritance, Mutation Safety & Where Logic Lives

Blueprints inherit. Data Assets do not. Base weapon Blueprint: Damage = 10, Range = 1000. Children Pistol/Rifle/Shotgun. Rifle overrides Damage = 25, leaves Range alone. Change Range on the base to 2000 → all three children that never overrode it pick up the new value automatically. The editor shows a revert arrow next to any property that diverges from its parent.

Data Assets are flat and standalone. Fifty weapon Data Assets means fifty separate edits to change one shared value (or a script, or multi-select property matrix editing) — there is no parent to change. Composition can simulate inheritance: give the Data Asset a pointer to a shared "archetype" asset and fall back to it in code when a field is unset. This is hand-built and needs fallback logic wherever the value is read.

Tradeoff worth naming: deep Blueprint hierarchies get hard to reason about ("why is this value 40 when nothing on this asset mentions damage"). Flat data is dumb but honest.

Runtime Mutation Safety

A Data Asset is one shared object in memory. If ten enemies reference DA_GoblinStats and one writes to it, all ten see the change — and in the editor, that write dirties the asset on disk, risking gameplay state leaking into source data. Treat Data Assets as read-only at runtime; copy values out into the actor/component for per-instance state.

Spawning from a Blueprint class is safe by construction — each spawned actor is its own object with its own copy of the values.

Where Logic Is Allowed to Live

Data-only Blueprints can silently grow logic — add one node and it's no longer data-only, with no warning or gate. Convenient for quick designer overrides, risky for "just data" assumptions elsewhere in the codebase. This is a team-discipline problem, not something the engine prevents.

Data Assets structurally cannot hold logic through the normal workflow (barring an unusual Blueprint subclass of a DataAsset class). This makes the data/logic split enforced by the tooling rather than by convention.


4

Loading, Memory & Choosing Between Them

Data Assets are the lighter option. A Blueprint drags in a UBlueprintGeneratedClass, its CDO, its component hierarchy (if an Actor), and every hard reference in its defaults — so a data-only weapon Blueprint with an assigned mesh/sound pulls those into memory on class load (Chapter 7 covers why hard references pull their target in).

UPrimaryDataAsset is the intended hook into the Asset Manager (Chapter 9): override GetPrimaryAssetId, register the type in project settings, get asset registry scanning, asset bundles, async loading, and chunking for free. Blueprint classes can be registered as primary assets too, but carry the extra class/CDO overhead. Both support TSoftObjectPtr for deferred loading of heavy content — this part is a wash.

Choosing Between Them

Two questions, in order:

  1. Does this get created, or just consumed? Spawned / instantiated / needs overridable functions → class (Blueprint). Configuration read by a system that already exists → Data Asset.
  2. Is shared-value inheritance useful here? A real tuning hierarchy where the base ripples down to children → Blueprints win outright. Genuinely flat data (item definitions, stat blocks, tuning curves) → Data Assets are cleaner and lighter.
Use caseChoice
Spawnable actor with behavior variantsBlueprint
Item definitions, stat blocks, config tablesData Asset
DataTable-like data but wanting real asset refs + type safetyData Asset
Designer-tunable variant of an existing actorData-only Blueprint

5

Keeping Leaf Blueprints Data-Only

Declaring a variable on a Blueprint always makes it a real Blueprint — this cannot be avoided directly. The fix is to reorganize the hierarchy so only leaf classes need to stay data-only.

Sandwich the Hierarchy

Only the variants at the bottom (the ones duplicated dozens of times for designers) need to be data-only. Classes in the middle that define structure are supposed to be real classes.

AWeapon (C++)              <- shared fields
  |- ARifle (C++)          <- rifle-only fields declared HERE
       |- BP_AK47          <- data only
       |- BP_M4            <- data only
       |- BP_ScoutRifle    <- data only
UCLASS(Blueprintable)
class ARifle : public AWeapon
{
    GENERATED_BODY()
public:
    UPROPERTY(EditDefaultsOnly, Category = "Rifle")
    int32 MagazineSize = 30;

    UPROPERTY(EditDefaultsOnly, Category = "Rifle")
    float ReloadTime = 2.0f;
};

Every data-only child inherits these fields with full revert-arrow support, and never declares anything itself. If C++ isn't available: same shape, one level lower. Make BP_Rifle_Base a regular Blueprint that declares the rifle variables; its children stay data-only. Pay the cost once instead of per-leaf.

Composition Instead of Subclassing

Put a pointer on the base class and let the pointed-at object carry type-specific data, so leaf Blueprints never need their own variables at all.

UCLASS(Blueprintable)
class AWeapon : public AActor
{
    UPROPERTY(EditDefaultsOnly, Category = "Config")
    TObjectPtr<UWeaponData> WeaponData;
};

URifleData : UWeaponData carries the rifle fields. Every weapon Blueprint stays data-only permanently — new weapon categories mean new Data Asset classes, not new Blueprint layers.

Subclassing vs. Composition Tradeoffs

Anti-pattern: One Fat Base Class

Putting every field (rifle, shotgun, launcher) on a single base class works for small projects but grows badly — a shotgun's Details panel ends up showing HomingTurnRate doing nothing, and nobody can tell which fields are live. If unavoidable, gate visibility cosmetically:

UPROPERTY(EditDefaultsOnly, meta = (EditCondition = "bIsProjectile"))
float ProjectileSpeed = 3000.f;
This only hides the field in the UI — the data still exists on every instance.

6

Instanced Ownership: EditInlineNew, Instanced, DefaultToInstanced

These solve one problem: UObject properties are references by default; sometimes ownership is wanted instead.

The Default Behavior

UPROPERTY(EditDefaultsOnly)
TObjectPtr<UWeaponConfig> Config;

This gives an asset picker — a reference to an existing asset on disk. Two weapons pointing at the same asset share it.

EditInlineNew (on the class)

A permission flag: "this class may be created inline in a Details panel," instead of only picked from the Content Browser.

UCLASS(EditInlineNew, Blueprintable, Abstract)
class UWeaponConfig : public UObject
{
    GENERATED_BODY()
};

Without it, the editor never offers to create one inline. Default for UObject is effectively NotEditInlineNew.

Instanced (on the property)

Says "this pointer owns its object."

UPROPERTY(EditDefaultsOnly, Instanced)
TObjectPtr<UWeaponConfig> Config;

Two effects: the Details panel becomes a class picker + inline expandable editor instead of an asset picker, and the object is deep-copied on instantiation — each child Blueprint and each spawned actor gets its own copy rather than sharing the parent's object. (Implies ExportSubObject, which is what makes the sub-object save inside the owning asset.)

DefaultToInstanced (on the class)

Convenience: every property pointing at this class is automatically treated as Instanced, even without the property-level specifier.

UCLASS(EditInlineNew, DefaultToInstanced, Blueprintable, Abstract)
class UWeaponConfig : public UObject { ... };

This is what UActorComponent itself declares — the reason every component pointer on an actor automatically owns its component without ever writing Instanced explicitly.

Caveat: DefaultToInstanced applies everywhere the class is referenced. If a shared reference to the same config object is ever wanted elsewhere, it makes that awkward. Prefer per-property Instanced when intent should stay visible at the point of use.

Putting It Together

Both EditInlineNew (class) and Instanced (property) are needed together — one without the other gives either a class with nothing offering to create it, or a property still wanting an asset reference.

UCLASS(EditInlineNew, Blueprintable, Abstract)
class UWeaponConfig : public UObject
{
    GENERATED_BODY()
public:
    UPROPERTY(EditDefaultsOnly)
    float Damage = 10.f;
};

UCLASS()
class URifleConfig : public UWeaponConfig
{
    GENERATED_BODY()
public:
    UPROPERTY(EditDefaultsOnly)
    int32 MagazineSize = 30;
};

UPROPERTY(EditDefaultsOnly, Instanced, Category = "Config")
TObjectPtr<UWeaponConfig> Config;
SituationChoice
Shared across many owners, authored once, browsable in Content BrowserData Asset, no specifiers
Owned by exactly one thing, never reused, no asset-file clutter wantedEditInlineNew + Instanced
Base class where the answer is always owned (e.g. components)DefaultToInstanced

Note: Instanced works on arrays too (TArray<TObjectPtr<UWeaponConfig>>) — a common pattern for a list of modifiers/effects where each entry is a different subclass.

Interview drill

Answer each out loud before re-reading the chapter.

  1. Why does a Blueprint child of an abstract C++ class default to concrete? What has to be done to keep it abstract?
  2. You add a Static Mesh Component to a data-only weapon Blueprint just to preview it, then delete it. Does the Blueprint's data-only status matter for anything besides the editor window it opens in?
  3. Ten enemies reference the same Data Asset and one of them writes to it at runtime. What happens, and how do you avoid it?
  4. A Rifle and a Shotgun both need a SpreadPattern field, but a Pistol doesn't. Where does the field go, and why?
  5. A class is marked EditInlineNew but not DefaultToInstanced, and a property of that type is declared without Instanced. What does the Details panel show, and why?
← Chapter 20 ↑ Index