Data-Driven Gameplay in UE5: DataTables and DataAssets
Hardcoding game values leads to painful iteration. Learn how to externalize your game data so designers can tune without touching code.
Why Data-Driven Design?
Early in development, it's tempting to hardcode values directly in code or Blueprints:
// Don't do this
void AWeapon::Fire()
{
float Damage = 25.0f; // Hardcoded
float FireRate = 0.5f; // Hardcoded
int32 MagazineSize = 30; // Hardcoded
// ...
}This works until you need to:
- Create 50 different weapons with different stats
- Let designers balance without programmer involvement
- Update values without recompiling
- Load different data for different game modes
- Export data for external tools or spreadsheets
Data-driven design separates "how things work" (code) from "what the values are" (data). Code defines behavior. Data defines configuration.
DataTables: Spreadsheet-Style Data
DataTables are Unreal's answer to spreadsheet data. Each row represents an item, enemy, level, or any other entity. Columns are properties.
Defining the Row Structure
First, create a struct that defines what each row contains:
// WeaponData.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataTable.h"
#include "WeaponData.generated.h"
USTRUCT(BlueprintType)
struct FWeaponData : public FTableRowBase
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadOnly)
FName WeaponID;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
FText DisplayName;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
float BaseDamage = 10.0f;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
float FireRate = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
int32 MagazineSize = 10;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
float ReloadTime = 2.0f;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
TSoftObjectPtr<UStaticMesh> WeaponMesh;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
TSoftClassPtr<AActor> ProjectileClass;
};Key points:
- Inherit from FTableRowBase
- Use USTRUCT(BlueprintType) for Blueprint access
- Use TSoftObjectPtr/TSoftClassPtr for asset references to avoid loading everything at once
Creating the DataTable Asset
In the Content Browser:
- Right-click, select Miscellaneous, then DataTable
- Choose your struct (FWeaponData) as the row type
- Add rows with the + button
- Each row needs a unique Row Name (used as the key)
Now you have a spreadsheet-like asset where designers can add and modify weapons without code changes.
Reading from DataTables in C++
// In your weapon class
UPROPERTY(EditDefaultsOnly)
UDataTable* WeaponDataTable;
void AWeapon::LoadWeaponData(FName WeaponID)
{
if (!WeaponDataTable)
{
UE_LOG(LogTemp, Error, TEXT("No weapon data table assigned"));
return;
}
// Find the row
FWeaponData* Data = WeaponDataTable->FindRow<FWeaponData>(
WeaponID,
TEXT("LoadWeaponData")
);
if (!Data)
{
UE_LOG(LogTemp, Warning, TEXT("Weapon %s not found"), *WeaponID.ToString());
return;
}
// Apply the data
BaseDamage = Data->BaseDamage;
FireRate = Data->FireRate;
MagazineSize = Data->MagazineSize;
ReloadTime = Data->ReloadTime;
// Load the mesh asynchronously
if (!Data->WeaponMesh.IsNull())
{
// Soft reference - load when needed
UStaticMesh* Mesh = Data->WeaponMesh.LoadSynchronous();
if (Mesh)
{
MeshComponent->SetStaticMesh(Mesh);
}
}
}Reading from DataTables in Blueprints
Use the "Get Data Table Row" node:
- Connect your DataTable reference
- Provide the Row Name
- Break the output struct to access individual values
- Handle the "Row Not Found" case
Iterating All Rows
void AWeaponManager::LoadAllWeapons()
{
if (!WeaponDataTable) return;
TArray<FName> RowNames = WeaponDataTable->GetRowNames();
for (const FName& RowName : RowNames)
{
FWeaponData* Data = WeaponDataTable->FindRow<FWeaponData>(
RowName,
TEXT("LoadAllWeapons")
);
if (Data)
{
// Process each weapon
RegisterWeapon(RowName, *Data);
}
}
}DataAssets: Individual Data Objects
DataAssets are useful when each item is complex enough to warrant its own asset file, or when you need inheritance and polymorphism.
Creating a DataAsset Class
// CharacterData.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "CharacterData.generated.h"
UCLASS(BlueprintType)
class UCharacterData : public UDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Identity")
FName CharacterID;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Identity")
FText DisplayName;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Identity")
FText Description;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Stats")
float MaxHealth = 100.0f;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Stats")
float MoveSpeed = 600.0f;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Stats")
float JumpHeight = 400.0f;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Visuals")
TSoftObjectPtr<USkeletalMesh> CharacterMesh;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Visuals")
TSoftObjectPtr<UAnimBlueprint> AnimationBlueprint;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Abilities")
TArray<TSubclassOf<UGameplayAbility>> Abilities;
};Creating DataAsset Instances
In the Content Browser:
- Right-click, select Miscellaneous, then DataAsset
- Choose your DataAsset class (UCharacterData)
- Fill in the properties
- Save with a descriptive name (DA_Character_Knight)
Using DataAssets
// Reference in your character class
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
UCharacterData* CharacterData;
void AMyCharacter::BeginPlay()
{
Super::BeginPlay();
if (CharacterData)
{
MaxHealth = CharacterData->MaxHealth;
GetCharacterMovement()->MaxWalkSpeed = CharacterData->MoveSpeed;
// Load mesh asynchronously
if (!CharacterData->CharacterMesh.IsNull())
{
USkeletalMesh* Mesh = CharacterData->CharacterMesh.LoadSynchronous();
GetMesh()->SetSkeletalMesh(Mesh);
}
}
}DataAsset Inheritance
DataAssets support inheritance, which DataTables don't:
// Base class
UCLASS(BlueprintType)
class UWeaponDataAsset : public UDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly) float BaseDamage;
UPROPERTY(EditDefaultsOnly) float FireRate;
};
// Derived class for ranged weapons
UCLASS(BlueprintType)
class URangedWeaponData : public UWeaponDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly) float ProjectileSpeed;
UPROPERTY(EditDefaultsOnly) float MaxRange;
UPROPERTY(EditDefaultsOnly) int32 MagazineSize;
};
// Derived class for melee weapons
UCLASS(BlueprintType)
class UMeleeWeaponData : public UWeaponDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly) float SwingArc;
UPROPERTY(EditDefaultsOnly) float ComboMultiplier;
};When to Use Each
Use DataTables When:
- You have many similar items (hundreds of inventory items)
- Data fits a flat structure (no inheritance needed)
- Designers want to edit in a spreadsheet view
- You need to import/export CSV data
- You're storing lookup tables or configuration
Use DataAssets When:
- Each item is complex with nested data
- You need inheritance (base weapon, ranged weapon, shotgun)
- Items have significantly different structures
- You want separate asset files for version control
- You need to add methods to the data class
Dynamic Content Loading
Soft references (TSoftObjectPtr, TSoftClassPtr) are essential for data-driven design. They let you reference assets without loading them until needed.
Asynchronous Loading
void AWeapon::LoadWeaponMeshAsync()
{
if (WeaponData->WeaponMesh.IsNull())
{
return;
}
// Check if already loaded
if (WeaponData->WeaponMesh.IsValid())
{
ApplyMesh(WeaponData->WeaponMesh.Get());
return;
}
// Load asynchronously
FStreamableManager& StreamableManager =
UAssetManager::GetStreamableManager();
StreamableManager.RequestAsyncLoad(
WeaponData->WeaponMesh.ToSoftObjectPath(),
FStreamableDelegate::CreateUObject(
this,
&AWeapon::OnWeaponMeshLoaded
)
);
}
void AWeapon::OnWeaponMeshLoaded()
{
if (WeaponData->WeaponMesh.IsValid())
{
ApplyMesh(WeaponData->WeaponMesh.Get());
}
}Loading Multiple Assets
void ACharacter::LoadCharacterAssets()
{
TArray<FSoftObjectPath> AssetsToLoad;
if (!CharacterData->CharacterMesh.IsNull())
{
AssetsToLoad.Add(CharacterData->CharacterMesh.ToSoftObjectPath());
}
if (!CharacterData->AnimationBlueprint.IsNull())
{
AssetsToLoad.Add(CharacterData->AnimationBlueprint.ToSoftObjectPath());
}
FStreamableManager& Manager = UAssetManager::GetStreamableManager();
Manager.RequestAsyncLoad(
AssetsToLoad,
FStreamableDelegate::CreateUObject(
this,
&ACharacter::OnAssetsLoaded
)
);
}Primary Asset Labels and Asset Manager
For larger projects, the Asset Manager provides more control:
// In your DataAsset
UCLASS(BlueprintType)
class UItemData : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
// Unique ID for this asset
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FPrimaryAssetId ItemId;
// Override to return the asset ID
virtual FPrimaryAssetId GetPrimaryAssetId() const override
{
return FPrimaryAssetId(
FPrimaryAssetType("Item"),
GetFName()
);
}
};Configure asset types in Project Settings under Asset Manager. This enables features like:
- Automatic asset discovery
- Chunk assignment for streaming
- Dependency tracking
- Async loading by asset type
Practical Patterns
Pattern: Registry System
// Central registry that loads and caches all items
UCLASS()
class UItemRegistry : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
UFUNCTION(BlueprintCallable)
UItemData* GetItem(FName ItemID);
private:
UPROPERTY()
TMap<FName, UItemData*> ItemCache;
void LoadAllItems();
};
void UItemRegistry::LoadAllItems()
{
UAssetManager& Manager = UAssetManager::Get();
TArray<FPrimaryAssetId> ItemIds;
Manager.GetPrimaryAssetIdList(
FPrimaryAssetType("Item"),
ItemIds
);
for (const FPrimaryAssetId& Id : ItemIds)
{
FAssetData AssetData;
Manager.GetPrimaryAssetData(Id, AssetData);
UItemData* Item = Cast<UItemData>(AssetData.GetAsset());
if (Item)
{
ItemCache.Add(Item->ItemId.ItemName, Item);
}
}
}Pattern: Composite Data
// Character data referencing other data assets
UCLASS()
class UCharacterBuildData : public UDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly)
UCharacterData* BaseCharacter;
UPROPERTY(EditDefaultsOnly)
TArray<UWeaponDataAsset*> StartingWeapons;
UPROPERTY(EditDefaultsOnly)
TArray<UAbilityData*> Abilities;
UPROPERTY(EditDefaultsOnly)
UProgressionData* ProgressionCurve;
};Pattern: Difficulty Scaling
// Enemy data with difficulty modifiers
USTRUCT(BlueprintType)
struct FEnemyData : public FTableRowBase
{
GENERATED_BODY()
UPROPERTY(EditAnywhere) float BaseHealth;
UPROPERTY(EditAnywhere) float BaseDamage;
UPROPERTY(EditAnywhere) float EasyMultiplier = 0.7f;
UPROPERTY(EditAnywhere) float NormalMultiplier = 1.0f;
UPROPERTY(EditAnywhere) float HardMultiplier = 1.5f;
float GetHealth(EDifficulty Difficulty) const
{
return BaseHealth * GetMultiplier(Difficulty);
}
float GetMultiplier(EDifficulty Difficulty) const
{
switch (Difficulty)
{
case EDifficulty::Easy: return EasyMultiplier;
case EDifficulty::Hard: return HardMultiplier;
default: return NormalMultiplier;
}
}
};Debugging Tips
- Asset Audit: Use the Asset Audit window to see what's loaded and why.
- Reference Viewer: Right-click assets to see their reference chains.
- Size Map: Analyze asset sizes to find data that's too heavy.
- Logging: Log when data is loaded/accessed to trace issues.
Automate Data Management with UnrealPilot
UnrealPilot can help you work with data-driven systems. Query your DataTables, find assets that reference specific data, or batch-modify data values across your project. Request beta access to streamline your data-driven workflows.