Skip to main content

πŸ—‚οΈ Lesson 7.1: DataTables & DataAssets in C++

Great games are data-driven: designers add a new weapon, enemy, or item by editing data, not code. Module 6 exposed your types to the editor; now we store many instances of that data in assets. DataTables and DataAssets are the two workhorses β€” and knowing which to use is half the skill.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Define a DataTable row struct with FTableRowBase
  • Look up rows by name with FindRow and iterate all rows
  • Create a UDataAsset for a single configuration object
  • Choose DataTable vs DataAsset for a given need
  • Reference these assets from C++ via UPROPERTY

Estimated Time: 60 minutes

Engine Version: Unreal Engine 5.8

In This Lesson

Two Tools for Data

Both store designer-edited data as assets, but they're shaped differently:

graph TD DT["DataTable
(a spreadsheet)"] --> R1["Row: Goblin"] DT --> R2["Row: Skeleton"] DT --> R3["Row: Dragon"] DA["DataAsset
(a single config object)"] --> One["One rich configuration
(can hold asset references)"]
Figure 1: A DataTable is many rows of the same struct (a spreadsheet of enemies). A DataAsset is one configuration object β€” richer per-instance, referenced individually.

πŸ“– The one-line distinction

DataTable = "a table of N things sharing one schema," keyed by row name β€” great for lists you look up (all enemy stats, all items). DataAsset = "one configured thing," referenced directly β€” great when each config is rich, holds asset references, and you point to it specifically (a character preset, a level config). Both are pure data β€” no behavior.

DataTable Row Structs

A DataTable's schema is a USTRUCT that inherits FTableRowBase. Each column is a UPROPERTY; each row an instance.

// EnemyRow.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataTable.h"      // for FTableRowBase
#include "EnemyRow.generated.h"

USTRUCT(BlueprintType)
struct FEnemyRow : public FTableRowBase   // ← inherit FTableRowBase
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Enemy")
    FText DisplayName;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Enemy")
    float MaxHealth = 100.0f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Enemy")
    float MoveSpeed = 400.0f;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Enemy")
    TSubclassOf<AActor> EnemyClass;    // asset reference β€” which BP to spawn
};

βœ… Where the table comes from

In the editor, right-click β†’ Miscellaneous β†’ Data Table, pick FEnemyRow as the row struct, and add rows (or import a CSV/JSON with matching columns). Designers fill in a spreadsheet; your C++ reads it. Note the row struct reuses everything from Module 6 β€” BlueprintType, TSubclassOf, exposed properties.

Looking Up Rows

Expose a UDataTable* property so a designer assigns the table, then look up rows by their FName key.

// A UPROPERTY the designer sets to the DT_Enemies asset:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Data")
TObjectPtr<UDataTable> EnemyTable;

void ASpawner::SpawnFromRow(FName RowName)
{
    if (!EnemyTable) { return; }

    // FindRow returns a pointer to the row struct, or nullptr if the key is absent.
    static const FString Context = TEXT("Enemy lookup");
    const FEnemyRow* Row = EnemyTable->FindRow<FEnemyRow>(RowName, Context);
    if (!Row) { return; }

    // Use the row's data to configure a spawn (Lesson 3.4):
    if (Row->EnemyClass)
    {
        AActor* Enemy = GetWorld()->SpawnActor<AActor>(
            Row->EnemyClass, GetActorTransform());
        // ...apply Row->MaxHealth, Row->MoveSpeed to the enemy...
    }
}

// Iterate every row:
TArray<FEnemyRow*> AllRows;
EnemyTable->GetAllRows<FEnemyRow>(TEXT("Iterate"), AllRows);

⚠️ FindRow returns a pointer β€” check it, and mind the context string

