How to Build a Dialogue System in Unity Without Plugins
A dialogue system does not need a plugin. ScriptableObjects, a small runner, and a clean data format give you branching, conditions, and localization without the dependency.

A dialogue system built from ScriptableObjects and a small monobehaviour runner covers branching, conditions, and localization without any external dependency. The entire thing is under 300 lines, easy to debug, and trivial to extend later. Start with the data format, not the UI.
Why build it yourself
Dialogue plugins solve a general problem. Your game has a specific one.
A plugin brings an editor, a runtime, a serialization format, and a set of assumptions about how dialogue works in your game. When those assumptions match, the plugin saves weeks. When they do not, you spend weeks working around them.
For most mobile games, the dialogue needs are simple enough that a custom system is faster to build, easier to maintain, and far easier to debug.
The data format
Everything flows from how you store the conversation. Get this right and the rest is straightforward.
A dialogue is a list of nodes. Each node has:
- An ID. A string, not an integer, so you can name them meaningfully.
- Speaker. Who is talking.
- Text. What they say, stored as a localization key.
- Choices. An array of options, each pointing to another node ID.
- Conditions. Optional checks that gate whether a choice appears.
- Events. Optional callbacks that fire when the node is entered.
Store this in a ScriptableObject. One asset per conversation. The inspector becomes your editor, and version control tracks changes for free.
The best dialogue format is the one your designer can edit without asking an engineer for help.
The ScriptableObject setup
Three assets handle everything.
DialogueNode. A serializable class, not a ScriptableObject itself. Contains the fields listed above. Nesting it inside the conversation asset keeps the project clean.
DialogueConversation. The ScriptableObject. Holds an array of DialogueNode, a starting node ID, and a display name. One asset per conversation in your game.
DialogueDatabase. A single ScriptableObject that references every conversation. Lets you look up a conversation by ID at runtime without scanning the project.
Keep the node class simple. Five fields cover most mobile dialogue needs. You can add portrait references, animation triggers, and audio cues later without changing the structure.
The runner
A single MonoBehaviour that does four things.
- Starts a conversation by loading the starting node.
- Displays the current node by sending text and choices to the UI.
- Evaluates conditions to filter which choices are available.
- Advances when the player picks a choice, loading the next node.
The runner does not own the UI. It raises events or calls an interface. This separation means you can swap the dialogue UI without touching the logic, which matters more than it sounds like it will.
Keep the runner under 100 lines. If it grows past that, something that belongs in the data has leaked into the code.
Branching without a graph editor
A graph editor is nice. It is not necessary.
Name your node IDs with a convention that makes the flow readable in a flat list.
- intro_01, intro_02 for linear sequences.
- choice_a_01, choice_b_01 for branches.
- end_good, end_bad for terminals.
With clear naming, the ScriptableObject inspector is readable enough for a game with dozens of conversations. A graph editor becomes worth building only when you pass a hundred nodes in a single conversation, which most mobile games never do.
Conditions and state
Conditions gate which choices appear. Keep the system simple.
A condition is a key-value check against a shared state dictionary. "HasItem:key" returns true if the player has the item. "QuestStage:rescue >= 2" checks progression.
Store the state dictionary separately from the dialogue system. The dialogue system reads it but never writes it. Events on nodes write it. This one-way dependency prevents the dialogue from becoming the game's state manager, which is a common trap in larger systems.
Three condition types cover most needs.
| Type | Example | Use |
|---|---|---|
| Flag check | HasItem:key | Gate a choice behind a pickup |
| Counter compare | Gold >= 50 | Gate behind a resource threshold |
| Quest stage | QuestStage:rescue == 2 | Gate behind story progression |
Parse these from strings so designers can type them into the inspector. A simple parser for this is under 30 lines.
Localization from the start
Store localization keys in the node, not raw text. This costs nothing extra during development and saves a full rewrite later.
The key is the node ID plus a suffix: intro_01_text, intro_01_choice_0. A CSV or JSON file maps keys to strings per language.
At runtime, the runner passes the key to a localization lookup. During development, the lookup returns the key itself if no translation exists, so you see readable text in the editor without needing a translation file yet.
This pattern is the same one used for UI strings, per Unity Packages Worth Importing. Applying it to dialogue from the start means the system is already localization-ready when the time comes.
Common mistakes
Four things that complicate dialogue systems unnecessarily.
- Putting logic in the text. Conditional text like "if player has sword, say X" belongs in the condition system, not in string formatting.
- Building a visual editor too early. The inspector works until it does not. Build the editor when you hit the wall, not before.
- Coupling the runner to the UI. The runner should not know whether dialogue appears in a bubble, a box, or a full-screen overlay.
- Storing save state inside the dialogue asset. Runtime state belongs in the save system, per Save Systems That Do Not Corrupt. The dialogue asset is read-only at runtime.
What we would do
Start with the ScriptableObject data format and two test conversations. Get the data right before writing the runner, because changing the data format later means touching every conversation asset.
Keep the runner minimal and event-driven. Let the UI subscribe to events rather than having the runner drive the UI directly.
Add localization keys from the first conversation, even if you only ship in one language. The cost is zero and the savings later are real.
The short version
- A ScriptableObject per conversation, with nodes stored as a serializable array.
- The runner is under 100 lines: load, display, evaluate conditions, advance.
- Name node IDs with a readable convention instead of building a graph editor early.
- Conditions check a shared state dictionary that the dialogue reads but never writes.
- Store localization keys from day one, not raw text.
- Keep the runner decoupled from the UI through events or an interface.
Build the data format first and test it with two conversations. If you want a dialogue system built into your game, reach out.
Related reading: Scriptable Objects for Game Config, Unity Packages Worth Importing, and Save Systems That Do Not Corrupt.
Got a game idea? We build it.
You bring the concept. We design, build, test and launch it, and you own 100% of the finished game.
Share Your Game Idea →