Design

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.

Vectra Play 6 min read
A Unity editor window showing a simple dialogue tree with branching paths

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:

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.

  1. Starts a conversation by loading the starting node.
  2. Displays the current node by sending text and choices to the UI.
  3. Evaluates conditions to filter which choices are available.
  4. 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.

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.

TypeExampleUse
Flag checkHasItem:keyGate a choice behind a pickup
Counter compareGold >= 50Gate behind a resource threshold
Quest stageQuestStage:rescue == 2Gate 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.

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

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.

#Unity#Design#Tools#Process
Your turn

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  →