HOME/ASSETS/DIALOGUE SYSTEM

ASSET DOCUMENTATION · VERSION 1.0

Dialogue System
Easy & Pro.

Complete reference for the graph editor, runtime, localization, and Excel workflow. Unity 2021 LTS+ · TextMeshPro · Zero external dependencies.

01 · QUICK START

Your first dialogue

Six steps to build and play your first dialogue graph.

  1. Create a graphOpen Window → Dialogue System → Graph Editor, then click Create. A new graph opens with one Say node.
  2. Build the flowRight-click or press Ctrl+Space to add nodes. Drag from an output port to an input port.
  3. Write the textClick a Say node and type the Speaker and Text straight into its body.
  4. SaveClick Save in the toolbar. There is no autosave and no Ctrl+S binding.
  5. Build a UIOpen Window → Dialogue System → Create Dialogue UI to generate a fully wired prefab.
  6. Play itAdd a DialogueTrigger, set its UI and graph, then call Interact() or enable triggerOnStart.

What's in the box

FileRole
DialogueGraph.csGraph asset (ScriptableObject) and node connections
DialogueNode.csSay, Choice, Event, Condition, End, Timer, and Random nodes
DialogueChoice.csChoice options, conditions, and session variables
DialogueLocalization.csRuntime locale tracking and TranslationSet
DialogueRunner.csOptional shared settings, variables, and event hub
DialogueUI.csRuntime player that drives a graph on screen
DialogueUIAutoSetup.csEditor wizard that builds a ready-made UI prefab
Editor/Graph window, canvas, node views, search, Excel utilities, and USS stylesheet. Keep it in a folder literally named Editor.

02 · CORE CONCEPTS

Graphs, node names, ports

A DialogueGraph is a ScriptableObject (Create → Dialogue System → Dialogue Graph) holding a polymorphic nodes list, a flat connections list (from/to GUID + port index), a startNodeGuid, and metadata: dialogueName, description, and category.

Each node has one input port (In) and one or more outputs. An output feeds exactly one destination: connecting it again replaces the old wire.

MethodWhat it does
GetNode(guid)Look up a node by GUID.
Node(name) / TryGetNode(...)Look up a node by its coding name.
StartNodeThe configured start node, or the first node if none is set.
GetConnectedNodes(guid, port)All nodes wired to an output port.
GetNextNode(guid, port)The single next node for a port; this is what the runtime uses.
Validate()Lists problems: empty graph, no start node, Choice without choices, duplicate or empty names.

03 · NODE TYPES

Seven building blocks

All nodes derive from DialogueNode (guid, position, nodeNameForCoding, NodeType, and an OnEnter(runner) extension point that the built-in runtime does not use).

Say

1 input, 1 output: Out. NPC line. Sets nameplate, portrait, and audio, types the text, then waits for Space / Left-Click.

  • text, speakerName: source text with per-language translations
  • portrait: Sprite · voiceLine: AudioClip
  • animationTrigger is not auto-fired; read it in onNodeStarted

Choice

1 input, one output per choice (index = i). Shows a prompt and buttons; flow continues down the picked button's port.

  • promptText, speakerName, choices[]
  • autoSelectTime: when >0, auto-picks port 0 after N seconds; 0 waits forever
  • buttonLayout: Left/Right/Top/Bottom (default Top); buttonSpacing default 2
  • Each choice has text, visibilityCondition, and showIfNotMet (greyed out instead of hidden)

Event

1 input, 1 output: Out. Pass-through call into game code: camera shake, quest update, cutscene, anything.

  • eventName: name your handler registers for · eventArg: free-form string
  • waitForCompletion: true pauses playback until the handler calls done()

Condition

1 input, 2 outputs: 0 True · 1 False. Evaluates a list of conditions and takes the True or False port.

conditions[] · mode: All (AND) or Any (OR).

End

1 input, no outputs. Terminates the graph, fires onDialogueEnded, and hides the panel. A graph may have several Ends. No fields.

Timer

1 input, 2 outputs: 0 Done · 1 Skip. Waits, then branches; optionally skippable by the player.

  • duration · showCountdown · countdownText (use {time} for remaining seconds)
  • skippable · skipButtonText: when empty, Space/Click skips instead of a button

