TutorialSeptember 12, 202613 min read

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:

  1. Right-click, select Miscellaneous, then DataTable
  2. Choose your struct (FWeaponData) as the row type
  3. Add rows with the + button
  4. 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:

  1. Right-click, select Miscellaneous, then DataAsset
  2. Choose your DataAsset class (UCharacterData)
  3. Fill in the properties
  4. 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.

Request beta access →