Back to editor

Dialogue Forge

A visual editor for building branching dialogue trees for games and interactive fiction. Design conversations with a node graph, then export structured JSON your game engine can traverse at runtime.

The Interface

Sidebar

Drag node types onto the canvas or click a template to load a starter project.

Canvas

Your graph lives here. Pan with Space+drag, zoom with the scroll wheel.

Inspector

Click any node to open its detail panel on the right — edit name, dialogue, emotion, and custom attributes.

Validation bar

Runs continuously. Surfaces errors (disconnected nodes, empty dialogue) and warnings at the bottom of the screen.

Node Types

Every node is either a Character node or an Action node.

Character node

Represents a speaker delivering a line of dialogue. Fill in the character name, dialogue text, and optional emotion. Add custom attributes (numbers, dropdowns, flags) to track game state — e.g. Courage: 7 or Faction: Rebel.

Action node types

Branch

Presents outgoing edges as labelled player choices. Each edge label (double-click to edit) becomes a selectable option in your game UI.

Trigger

Fires a game event and auto-advances. Use it to unlock quests, set flags, or award items. Attach a Flag Name attribute to name the event.

Jump

Redirects the flow to another part of the graph — useful for looping back or skipping ahead without drawing a long edge.

End

Terminates the conversation. Connect the final node in every path to an End node so your runtime knows when to close the dialogue window.

Building a Graph

  1. 1Drag a Character or Action node from the sidebar onto the canvas, or click "Add Node" at the bottom of the sidebar.
  2. 2Click a node to open its inspector. Edit name, dialogue, and any attributes.
  3. 3Connect nodes by hovering over a node's right handle until the cursor changes, then drag to another node's left handle.
  4. 4For Branch nodes, double-click each outgoing edge label to type the player choice text.
  5. 5Every conversation path must end at an End action node. The validation bar will flag open paths.

Selection & Multi-Select

Click — select a single node and open it in the inspector.

Shift + Click — add a node or edge to the current selection.

Shift + drag on the canvas — draw a selection rectangle.

CtrlC — copy all selected nodes and the edges that connect them to each other.

CtrlV — paste the copied subgraph offset by 40 px. All internal edges are recreated with fresh IDs; connections to nodes outside the selection are not copied.

Del — delete all selected nodes and/or selected edges in one action.

Click an edge to select it, then press Del to remove just that edge without touching its nodes.

Keyboard Shortcuts

Undo
CtrlZ
Redo
CtrlY
Duplicate selected node
CtrlD
Copy selected nodes (preserves edges)
CtrlC
Paste copied subgraph
CtrlV
Search nodes
CtrlF
Auto layout
CtrlL
Save / export JSON
CtrlS
Delete selected node(s) or edge(s)
Del
Deselect / close panels
Esc
Pan the canvas
Space+ drag
Multi-select (click or drag)
Shift

Saving & Loading

Save / Export

Clicking Save (or pressing CtrlS) downloads a .forge.json file. This is your project file — keep it somewhere safe.

Import

Click the Import button and select a previously saved .forge.json(or any compatible JSON). If the canvas has content you'll be asked to confirm before replacing it.

Preview

The Preview button runs the dialogue in a lightweight simulator so you can walk through every branch before exporting.

The Exported JSON

The export is a self-contained JSON object. Here's the top-level shape:

{
  "version": 1,
  "name": "My Dialogue",
  "exportedAt": "2025-01-01T00:00:00.000Z",
  "nodes": [ ...node objects... ],
  "edges": [ ...edge objects... ]
}

A character node:

{
  "id": "char-abc123",
  "type": "character",
  "position": { "x": 0, "y": 0 },        // editor layout only — ignore at runtime
  "data": {
    "name": "Guard",
    "dialogue": "Halt! Who goes there?",
    "emotion": "angry",                   // free-form string
    "portrait": "",                       // URL or asset path
    "attributeSchema": [
      { "id": "attr-hp", "name": "HP", "type": "number", "defaultValue": 100 }
    ],
    "attributes": { "attr-hp": 100 }      // current values, keyed by schema id
  }
}

An action node:

{
  "id": "act-def456",
  "type": "action",
  "position": { "x": 240, "y": 0 },
  "data": {
    "actionType": "branch",   // "branch" | "trigger" | "jump" | "end"
    "label": "Player Choice",
    "attributeSchema": [],
    "attributes": {}
  }
}

An edge:

