[UE5] Managing Parameters with Data Tables: Tune Your Game in CSV

Created: 2025-12-12Last updated: 2026-07-20

Stop opening Blueprints every time you tweak a number. This guide illustrates Data Tables built from a struct row shape and how to bulk-edit them in CSV, with a hands-on walkthrough of managing enemy parameters in a table and loading them on spawn.

You want to change an enemy's attack power from 10 to 12. Just for that, you open the Blueprint, hunt down the variable, compile, and hit Play . Once is fine, but balancing means doing it dozens of times.

A Data Table moves those numbers out into a table . Open it in Excel, tune everything at once, and import it back. The Blueprint only says "read this row from the table", and you never touch it again.

This article covers how to create a Data Table, how to import CSVs, and how to manage enemy parameters in a table and load them on spawn .

Numbers buried inside a Blueprint moving out into a table

What You'll Learn

  • A Data Table is a struct lined up as the shape of each row
  • Row Name is the key for each row (this matters most)
  • In CSV, column 1 is the row name and row 1 matches your variable names exactly
  • Use UTF-8 with BOM for non-ASCII text
  • Hands-on: manage enemy parameters in a table and load them on spawn

Sponsored

A Data Table Is "a Table of Structs"

In one sentence, a Data Table is a struct turned into the shape of one row, repeated across many rows .

A struct defining the shape of a row, with many rows of that shape lined up in a table

That means you need the struct first . Build a struct with attack, health, and movement speed, and those become the table's columns . After that you just keep adding rows.

Row NameAttackHealthSpeed
Slime530200
Goblin1260350
Ogre30200150

Once your data is in this shape, you get the following.

  • Tuning happens outside of Blueprint: no compiling required
  • You can see everything at once: compare the balance across enemies side by side
  • You can edit it in Excel: export and import via CSV

And the Blueprint side only says "read the Goblin row" . It has no idea what the numbers are.

Steps to Create One

There are two steps : create the struct, then create the Data Table pointing at it.

The two steps: create a struct, then specify it as the row shape to create a Data Table

1. Create the struct

Right-click → BlueprintsStructure . Define the fields (→ Creating Structs).

2. Create the Data Table

Right-click → MiscellaneousData Table . It asks for the Row Structure , so pick the struct you just made.

The Row Structure can't be changed later. It's locked to whatever struct you chose at creation. If you want a different shape, you'll be rebuilding the Data Table, so it's safest to finalize the struct's fields first .

If you define the struct in C++, it must inherit FTableRowBase . Structs created in Blueprint can be selected as a Row Structure as-is.

USTRUCT(BlueprintType)
struct FEnemyData : public FTableRowBase   // <- this is required
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    int32 Attack = 0;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float Health = 100.0f;
};

Bulk-Editing in CSV

You can edit a Data Table in the editor, but once the row count grows, CSV is dramatically faster .

Diagram showing that CSV column 1 must be the row name and row 1 must match the struct's variable names

CSVs follow a fixed format . Deviate from it and the import fails.

Name,Attack,Health,Speed
Slime,5,30,200
Goblin,12,60,350
Ogre,30,200,150
RuleDetails
Column 1The key for each row (Row Name). Must be unique
Row 1Exact match with the struct's variable names. Case-sensitive
SeparatorComma
EncodingUTF-8 with BOM if it contains non-ASCII text

There are two ways to import.

  • Overwrite an existing Data Table: right-click the asset → Reimport (or Import)
  • Create a new one from CSV: drag and drop the CSV into the Content Browser and pick a Row Structure

For day-to-day tuning you use the former. Edit the CSV, right-click, import. Done.

If text is garbled, suspect the save format. Picking plain "CSV" in Excel can produce a legacy regional encoding depending on your system. Choose "CSV UTF-8 (Comma delimited)" , or re-save it as UTF-8 with BOM in a text editor.

Reading It from Blueprint

Reading takes a single node: Get Data Table Row .

Get Data Table Row taking a Data Table and Row Name, returning Out Row and Found Row
PinDetails
Data TableWhich table to read
Row NameWhich row to read (the key)
Out RowThe struct for the row that was found
Found RowWhether it was found (Boolean)

And here's the biggest thing to watch for .

A miss is not an error. Out Row comes back as an empty struct (all zeros and empty strings) . So the symptom shows up as "for some reason the attack power is 0", and the cause is hard to spot. Always branch on Found Row .

Typos in Row Names happen constantly. Goblin and goblin are treated as different keys.

Sponsored

Hands-On: Managing Enemy Parameters in a Table

RPG monsters, tower defense waves, shoot-'em-up enemy ships. "Same mechanics, different numbers" shows up in every genre. Here we'll stop making one Blueprint per enemy and manage them in a table instead.