Random

1 input, portCount outputs (labeled with %). Picks one output port each time it is reached.

portCount (minimum 2) · weighted · weights[] (higher = more likely).

04 · VARIABLES & CONDITIONS

State, flags and branching

DialogueVariables is a lightweight key → string store, one per DialogueRunner (or per DialogueUI without a runner).

MethodPurpose
Set(key, value)Session-scoped variable.
SetGlobal(key, value)Static, process-wide; shared by every instance. Use for persistent flags such as quest state.
TryGet(key, out value)Session value first, then global fallback.
Increment(key, delta = 1)Parses as float, adds delta, stores it back.
Format(text)Replaces every {name} with its value; automatic in Say / Choice text.
SaveGlobals() · LoadGlobals(dict) · ClearGlobals()Hooks for your own save system.

DialogueCondition

Fields: variableName, op (Equals, NotEquals, GreaterThan, LessThan, GreaterOrEqual, LessOrEqual, Contains), and value.

  • Comparison is numeric when both sides parse as floats; otherwise it is a string comparison.
  • Contains is always a substring check.
  • A variable that does not exist evaluates to false.
  • DialogueCondition.EvaluateAll(list, mode, vars) powers Condition nodes, DialogueTrigger.startConditions, and choice visibility.

05 · RUNTIME

DialogueUI, events, runner, trigger

DialogueUI

The main runtime controller. Attach one to your dialogue Canvas (or let the wizard do it). Each UI is self-contained, so several can coexist, for example one per split-screen player.