{
  "id": "edge-ghi789",
  "source": "act-def456",      // id of the node this edge exits
  "target": "char-jkl012",     // id of the node this edge enters
  "type": "dialogue",
  "data": {
    "optionText": "I'm a traveler.",   // player-facing choice label (branch edges)
    "conditions": {},                  // reserved for runtime conditions
    "metadata": {}                     // reserved for custom data
  }
}

Using the JSON in Your Game

The graph is a directed acyclic graph (DAG). At runtime, load the JSON, find the entry node, and walk edges until you hit an end action. Below is a complete, copy-pasteable TypeScript runtime.

Step 1 — TypeScript types

interface NodeData {
  name?: string;
  dialogue?: string;
  emotion?: string;
  portrait?: string;
  actionType?: "branch" | "trigger" | "jump" | "end";
  label?: string;
  attributes?: Record<string, unknown>;
}

interface DialogueNode {
  id: string;
  type: "character" | "action";
  data: NodeData;
}

interface DialogueEdge {
  id: string;
  source: string;
  target: string;
  data: { optionText: string; conditions: object; metadata: object };
}

interface DialogueGraph {
  version: number;
  name: string;
  nodes: DialogueNode[];
  edges: DialogueEdge[];
}

Step 2 — Build lookup maps

import graphJson from "./my-dialogue.forge.json";

const graph = graphJson as DialogueGraph;

// O(1) lookups by id
const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));

// Outgoing edges grouped by source node
const edgesFrom = new Map<string, DialogueEdge[]>();
for (const edge of graph.edges) {
  if (!edgesFrom.has(edge.source)) edgesFrom.set(edge.source, []);
  edgesFrom.get(edge.source)!.push(edge);
}

Step 3 — Find the entry node

function findStartNode(graph: DialogueGraph): DialogueNode {
  const hasIncoming = new Set(graph.edges.map((e) => e.target));
  // Root nodes have no incoming edges
  return graph.nodes.find((n) => !hasIncoming.has(n.id)) ?? graph.nodes[0];
}

Step 4 — Dialogue runner

class DialogueRunner {
  private currentId: string;

  constructor(startNode: DialogueNode) {
    this.currentId = startNode.id;
  }

  get current(): DialogueNode        { return nodeById.get(this.currentId)!; }
  get choices(): DialogueEdge[]      { return edgesFrom.get(this.currentId) ?? []; }
  get isEnded(): boolean {
    const n = this.current;
    return n.type === "action" && n.data.actionType === "end";
  }

  /** Move to the next node. Pass a choiceIndex for branch nodes. */
  advance(choiceIndex = 0): boolean {
    if (this.isEnded) return false;
    const edge = this.choices[choiceIndex];
    if (!edge) return false;
    this.currentId = edge.target;
    return true;
  }
}

// ── Example traversal loop ─────────────────────────────────────
const runner = new DialogueRunner(findStartNode(graph));

while (!runner.isEnded) {
  const node = runner.current;

  if (node.type === "character") {
    console.log(`${node.data.name}: ${node.data.dialogue}`);
    runner.advance();                    // auto-advance (single outgoing edge)

  } else if (node.data.actionType === "branch") {
    runner.choices.forEach((e, i) =>
      console.log(`  [${i}] ${e.data.optionText}`)
    );
    const playerChoice = 0;             // ← replace with real player input
    runner.advance(playerChoice);

  } else if (node.data.actionType === "trigger") {
    fireGameEvent(node.data.label!);    // ← your event system here
    runner.advance();

  } else if (node.data.actionType === "jump") {
    runner.advance();                    // follows the single outgoing edge
  }
}

Other engines

Unity (C#) — use Newtonsoft.Json or System.Text.Json to deserialize into equivalent C# classes. The traversal logic maps 1-to-1.

Godot (GDScript) — use JSON.parse_string()and store nodes/edges in Dictionary arrays. GDScript's dynamic typing makes traversal straightforward.

Unreal (C++/Blueprints) — deserialize with FJsonSerializer into TMap structures. Blueprint-callable functions can wrap the runner pattern.

Tips

  • The position field on each node is only for the editor canvas — safely ignore it at runtime.
  • conditions and metadata on edges are reserved for future runtime logic, e.g. only show a branch choice if the player has enough gold.
  • Character attributes mirror the attributeSchema— use the schema to know each value's type before reading it.
  • Files are versioned ("version": 1) so you can handle format migrations as the tool evolves.