Six steps to build and play your first dialogue graph.
Create a graphOpen Window → Dialogue System → Graph Editor, then click Create. A new graph opens with one Say node.
Build the flowRight-click or press Ctrl+Space to add nodes. Drag from an output port to an input port.
Write the textClick a Say node and type the Speaker and Text straight into its body.
SaveClick Save in the toolbar. There is no autosave and no Ctrl+S binding.
Build a UIOpen Window → Dialogue System → Create Dialogue UI to generate a fully wired prefab.
Play itAdd a DialogueTrigger, set its UI and graph, then call Interact() or enable triggerOnStart.
What's in the box
File
Role
DialogueGraph.cs
Graph asset (ScriptableObject) and node connections
DialogueNode.cs
Say, Choice, Event, Condition, End, Timer, and Random nodes
DialogueChoice.cs
Choice options, conditions, and session variables
DialogueLocalization.cs
Runtime locale tracking and TranslationSet
DialogueRunner.cs
Optional shared settings, variables, and event hub
DialogueUI.cs
Runtime player that drives a graph on screen
DialogueUIAutoSetup.cs
Editor 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.
Method
What it does
GetNode(guid)
Look up a node by GUID.
Node(name) / TryGetNode(...)
Look up a node by its coding name.
StartNode
The 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
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 field
Purpose
dialoguePanel
GameObject shown/hidden while dialogue runs.
speakerNameText, dialogueBodyText
TMP_Text references.
portraitImage, nameplateBackground
Portrait sprite; nameplate hidden when no speaker is set.
choicesContainer, choiceButtonPrefab
Parent for runtime-spawned buttons; prefab = Button + child TMP_Text.
charsPerSecond, typingSound
Typewriter speed and optional per-character sound.
pauseGameDuringDialogue
Time.timeScale = 0 while running (shared runner's setting wins).
skipToEndOnInput
Space/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
Event
Fires when
Typical use
onDialogueStarted
StartDialogue() called, before the graph begins
Freeze player, open dialogue camera
onDialogueEnded
End node reached or StopDialogue()
Resume control, save progress
onNodeStarted(name)
Every node, on entry
Logging, analytics, drive an Animator
onNodeEnded(name)
Every node, after behavior and before next
Per-node cleanup
Node("X").onStarted / .onEnded
Only for node X
Give 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 field
Purpose
dialogueUI
Target UI (auto-found via FindAnyObjectByType in Awake if empty).
graph
The DialogueGraph to play.
triggerOnStart
Fire automatically in Start().
triggerOnTrigger · interactionTag
Fire on OnTriggerEnter (needs trigger Collider); tag defaults to “Player”.
startConditions · conditionMode
Gate 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.
Toolbar
Action
Save
Writes node positions and connections into the asset, then AssetDatabase.SaveAssets().
Validate
Runs DialogueGraph.Validate() and shows the result.
Language (globe)
Language picker, plus Excel export/import.
Auto Layout
Tidy columns via breadth-first walk from the start node; unreachable nodes stay put.
Center
Frames the whole graph (FrameAll).
Undo / Redo · Delete
Undo/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
Node
English field
Translation field
SayNode
text
textTranslations
SayNode
speakerName
speakerTranslations
ChoiceNode
promptText
promptTranslations
ChoiceNode
speakerName
speakerTranslations
DialogueChoice
text
textTranslations
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)
Entry
What it does
Edit in / language
Repopulates 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 / language
Hides it from the picker. Stored translations are kept until you re-add the code.
Export / Import Excel
See 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
ExportLanguage menu → Export All Dialogues to Excel…
TranslateTranslator fills the empty language cells.
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.
Graph
Node
Field
Index
en (default)
fr
de
Chapter1
Intro_Say
Text
—
Hello, traveler!
Chapter1
Intro_Say
Speaker
—
Old Man
Chapter1
Choice_1
Prompt
—
What do you want?
Chapter1
Choice_1
Choice
0
Tell me about the ruins
Chapter1
Choice_1
Choice
1
Nothing, 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.
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);
}
}