Last week we looked at DataTable vs DataAsset vs Primary DataAsset. Dialogue is where that choice stops being theoretical. Almost every first dialogue system starts the same way: a Widget Blueprint, a Set Text node, and a Branch. It works. Then the NPC gets a second topic, then a condition, then a choice that unlocks a quest — and 40 nodes later you can't tell which pin leads where.
The fix isn't a better node layout. It's moving the conversation out of the graph entirely.
The rule: dialogue is data, not logic
Your Blueprint should know how to play a conversation. It should not know what anyone says. That single split is what makes the difference between a system that survives 30 lines and one that survives 3000.
Concretely: one DataTable holds the lines. One Blueprint reads a row, shows it, waits for input, jumps to the next row ID. The Blueprint never grows. The table does.
The row struct
Start with a struct — call it S_DialogueLine. The minimum that actually scales:
CB0
And the choice struct:
CB1
FName for IDs is deliberate — comparisons are pointer-cheap, and typos show up in the editor's dropdown instead of at runtime.
Use FText, never FString
This is the one that bites hardest and latest. FString cannot be localised. FText can. If you write your dialogue into FString fields and ship 4000 lines, your translation path is a rewrite of every row.
Better still: put the actual strings in a String Table and have the DataTable reference the keys. Then your translators get one asset, not fifteen tables. You can wire this up at any point, but doing it on day one costs nothing.
Conditions without a spaghetti graph
The temptation is Branch → Branch → Branch. Don't. Give each line an optional ConditionTag and each choice an optional RequiredTag, then ask one question at runtime: does the player's tag container have this tag?
CB2
That's the whole condition system. Quest progress, reputation, "has spoken to the blacksmith" — all of them become tags. One code path, unlimited conditions.
Facts: the memory layer
NPCs need to remember. Keep a simple map on a Game Instance Subsystem: Map<FName, int32>. TalkedToLeif = 1. BoughtSword = 3. Conditions read it, events write it, your save system serialises one map instead of hunting through every NPC.
Keep it flat and keep it small. A Fact is a number, not an object.
Portraits and voice lines: soft references only
Every dialogue row that hard-references a portrait texture or a voice cue pulls that asset into memory the moment the DataTable loads. A 300-row table with hard-referenced 2K portraits is a multi-hundred-megabyte hit before a single word is spoken.
Use TSoftObjectPtr / soft references in the struct and async-load on display — the pattern from async loading in Blueprints. The portrait appears one frame later. Nobody notices. Your memory graph does.
Events: tell, don't reach
When a choice should open a door or start a quest, do not let the dialogue Blueprint find the door. Fire an EventTag and let a listener handle it — the Event Dispatcher pattern again. Your dialogue system stays a dialogue system instead of slowly becoming your quest system.
Common pitfalls
- FString instead of FText. Costs you the entire localisation path. Unfixable cheaply once you have volume.
- Hard-referencing portraits and audio in the row struct. Loads your whole cast at startup.
- Storing the conversation state on the NPC. Two NPCs in the same conversation, or a save mid-dialogue, and it falls apart. State belongs to a subsystem.
- Row IDs as text you type by hand. One typo, one silent dead end. Use a
Namefield and let the DataTable dropdown validate it. - Building a node-graph editor before you have 50 lines of dialogue. A DataTable in a spreadsheet is faster to author than any graph until you're genuinely branching hard. Export CSV, edit in Excel, re-import.
30-second recap
- Blueprint plays conversations; the DataTable is the conversation.
FTextfrom day one, ideally backed by a String Table.- IDs are
FName. Jumps are ID lookups, not node wires. - Conditions and choices gate on Gameplay Tags — one code path.
- Facts live in a subsystem as a flat
Map<FName, int32>. - Portraits and voice are soft references, loaded async on display.
Next Sunday: abilities and cooldowns without GAS — why the full Gameplay Ability System is often the wrong first tool, and what a lightweight alternative looks like.
— Marco
Comments
Leave a comment