Inspector fieldPurpose
dialoguePanelGameObject shown/hidden while dialogue runs.
speakerNameText, dialogueBodyTextTMP_Text references.
portraitImage, nameplateBackgroundPortrait sprite; nameplate hidden when no speaker is set.
choicesContainer, choiceButtonPrefabParent for runtime-spawned buttons; prefab = Button + child TMP_Text.
charsPerSecond, typingSoundTypewriter speed and optional per-character sound.
pauseGameDuringDialogueTime.timeScale = 0 while running (shared runner's setting wins).
skipToEndOnInputSpace/Click during typing completes the line instead of being ignored.

API

StartDialogue(graph) · StopDialogue() begin from the start node / force-stop (fires onDialogueEnded).

IsRunning · CurrentNode · Variables expose current state; Variables is the runner's or UI's own.

RegisterEventHandler(name, h) / Unregister... hook up Event nodes. Node(nodeName) returns { onStarted, onEnded } UnityEvents for that node.

Events and firing order

EventFires whenTypical use
onDialogueStartedStartDialogue() called, before the graph beginsFreeze player, open dialogue camera
onDialogueEndedEnd node reached or StopDialogue()Resume control, save progress
onNodeStarted(name)Every node, on entryLogging, analytics, drive an Animator
onNodeEnded(name)Every node, after behavior and before nextPer-node cleanup
Node("X").onStarted / .onEndedOnly for node XGive this quest, play this sound

Use global events for “every node the same way” (logging, a UI flash). Use Node("Name") for one special node. This scales better than an if (nodeName == "...") chain.

DialogueRunner, DialogueTrigger, TypewriterEffect

  • DialogueRunner is an optional shared hub. Link it via DialogueUI.sharedRunner so several UIs share Variables and Event handlers. Its pauseGameDuringDialogue / skipToEndOnInput override the UI's, but a UI's own handler for an event name wins over the runner's.
  • TypewriterEffect is auto-added to the DialogueUI GameObject; you rarely touch it.
DialogueTrigger fieldPurpose
dialogueUITarget UI (auto-found via FindAnyObjectByType in Awake if empty).
graphThe DialogueGraph to play.
triggerOnStartFire automatically in Start().
triggerOnTrigger · interactionTagFire on OnTriggerEnter (needs trigger Collider); tag defaults to “Player”.
startConditions · conditionModeGate the trigger behind conditions.

Call Interact() or TryStartDialogue() yourself for scripted starts. The trigger refuses to start while IsRunning is true.

Choice button layout

ApplyChoiceLayout disables any LayoutGroup on choicesContainer, measures each button, then stacks them from the container pivot in the chosen direction (Left / Right / Top / Bottom) with buttonSpacing plus the largest button size, so buttons never overlap. Default is Top / 2 px.

06 · GRAPH EDITOR

Canvas, toolbar, shortcuts

Open via Window → Dialogue System → Graph Editor, or double-click any DialogueGraph asset.

  • Left panel: every graph in the project, grouped in collapsible folders. Create / Load / Refresh buttons, an x per row to delete (with confirmation), and a green category badge.
  • Right panel: canvas, with a MiniMap pinned top-left.
ToolbarAction
SaveWrites node positions and connections into the asset, then AssetDatabase.SaveAssets().
ValidateRuns DialogueGraph.Validate() and shows the result.
Language (globe)Language picker, plus Excel export/import.
Auto LayoutTidy columns via breadth-first walk from the start node; unreachable nodes stay put.
CenterFrames the whole graph (FrameAll).
Undo / Redo · DeleteUndo/redo; delete the open graph asset (confirmed, permanent).

Creating and wiring nodes

  • Right-click the canvas → Add Node, press Ctrl+Space, or drop a dragged wire on empty space to open the searchable node list: Basic (Say, Choice, End) · Logic (Condition, Event) · Flow (Timer, Random).
  • Drag from an output dot to an input dot. Compatible ports highlight; a port cannot connect to itself or to the same side.
  • Deleting a node removes its connections. Deleting the start node clears startNodeGuid (falls back to the first node).
  • Deleting a choice removes its port and shifts later wires down, so there are never gaps in port indices.
  • Copy/paste (Ctrl+C / Ctrl+V) gives new GUIDs and unique names. Wires between copied nodes are kept; wires to outside nodes are not.
  • Every structural edit is wrapped in Undo.RecordObject: Ctrl+Z, Ctrl+Y / Ctrl+Shift+Z.

Node view

Color-coded: blue Say, green Choice, orange Event, purple Condition, dark red End, teal Timer, violet Random. The start node gets a highlighted border.

  • Collapsible + preview: Arrow button in title bar collapses a node. A live preview line shows roughly what it displays at runtime.
  • Language badge: editing a non-English language shows a colored badge (e.g. FR) on every translatable node.

07 · LOCALIZATION

Translations stored right on the nodes

NodeEnglish fieldTranslation field
SayNodetexttextTranslations
SayNodespeakerNamespeakerTranslations
ChoiceNodepromptTextpromptTranslations
ChoiceNodespeakerNamespeakerTranslations
DialogueChoicetexttextTranslations

At runtime, GetFormattedText, GetLocalizedSpeaker, GetLocalizedPrompt, and DialogueChoice.GetText resolve against LocalizationSystem.CurrentLocale, fall back to English, then apply {variable} substitution. DialogueUI needs no changes.

using EasyProDialogues;
using UnityEngine;

public class LocalizationExample : MonoBehaviour
{
    public void SwitchToFrench()
    {
        string currentLocale = LocalizationSystem.CurrentLocale; // "en", "fr", ... (default "en")
        LocalizationSystem.SetLocale("fr"); // Switch instantly; the active dialogue updates.
        string defaultLocale = LocalizationSystem.DefaultLocale; // "en"
    }
}
  • DialogueLocalizationLoader (optional) picks the starting language: startLocale forces one; empty auto-detects via SystemLanguageToLocale. rememberChoice persists to PlayerPrefs. Call loader.SetLocale("de") from your settings menu.

Language menu (globe icon)

EntryWhat it does
Edit in / languageRepopulates the canvas so every text field shows and edits that language.
Add Language…About 28 presets (French, German, Spanish, Japanese, Arabic…) or a custom code; switches you into it.
Remove Language / languageHides it from the picker. Stored translations are kept until you re-add the code.
Export / Import ExcelSee Excel Workflow.

English (en) is always present and cannot be removed. The project language list lives in an auto-created asset at Assets/Editor/EasyProDialogues/DialogueLocalizationSettings.

In a non-English language, labels read Text [FR], fields get a light-blue border, and an empty field shows a gray “EN: …” hint. Clearing a field removes that override (runtime falls back to English).

08 · EXCEL WORKFLOW

Export, translate, import

  1. ExportLanguage menu → Export All Dialogues to Excel…
  2. TranslateTranslator fills the empty language cells.
  3. ImportLanguage menu → Import Excel into Dialogues…

Export scans every graph in the project into one Translations sheet. Translators only fill empty cells in the language columns.

GraphNodeFieldIndexen (default)frde
Chapter1Intro_SayText—Hello, traveler!
Chapter1Intro_SaySpeaker—Old Man
Chapter1Choice_1Prompt—What do you want?
Chapter1Choice_1Choice0Tell me about the ruins
Chapter1Choice_1Choice1Nothing, goodbye
  • Graph is asset filename (guaranteed unique); Node is coding name; Field is Text, Speaker, Prompt, or Choice; Index is the zero-based choice number.

Import rules

  • Rows match by Graph + Node + Field (+ Index). Renamed/deleted nodes are skipped and the dialog reports the count.
  • A non-empty en (default) cell overwrites the English source.
  • Each language column overwrites that translation, including empty cells, which clear it.
  • Unknown headers are treated as new language codes and auto-added (e.g. pt-BR).
  • Column order does not matter (matched by header). Extra helper columns like Notes are ignored.

09 · UI SETUP WIZARD

One-click dialogue prefab

Window → Dialogue System → Create Dialogue UI builds a complete, styled dialogue Canvas in the active scene, saves it as a prefab (plus a ChoiceButton sub-prefab), and wires every DialogueUI field for you.

Options

Save path, panel height (% of screen), include portrait frame, and Animator Controller slot.

Colors

Panel background, nameplate, body text, speaker text, choice button (normal), and choice button text.

Generated hierarchy

DialogueCanvas (Canvas, CanvasScaler, GraphicRaycaster, AudioSource, DialogueUI)
└─ DialoguePanel
   ├─ Nameplate → SpeakerNameText
   ├─ PortraitFrame → PortraitImage (if enabled)
   ├─ DialogueBodyText
   ├─ ContinueIndicator (▼)
   └─ ChoicesContainer
      └─ ChoiceButton instances created at runtime

10 · C# EXAMPLES

Dialogue API patterns

Examples show API usage. Add the referenced scene objects and project-specific handlers.

Start a dialogue from script · NpcInteractable.cs

using EasyProDialogues;
using UnityEngine;

public class NpcInteractable : MonoBehaviour
{
    [SerializeField] DialogueUI dialogueUI;
    [SerializeField] DialogueGraph graph;

    public void OnPlayerInteract()
    {
        if (dialogueUI.IsRunning) return; // already talking
        dialogueUI.StartDialogue(graph);
    }
}

Dialogue, global and named node events · DialogueEventsExample.cs

using EasyProDialogues;
using UnityEngine;

public sealed class DialogueEventsExample : MonoBehaviour
{
    [SerializeField] private DialogueUI dialogueUI;

    private void OnEnable()
    {
        if (dialogueUI == null) return;

        // Dialogue-level: once per StartDialogue() / EndNode.
        dialogueUI.onDialogueStarted.AddListener(OnDialogueStarted);
        dialogueUI.onDialogueEnded.AddListener(OnDialogueEnded);

        // Global: fires for every node, passing its coding name.
        dialogueUI.onNodeStarted.AddListener(OnNodeStarted);
        dialogueUI.onNodeEnded.AddListener(OnNodeEnded);

        // Named: fires for one node only.
        var quest = dialogueUI.Node("Quest_Accepted");
        quest.onStarted.AddListener(OnQuestStarted);
        quest.onEnded.AddListener(OnQuestEnded);
    }

    private void OnDisable()
    {
        if (dialogueUI == null) return;
        dialogueUI.onDialogueStarted.RemoveListener(OnDialogueStarted);
        dialogueUI.onDialogueEnded.RemoveListener(OnDialogueEnded);
        dialogueUI.onNodeStarted.RemoveListener(OnNodeStarted);
        dialogueUI.onNodeEnded.RemoveListener(OnNodeEnded);

        var quest = dialogueUI.Node("Quest_Accepted");
        quest.onStarted.RemoveListener(OnQuestStarted);
        quest.onEnded.RemoveListener(OnQuestEnded);
    }

    private void OnDialogueStarted() => Debug.Log("Started");
    private void OnDialogueEnded() => Debug.Log("Ended");
    private void OnNodeStarted(string nodeName) => Debug.Log($"Entered {nodeName}");
    private void OnNodeEnded(string nodeName) => Debug.Log($"Finished {nodeName}");
    private void OnQuestStarted() => Debug.Log("Quest accepted");
    private void OnQuestEnded() => Debug.Log("Quest node ended");
}

Use named methods instead of lambdas if you need RemoveListener in OnDisable.

Event node handlers · ShopkeeperEvents.cs

using EasyProDialogues;
using UnityEngine;

public sealed class ShopkeeperEvents : MonoBehaviour
{
    [SerializeField] private DialogueUI dialogueUI;

    private void Awake()
    {
        if (dialogueUI == null) return;

        // Fire-and-forget (waitForCompletion = false): done is null.
        dialogueUI.RegisterEventHandler("ShakeCamera", (eventNode, done) =>
        {
            Debug.Log($"Shake camera: {eventNode.eventArg}");
        });

        // Wait-for-completion (waitForCompletion = true): call done when finished.
        dialogueUI.RegisterEventHandler("OpenShop", (eventNode, done) =>
        {
            Debug.Log($"Open shop: {eventNode.eventArg}");
            done?.Invoke(); // Replace with the shop's on-closed callback.
        });
    }

    private void OnDestroy()
    {
        if (dialogueUI == null) return;
        dialogueUI.UnregisterEventHandler("ShakeCamera");
        dialogueUI.UnregisterEventHandler("OpenShop");
    }
}

Variables and saving globals · DialogueSaveSystem.cs

using System.Collections.Generic;
using EasyProDialogues;
using UnityEngine;

public static class DialogueSaveSystem
{
    [System.Serializable]
    private sealed class SaveData
    {
        public List<string> keys = new List<string>();
        public List<string> values = new List<string>();
    }

    public static void Save(DialogueUI dialogueUI)
    {
        dialogueUI.Variables.Set("gold", "150");
        dialogueUI.Variables.Increment("gold", 50);
        DialogueVariables.SetGlobal("quest_blacksmith_done", "true");

        var data = new SaveData();
        foreach (var pair in DialogueVariables.SaveGlobals())
        {
            data.keys.Add(pair.Key);
            data.values.Add(pair.Value);
        }
        PlayerPrefs.SetString("dialogue_globals", JsonUtility.ToJson(data));
        PlayerPrefs.Save();
    }

    public static void Load()
    {
        if (!PlayerPrefs.HasKey("dialogue_globals")) return;
        var data = JsonUtility.FromJson<SaveData>(PlayerPrefs.GetString("dialogue_globals"));
        if (data == null || data.keys == null || data.values == null) return;
        var globals = new Dictionary<string, string>();
        int count = System.Math.Min(data.keys.Count, data.values.Count);
        for (int i = 0; i < count; i++) globals[data.keys[i]] = data.values[i];
        DialogueVariables.LoadGlobals(globals);
    }
}

Language switcher · LanguageDropdown.cs

using EasyProDialogues;
using UnityEngine;
using UnityEngine.UI;

public sealed class LanguageDropdown : MonoBehaviour
{
    [SerializeField] private Dropdown dropdown;
    [SerializeField] private DialogueLocalizationLoader loader;
    private readonly string[] codes = { "en", "fr", "de" }; // Keep this order in the Dropdown options.

    private void Start()
    {
        if (dropdown == null || loader == null) return;
        int selectedIndex = System.Array.IndexOf(codes, LocalizationSystem.CurrentLocale);
        if (selectedIndex >= 0) dropdown.SetValueWithoutNotify(selectedIndex);
        dropdown.onValueChanged.AddListener(OnLocaleSelected);
    }

    private void OnDestroy()
    {
        if (dropdown != null) dropdown.onValueChanged.RemoveListener(OnLocaleSelected);
    }

    private void OnLocaleSelected(int index)
    {
        if (index >= 0 && index < codes.Length) loader.SetLocale(codes[index]);
    }
}

Shared runner across several UIs · GameBootstrap.cs

Useful for split-screen co-op, or separate “NPC talk” and “narrator caption” UIs that share quest flags and handlers.

using EasyProDialogues;
using UnityEngine;

public sealed class GameBootstrap : MonoBehaviour
{
    [SerializeField] private DialogueUI player1UI;
    [SerializeField] private DialogueUI player2UI;
    [SerializeField] private DialogueRunner sharedRunner;

    private void Awake()
    {
        if (sharedRunner == null) return;
        if (player1UI != null) player1UI.sharedRunner = sharedRunner;
        if (player2UI != null) player2UI.sharedRunner = sharedRunner;

        // Registered once, available to UIs using this runner.
        sharedRunner.RegisterEventHandler("GlobalAnnounce", (eventNode, done) =>
            Debug.Log($"Announcement: {eventNode.eventArg}"));
    }
}

Full example: shopkeeper mini-scene · Shopkeeper.cs

using EasyProDialogues;
using UnityEngine;

public sealed class Shopkeeper : MonoBehaviour
{
    [SerializeField] private DialogueUI dialogueUI;
    [SerializeField] private DialogueGraph shopGraph;

    private void Awake()
    {
        if (dialogueUI == null) return;
        dialogueUI.RegisterEventHandler("BuyPotion", (eventNode, done) =>
        {
            dialogueUI.Variables.Increment("gold", -10);
            Debug.Log("Potion purchased"); // Connect your inventory system here.
        });
    }

    public void Talk()
    {
        if (dialogueUI == null || shopGraph == null) return;
        dialogueUI.Variables.Set("gold", "150");
        dialogueUI.StartDialogue(shopGraph);
    }
}

Shopkeeper graph flow

Say: "Welcome!"
  └─ Condition: gold ≥ 10
      ├─ True → Choice: "Buy potion?"
      │           ├─ Yes → Event: BuyPotion → Say: "Enjoy!" → End
      │           └─ No  → Say: "Come back" → End
      └─ False → Say: "Come back" → End

Editor scripts · BulkUpdateChoiceLayout.cs

#if UNITY_EDITOR
using UnityEditor;
using EasyProDialogues;

public static class BulkUpdateChoiceLayout
{
    [MenuItem("Tools/Dialogue System/Set All Choices To Top+2px")]
    public static void Run()
    {
        foreach (var guid in AssetDatabase.FindAssets("t:DialogueGraph"))
        {
            var path = AssetDatabase.GUIDToAssetPath(guid);
            var graph = AssetDatabase.LoadAssetAtPath<DialogueGraph>(path);
            if (graph == null) continue;

            foreach (var node in graph.nodes)
            {
                ChoiceNode choice = node as ChoiceNode;
                if (choice == null) continue;
                choice.buttonLayout = ButtonLayoutPosition.Top;
                choice.buttonSpacing = 2f;
                EditorUtility.SetDirty(graph);
            }
        }
        AssetDatabase.SaveAssets();
    }
}
#endif

Excel from a build pipeline / CI (Editor)

using UnityEngine;

public static class DialogueExcelPipeline
{
#if UNITY_EDITOR
    public static void ExportAndImport()
    {
        const string path = "Translations/AllText.xlsx";
        DialogueLocalizationEditorUtility.ExportAllGraphsToExcel(path);
        var (matched, skipped) = DialogueLocalizationEditorUtility.ImportExcelIntoGraphs(path);
        Debug.Log($"Imported {matched}; skipped {skipped}");
    }
#endif
}

Build a DialogueTrigger from code · NpcSpawner.cs

using EasyProDialogues;
using UnityEngine;

public static class NpcSpawner
{
    public static GameObject Create(DialogueUI dialogueUI, DialogueGraph greetingGraph)
    {
        var npc = new GameObject("NPC");
        var collider = npc.AddComponent<SphereCollider>();
        collider.isTrigger = true;
        collider.radius = 2f;

        var trigger = npc.AddComponent<DialogueTrigger>();
        trigger.dialogueUI = dialogueUI;
        trigger.graph = greetingGraph;
        trigger.triggerOnTrigger = true;
        trigger.interactionTag = "Player";
        return npc;
    }
}

11 · TIPS & GOTCHAS

Things that bite