Dialogue Manager
Related skills: dialogue-system for hand-rolled dialogue data structures, localization for translating lines, popochiu for full adventure-game workflows.
Addon: Dialogue Manager · version
v3.10.4· Godot 4.6 · MIT · source: https://github.com/nathanhoad/godot_dialogue_manager · GDScript with official C# support.
1. When to use Dialogue Manager
| Approach | Best for |
|---|---|
dialogue-system skill (hand-rolled Resource data) | Full control over data shape, no addon dependency, small dialogue trees |
| Dialogue Manager | Script-like .dialogue text format, branching responses, conditions/mutations, translation pipeline, visual editor tab, official C# wrapper |
| Full adventure-game framework | Point-and-click games needing rooms/inventory/actors bundled with dialogue — Dialogue Manager only owns the dialogue layer |
Choose Dialogue Manager when writers want to author branching dialogue as readable script text (not
Inspector-edited Resources) and you want built-in conditions, mutations, random lines, and a translation
workflow (CSV or PO) for free. If you need dialogue as strongly-typed Resources you fully own, use
dialogue-system instead. Dialogue Manager only handles dialogue — it is not a full adventure/quest
framework, so a larger point-and-click framework would still own rooms, inventory, and actors around it.
2. Install & setup
- Godot AssetLib → search "Dialogue Manager" → Download. Or copy
addons/dialogue_manager/from the GitHub repo intores://addons/dialogue_manager/. - Project → Project Settings → Plugins → enable "Dialogue Manager". This adds a Dialogue tab to
the bottom editor panel and registers the
DialogueManagerautoload automatically — no manual autoload step needed. addons/dialogue_manager/plugin.cfg:
ini[plugin] name="Dialogue Manager" description="A powerful nonlinear dialogue system" author="Nathan Hoad" version="3.10.4" script="plugin.gd"
- C# projects need no extra NuGet package —
using DialogueManagerRuntime;is enough (§4). - Settings live at Project Settings → General → Dialogue Manager: notably State Autoload
Shortcuts (autoload names usable in dialogue without a prefix) and Balloon Path (the scene
show_dialogue_balloon()opens — leave empty to use the built-in example balloon).
3. .dialogue syntax
Open/create a .dialogue file from the Dialogue editor tab. Lines are Character: text or bare
text (narrator). Godot's RichTextLabel BBCode works, plus extras: [[A|B|C]] (random inline pick),
[wait=N] / [wait="ui_accept"] (pause typing), [speed=N], [next=auto] (auto-advance).
Titles and jumps — ~ name marks a title; => name jumps to it; => END ends the flow; => END!
force-ends past any pending jump-and-returns; =>< name jumps and returns here once that branch hits END:
~ start Nathan: Well? - First one - Another one => another_title - Start again => start => END ~ another_title Nathan: Another one? => END
Responses (- ) nest by indentation and can carry a condition in [...] — put any jump last:
Nathan: How many projects have you started and not finished? - Just a couple Nathan: That's not so bad. - A lot [if SomeGlobal.some_property == true] Nathan: Maybe you should finish one before another. - Another one [if SomeGlobal.some_method()] => another_title
Conditions — if / elif / else, boolean and/or/(), and match/while blocks:
if SomeGlobal.some_property >= 10 Nathan: That property is >= 10. elif SomeGlobal.some_other_property == "some value" Nathan: Or we might be in here. else Nathan: If neither are true, I'll say this.
Inline conditions: Nathan: I have done this [if already_done]once again[/if], with an optional
[else]. Null-safe member access uses ?.: if some_node_reference?.name == "SomeNode".
Mutations — set assigns state, do calls a method or emits a signal; both can run inline
([do wave()], suppress the implicit await with [do! wave()]):
if SomeGlobal.has_met_nathan == false do SomeGlobal.animate("Nathan", "Wave") Nathan: Hi, I'm Nathan. set SomeGlobal.has_met_nathan = true
Built-in mutations: do wait(float), do debug(...). Emit a signal from dialogue with
do SomeGlobal.some_signal.emit("arg").
Locals vs extra game states — set locals.asked = true creates a per-conversation temp variable
(a convention implemented by the example balloon, not a Dialogue Manager core feature). Objects passed
in the extra_game_states array are referenced directly by name and their mutations persist after the
conversation ends — pass instances, not classes (GameStateClass.new(), never the bare class).
Randomised lines — prefix with % (equal weight) or %N (relative weight); a blank line separates
random groups:
Nathan: I will say this. %3 Nathan: This line has a 60% chance of being picked %2 Nathan: This line has a 40% chance of being picked
Variables and tags — {{SomeGlobal.some_property}} interpolates state into text (also usable as a
character name). [#happy, #mood=calm] attaches tags, readable via line.get_tag_value("mood").
4. Runtime API & example balloon
Load a DialogueResource (.dialogue file) and either let DialogueManager open a balloon for you, or
pull lines manually with await DialogueManager.get_next_dialogue_line(resource, title). When there is
no next line it returns null — check falsy in GDScript (if not line: / while line:) and
line != null in C#. (The tag's API.md prose says "empty dictionary {}", but the v3.10.4 source
returns null on every end-of-dialogue path — a line == {} check would never fire.)
GDScript
gdscript# npc.gd extends Node2D @export var dialogue_resource: DialogueResource func _on_interact() -> void: # Opens the configured balloon (Settings → Balloon Path), or the built-in example balloon. DialogueManager.show_dialogue_balloon(dialogue_resource, "start") func _manual_walk() -> void: # Manual traversal — build a totally custom balloon around this loop. var line: DialogueLine = await DialogueManager.get_next_dialogue_line(dialogue_resource, "start") while line: print("%s: %s" % [line.character, line.text]) if line.responses.is_empty(): line = await DialogueManager.get_next_dialogue_line(dialogue_resource, line.next_id) else: var chosen: DialogueResponse = line.responses[0] # replace with real UI selection line = await DialogueManager.get_next_dialogue_line(dialogue_resource, chosen.next_id)
C#
csharp// Npc.cs using Godot; using DialogueManagerRuntime; public partial class Npc : Node2D { [Export] public Resource DialogueResource; private void OnInteract() { DialogueManager.ShowDialogueBalloon(DialogueResource, "start"); } private async void ManualWalk() { var line = await DialogueManager.GetNextDialogueLine(DialogueResource, "start"); while (line != null) { GD.Print($"{line.Character}: {line.Text}"); if (line.Responses.Count == 0) { line = await DialogueManager.GetNextDialogueLine(DialogueResource, line.NextId); } else { var chosen = line.Responses[0]; // replace with real UI selection line = await DialogueManager.GetNextDialogueLine(DialogueResource, chosen.NextId); } } } }
Other DialogueManager methods: show_dialogue_balloon_scene(balloon_scene, resource, title) (open a
specific balloon scene), show_example_dialogue_balloon(resource, title) (force the built-in balloon),
create_resource_from_text(text) (compile a .dialogue string at runtime — fails on syntax errors). C#
names are identical PascalCase: ShowDialogueBalloonScene, ShowExampleDialogueBalloon,
CreateResourceFromText.
get_next_dialogue_line takes a mutation_behaviour param — GDScript DMConstants.MutationBehaviour,
C# MutationBehaviour (both: Wait default, DoNotWait, Skip). The enum lives on DMConstants, not
on the DialogueManager autoload. Leave it Wait unless you know otherwise; the example balloon only
supports Wait.
5. Signals & typed access
DialogueLine fields: id, next_id, character, text, tags (PackedStringArray),
translation_key, responses (Array[DialogueResponse]), concurrent_lines. DialogueResponse adds
is_allowed: bool and condition_as_text: String on top of the same id/next_id/character/text/
tags/translation_key fields.
GDScript
gdscriptfunc _ready() -> void: DialogueManager.dialogue_started.connect(_on_dialogue_started) DialogueManager.dialogue_ended.connect(_on_dialogue_ended) DialogueManager.got_dialogue.connect(_on_got_dialogue) DialogueManager.mutated.connect(_on_mutated) func _on_dialogue_started(resource: DialogueResource) -> void: pass func _on_dialogue_ended(resource: DialogueResource) -> void: pass func _on_got_dialogue(line: DialogueLine) -> void: print(line.character, ": ", line.text, " tags=", line.tags) func _on_mutated(mutation: Dictionary) -> void: pass # fires before a `do`/inline mutation runs (not `set` lines)
C#
csharpusing DialogueManagerRuntime; public override void _Ready() { DialogueManager.DialogueStarted += (Resource resource) => { }; DialogueManager.DialogueEnded += (Resource resource) => { }; DialogueManager.GotDialogue += (DialogueLine line) => { GD.Print($"{line.Character}: {line.Text} tags={line.Tags}"); }; DialogueManager.Mutated += (Godot.Collections.Dictionary mutation) => { }; }
⚠️ These C# events are bridged to the underlying Godot signals lazily, on the first access to
DialogueManager.Instance. A project that only subscribes here and then drives dialogue from GDScript never touchesInstance, so the handlers never fire. Touch the API from C# at least once (e.g._ = DialogueManager.Instance;, or run the dialogue viaGetNextDialogueLine/ShowDialogueBalloonfrom C#) to wire them up.
The built-in responses menu node only exposes response_selected as a Godot signal, so connect it with
Connect + Callable instead of a C# event handler:
csharpresponsesMenu.Connect("response_selected", Callable.From((DialogueResponse response) => { // advance using response.NextId }));
passed_title(title) (GDScript) / DialogueManager.PassedTitle += (string title) => { } (C#) fires
every time a ~ title marker is crossed — useful for analytics or save-point bookmarking.
6. Translations
By default all dialogue/response text is run through Godot's tr(). The DialogueManager.translation_source
property picks the backend — its enum type is GDScript DMConstants.TranslationSource / C#
TranslationSource (None, CSV, PO, Guess default), not DialogueManager.TranslationSource.
Guess inspects your locale project settings for a PO file and falls back to CSV.
Static per-line IDs (Nathan: Hi! I'm Nathan. [ID:HI_IM_NATHAN]) give a stable translation_key for
matching voiced lines and CSV/PO round-trips, instead of keying off the literal text. .dialogue files
auto-register in the POT Generation list; a ## comment line before a dialogue line becomes a
#. TRANSLATORS: note in the exported PO/POT. Export/import CSV from the editor's Translations menu
(Dialogue tab); re-import matches by static ID when present, otherwise by literal text.
See the localization skill for TranslationServer, locale switching, and RTL — Dialogue Manager
only produces the translation keys and CSV/PO export; wiring locale changes into the running game is
localization's job.
Implementation checklist
- Dialogue Manager plugin enabled in Project Settings → Plugins (registers the
DialogueManagerautoload automatically) -
.dialogueresources loaded viaload()/preload(), not parsed by hand - Every
get_next_dialogue_line/GetNextDialogueLinecall isawaited - Manual traversal loops detect dialogue end via the
nullreturn (if not line:in GDScript,line != nullin C#) — never compare against{} - Response lines put
[if condition]before any=> targetjump, never after -
extra_game_statesentries are instances (GameStateClass.new()), not bare classes - C# state properties that dialogue reads/writes carry
[Export] - C# code uses
using DialogueManagerRuntime;and PascalCase members (ShowDialogueBalloon,GetNextDialogueLine,DialogueStarted,GotDialogue) - Custom balloons override Balloon Path in Settings rather than forking
show_dialogue_ballooncall sites - Static translation IDs (
[ID:KEY]) added to any line needing voice-over or stable CSV/PO keys - Inline
do/awaitmutations usedo!when the caller shouldn't block on them


