The tools and patterns that let designers configure gameplay without touching C++ — and how to handle schema changes mid-production.
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.
Getting this boundary right means:
The three main tools for this in Unreal are Data Tables, Data Assets, and the UPROPERTY exposure system.
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.
// 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.
UDataTable* WeaponTable = ...;
FWeaponData* Row = WeaponTable->FindRow<FWeaponData>(
FName("Rifle"),
TEXT("WeaponLookup")
);
if (Row)
{
ApplyDamage(Row->BaseDamage);
}
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.
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 | Data Asset | |
|---|---|---|
| Shape | Many rows, same struct | One object, can be complex |
| Best for | Large flat datasets (stats, strings) | Rich individual configurations |
| Designer UX | Spreadsheet view in editor | Full property panel |
| References | Rows reference by FName key | Directly referenced as an asset |
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;
// Request async load, get callback when done
FStreamableManager& Streamable = UAssetManager::GetStreamableManager();
Streamable.RequestAsyncLoad(
WeaponMesh.ToSoftObjectPath(),
FStreamableDelegate::CreateUObject(
this, &AWeapon::OnMeshLoaded
)
);
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.
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;
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")
Never remove a field while assets that reference it still exist in production. The workflow:
meta=(DeprecatedProperty) and communicate to content ownersAdd 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());
}
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.
| Factor | Designer-facing | Engineer-owned |
|---|---|---|
| Iteration frequency | Changed constantly during production | Set once, rarely touched |
| Blast radius of a mistake | Low — bad value breaks one feature | High — bad value breaks the system |
| Performance sensitivity | Not on a hot path | Called every frame, must be fast |
| Who owns the data | Designer or artist | Programmer |
| Requires compilation | Never — asset or config only | Yes, or never exposed |
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++.
| Specifier | What it exposes |
|---|---|
EditDefaultsOnly | Editable in Blueprint Class Defaults only — per-type tuning |
EditInstanceOnly | Editable per placed actor in the level |
BlueprintReadOnly | Blueprint 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 |