FindRow gives you a pointer to the row (nullptr if the name isn't in the table) β€” same "check before use" habit as TMap::Find (Lesson 2.4). The second argument is a context string used only for warning messages when a lookup fails; pass something descriptive so failures are easy to trace in the log.

DataAssets

A UDataAsset is a standalone asset that is one configuration. You subclass it, add properties, and create instances as assets in the Content Browser. Unlike a DataTable row, a DataAsset is referenced directly and can be as rich as you like.

// WeaponDataAsset.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "WeaponDataAsset.generated.h"

UCLASS(BlueprintType)
class MYPROJECT_API UWeaponDataAsset : public UDataAsset
{
    GENERATED_BODY()

public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon")
    FText DisplayName;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon")
    TObjectPtr<USkeletalMesh> Mesh;      // direct asset references are fine here

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon")
    TObjectPtr<USoundBase> FireSound;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon")
    FWeaponStats Stats;                   // reuse the struct from Lesson 6.3
};
// Reference one from a weapon and read it:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon")
TObjectPtr<UWeaponDataAsset> WeaponData;

void AWeaponBase::ApplyData()
{
    if (WeaponData)
    {
        Mesh->SetSkeletalMesh(WeaponData->Mesh);
        Stats = WeaponData->Stats;
    }
}

πŸ“– DataAsset vs "just a Blueprint default"

You could also store config on a Blueprint subclass (Lesson 6.4). DataAssets shine when you want to share one config across many things, swap configs at runtime, or keep data separate from the actor that uses it. For deeper asset-management features (async loading, discovery by type), UPrimaryDataAsset adds an "asset manager" identity β€” worth knowing exists as you scale.

Which One to Use

Use a DataTable when…Use a DataAsset when…
You have many rows of the same schemaEach config is a distinct, rich object
You look items up by a key (row name)You reference the config directly by pointer
Data suits a spreadsheet / CSV importData includes many asset references
e.g. all item stats, loot tables, dialogue linese.g. a weapon preset, a character class, a level config
πŸ’‘ They combine well. A common setup: a DataTable of items where each row's UPROPERTY points to a DataAsset for the heavy per-item config. The table gives you the lookup and overview; the DataAssets hold the rich detail. Don't treat it as either/or.

Hands-on Exercise & Quiz

πŸ‹οΈ Exercise: An item catalog

Objective: Model item data both ways and compare.

  1. Define FItemRow : public FTableRowBase with a name, an icon (TObjectPtr<UTexture2D>), a stack size, and a TSubclassOf<AActor> PickupClass.
  2. Write bool GetItem(FName Id, FItemRow& Out) const that looks up a row from a UDataTable* and copies it out (return false if missing).
  3. Now imagine one "legendary sword" with lots of unique asset references (custom mesh, VFX, sound, ability). Would you add it as a row or make a DataAsset? Why?
βœ… Lookup & answer
bool UItemLibrary::GetItem(FName Id, FItemRow& Out) const
{
    if (!ItemTable) { return false; }
    if (const FItemRow* Row = ItemTable->FindRow<FItemRow>(Id, TEXT("GetItem")))
    {
        Out = *Row;
        return true;
    }
    return false;
}

The legendary sword is a good DataAsset β€” it's one distinct, richly-configured object with many asset references, referenced directly. Common items fit the table's uniform schema.

🎯 Quick Quiz

Question 1: A DataTable's row struct must inherit from:

Question 2: FindRow returns what when the row name isn't in the table?

Question 3: One richly-configured object referenced directly (not looked up by key) is best as a:

Summary

πŸŽ‰ Key Takeaways

  • DataTable = many rows of one schema, keyed by row name; the row struct inherits FTableRowBase.
  • Look up with Table->FindRow<FRow>(Name, Context) (returns a pointer β€” check it) or iterate with GetAllRows.
  • DataAsset (subclass UDataAsset) = one rich configuration object, referenced directly, ideal for many asset references.
  • Choose by shape: uniform list looked up by key β†’ DataTable; distinct rich config referenced directly β†’ DataAsset. They combine well.
  • Both reuse Module 6 interop β€” BlueprintType, TSubclassOf, exposed properties β€” to stay designer-editable.

πŸ“š Additional Resources

πŸš€ What's Next?

DataTables and DataAssets hold gameplay data. But some values are settings β€” volume, difficulty, key bindings β€” that belong in .ini files and persist between runs. Next: config properties.

πŸŽ‰ Lesson complete!

Your game runs on data. Next, on settings.