Setup to follow along: prepare the following.

KindNameContents
StructureS_EnemyDataParameters for one enemy type
Data TableDT_EnemyRow Structure set to S_EnemyData
CharacterBP_EnemyJust one . Numbers come from the table

Step 1: create the struct. Add these fields to S_EnemyData .

Variable nameTypeDefault value
AttackInteger0
HealthFloat100.0
MoveSpeedFloat300.0

Step 2: create the Data Table. Right-click → MiscellaneousData Table → select S_EnemyData as the Row Structure. Name it DT_Enemy .

Step 3: prepare a CSV and import it. In a text editor, write the following and save it as Enemy.csv in UTF-8 with BOM .

Name,Attack,Health,MoveSpeed
Slime,5,30,200
Goblin,12,60,350
Ogre,30,200,150

Right-click DT_EnemyReimport (or Import) and select this CSV. Open it and you should see three rows .

Step 4: have the enemy read it. Create two variables on BP_Enemy .

Variable nameTypeSetting
EnemyRowNameNameCheck Instance Editable (so you can set it per level instance)
CurrentHealthFloatDefault 0.0

Then read from the table on BeginPlay .

Node graph from BeginPlay through Get Data Table Row, branching on Found Row and applying the parameters
Event BeginPlay (BP_Enemy)
  → Get Data Table Row (Data Table: DT_Enemy, Row Name: EnemyRowName)
  → Branch (Condition: Found Row)
      True  → Break S_EnemyData (Out Row)
            → Set CurrentHealth (= Health)
            → Set Max Walk Speed (Target: Character Movement, = MoveSpeed)
            → Print String (Append("Load succeeded: ", EnemyRowName))
      False → Print String (Append("Row not found: ", EnemyRowName))  <- always include this

The key point is to always write the Found Row branch . Without it, a missing row quietly runs the enemy with zeros.

Step 5: place them in the level and check. Drop three BP_Enemy actors into the level and set each one's Enemy Row Name in the details panel to Slime / Goblin / Ogre .

Hit Play and check. All three use the same Blueprint, yet their movement speeds are visibly different. Slime is slow, Goblin is fast, Ogre is slower still. The log also shows three "Load succeeded" lines.

The real payoff starts here. Open Enemy.csv , change Goblin 's MoveSpeed to 600 , Reimport , and hit Play. Without ever opening the Blueprint, only the Goblin got faster.

Troubleshooting if it doesn't work.

  • You get "Row not found" → the spelling or capitalization of the Row Name. Copying the name shown in the table is the reliable fix
  • Garbled text on import → re-save as UTF-8 with BOM
  • Importing doesn't add rows → row 1 of your CSV doesn't match the struct's variable names
  • Speed doesn't change → did you wire Character Movement into the Target of Set Max Walk Speed ?

Two things to take away.

  • Don't ignore Found Row: a miss returns an empty struct, so the game quietly runs with zeros. Just logging the failure dramatically shortens the time it takes to find the cause
  • Keep it to one Blueprint: making a Blueprint per enemy type means touching all of them every time you fix shared logic. One system, numbers in a table is the maintainable shape

When you want the same data held per asset rather than as a table, Data Asset is the next option. If you need to rethink the row shape itself, head back to Structs.

Sponsored

Bonus: Good to Know for Later

Keeping CSVs inside the project makes them easier to manage. Create a folder like Content/Data/CSV for the source files and Git can track the diffs. Note that files placed directly under Content will be treated as assets by UE, so deciding on a location up front avoids accidents.

You can get the list of rows. Get Data Table Row Names returns every Row Name in the table as an array. Features like "spawn a random enemy" or "list every entry in a bestiary" start from this.

Data Tables aren't meant to be rewritten at runtime. They're strictly a place to store configuration values . Values that change, like current HP, live in Blueprint variables after loading. That's exactly why the hands-on copies Health into CurrentHealth .

Changing the struct can wipe the table's contents. Adding or removing fields may reset the data you entered. Export to CSV before making changes and you can restore if things break. Right-click → Export to write it out.


Summary

Four things to keep in mind when using Data Tables.

AspectTakeaway
PrerequisiteStruct first . The Row Structure can't be changed later
KeyLook rows up by Row Name . Case-sensitive
CSVColumn 1 is the key, row 1 matches variable names exactly, UTF-8 with BOM
Biggest gotchaA miss isn't an error . Branch on Found Row

And one design rule: one system, numbers in a table . Stop adding a Blueprint per enemy type and both tuning and bug-fixing get far easier.

That Blueprint you keep opening to tweak numbers, could those values live in a table instead?