Chapter 05

Data-Driven Design in Unreal

The tools and patterns that let designers configure gameplay without touching C++ — and how to handle schema changes mid-production.

In this chapter

  1. The Philosophy
  2. Data Tables
  3. Data Assets
  4. Soft References & Async Loading
  5. Schema Changes Mid-Production
  6. The Designer Tool vs. Engineer Code Line
1

The Philosophy

Data-driven design in Unreal means separating behavior (C++) from configuration (assets). The code defines what a system can do; data assets define how it behaves for any specific instance.

The rule: C++ defines behavior and constraints. Content defines composition and tuning. A weapon's damage falloff curve belongs in a data asset. A character's walk speed belongs in an exposed Blueprint property. Neither belongs hardcoded in a constructor.

Getting this boundary right means:

The three main tools for this in Unreal are Data Tables, Data Assets, and the UPROPERTY exposure system.


2

Data Tables

A Data Table is a spreadsheet-like asset where each row is a struct. They are ideal for large sets of similar data — weapon stats, enemy definitions, level progression curves, localization strings.

Defining the row struct in C++

// C++ defines the schema — what columns exist
USTRUCT(BlueprintType)
struct FWeaponData : public FTableRowBase
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadOnly)
    float BaseDamage = 25.f;

    UPROPERTY(EditAnywhere, BlueprintReadOnly)
    float FireRate = 0.15f;

    UPROPERTY(EditAnywhere, BlueprintReadOnly)
    int32 MagazineSize = 30;

    UPROPERTY(EditAnywhere, BlueprintReadOnly)
    TSoftObjectPtr<UStaticMesh> WeaponMesh;
};

Designers then create a DT_Weapons Data Table asset using this struct, and fill in rows for Rifle, Shotgun, Sniper, etc. in the editor — no code changes needed to add a new weapon type.

Reading a row at runtime

UDataTable* WeaponTable = ...;
FWeaponData* Row = WeaponTable->FindRow<FWeaponData>(
    FName("Rifle"),
    TEXT("WeaponLookup")
);
if (Row)
{
    ApplyDamage(Row->BaseDamage);
}

CSV and JSON import

Data Tables can be imported from CSV or JSON, which means a designer can maintain weapon stats in a spreadsheet and re-import without opening Unreal. This is a key workflow for large content teams.


3

Data Assets

A Data Asset (UDataAsset) is a single UObject-based asset used when each entry is complex enough to need its own asset file rather than a row in a table. They work well for ability definitions, character configurations, and anything with nested references.

UCLASS(BlueprintType)
class UAbilityData : public UDataAsset
{
    GENERATED_BODY()

public:
    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
    TSubclassOf<UGameplayAbility> AbilityClass;

    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
    float Cooldown = 2.0f;

    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
    FGameplayTagContainer GrantedTags;

    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
    TSoftObjectPtr<UTexture2D> Icon;
};

Data Table vs. Data Asset

Data TableData Asset
ShapeMany rows, same structOne object, can be complex
Best forLarge flat datasets (stats, strings)Rich individual configurations
Designer UXSpreadsheet view in editorFull property panel
ReferencesRows reference by FName keyDirectly referenced as an asset

4

Soft References & Async Loading

A hard reference (UPROPERTY pointing directly to a UObject*) causes the asset to be loaded into memory when the owning object loads. In a large game, this chains into hundreds of megabytes loaded upfront.

A soft reference (TSoftObjectPtr or TSoftClassPtr) stores a path but does not load the asset until explicitly requested. This is how you keep memory under control in data-driven systems.

// Hard reference — loads mesh the moment this object loads
UPROPERTY(EditDefaultsOnly)
UStaticMesh* WeaponMesh;

// Soft reference — just a path, no load cost until needed
UPROPERTY(EditDefaultsOnly)
TSoftObjectPtr<UStaticMesh> WeaponMesh;

Async loading

// Request async load, get callback when done
FStreamableManager& Streamable = UAssetManager::GetStreamableManager();
Streamable.RequestAsyncLoad(
    WeaponMesh.ToSoftObjectPath(),
    FStreamableDelegate::CreateUObject(
        this, &AWeapon::OnMeshLoaded
    )
);
Use soft references in data assets by default. Only use hard references when you are certain the asset is always needed immediately (e.g. a character's primary mesh).

5

Schema Changes Mid-Production

Changing a data schema (adding, renaming, or removing a field from a struct or Data Table row) mid-production is one of the most disruptive operations in Unreal development. Here is the process for handling it safely.

Adding a new field

The safest change. Add the new UPROPERTY with a sensible default. Existing assets that do not have the field serialized will use the default. No migration needed.

// Safe to add — existing rows get the default value
UPROPERTY(EditAnywhere, BlueprintReadOnly)
float ArmorPenetration = 0.0f;

Renaming a field

Unreal will not automatically migrate serialized data to a renamed property — the old data is lost. Use the UPROPERTY metadata tag DisplayName to rename the UI label without renaming the variable, or use a redirect in DefaultEngine.ini:

[CoreRedirects]
+PropertyRedirects=(OldName="/Script/MyGame.FWeaponData.OldName",NewName="NewName")

Removing a field

Never remove a field while assets that reference it still exist in production. The workflow:

  1. Mark the property meta=(DeprecatedProperty) and communicate to content owners
  2. Give a migration window for content to stop using it
  3. Run a validation pass to confirm no assets reference it
  4. Remove the property in the next clean pass

The guardrail

Add validation on asset load so bad-data conditions surface at cook time, not at runtime in a live build:

virtual void PostLoad() override
{
    Super::PostLoad();
    // Catch misconfigured assets at load time
    ensureMsgf(BaseDamage > 0.f, TEXT("WeaponData %s has invalid BaseDamage"), *GetName());
}
The senior signal in a schema change question: you do not just describe the migration — you describe the guardrail that prevents the same class of problem from recuring.

6

The Designer Tool vs. Engineer Code Line

A recurring interview question: "Where do you draw the line between a designer-facing tool and engineer-owned code?" This is a rubric question — they want your framework for making that decision, not a single answer.

The rubric

FactorDesigner-facingEngineer-owned
Iteration frequencyChanged constantly during productionSet once, rarely touched
Blast radius of a mistakeLow — bad value breaks one featureHigh — bad value breaks the system
Performance sensitivityNot on a hot pathCalled every frame, must be fast
Who owns the dataDesigner or artistProgrammer
Requires compilationNever — asset or config onlyYes, or never exposed

In practice

High-iteration, low-blast-radius surfaces should be designer-facing. Expose them as UPROPERTY(EditDefaultsOnly) on Blueprint subclasses, or as rows in a Data Table. Lock anything performance-sensitive or architecturally load-bearing behind C++.

A useful test: "Could a designer break the game by setting this value wrong?" If yes and the blast radius is high, it belongs in C++ with validation. If yes but only in a localized way, expose it with clamping and a tooltip.

UPROPERTY specifiers as the control surface

SpecifierWhat it exposes
EditDefaultsOnlyEditable in Blueprint Class Defaults only — per-type tuning
EditInstanceOnlyEditable per placed actor in the level
BlueprintReadOnlyBlueprint can read state, cannot write it — good for exposing runtime values
meta=(ClampMin="0.0", ClampMax="100.0")Clamp input range in the editor
meta=(DeprecatedProperty)Marks a property for removal, surfaces a warning
← Chapter 4 ↑ Index Chapter 6 →