ποΈ 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
FindRowand iterate all rows - Create a
UDataAssetfor 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:
(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)"]
π 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 schema | Each 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 import | Data includes many asset references |
| e.g. all item stats, loot tables, dialogue lines | e.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.
- Define
FItemRow : public FTableRowBasewith a name, an icon (TObjectPtr<UTexture2D>), a stack size, and aTSubclassOf<AActor> PickupClass. - Write
bool GetItem(FName Id, FItemRow& Out) constthat looks up a row from aUDataTable*and copies it out (return false if missing). - 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 withGetAllRows. - 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.