Building a Custom Story Graph Editor in Unity (Visual Narrative System)

Real‑Time Volumetric Cloud Rendering in Unity Using Raymarching
August 29, 2026
Creating a Dynamic Dialogue Engine
Creating a Dynamic Dialogue Engine with AI‑Driven Responses in Unity
September 1, 2026
Real‑Time Volumetric Cloud Rendering in Unity Using Raymarching
August 29, 2026
Creating a Dynamic Dialogue Engine
Creating a Dynamic Dialogue Engine with AI‑Driven Responses in Unity
September 1, 2026

Building a Custom Story Graph Editor in Unity (Visual Narrative System)

A Story Graph Editor is a visual tool used to create branching dialogue, quests, narrative logic, and complex story flows.
Games such as Detroit: Become Human, Life is Strange, and Disco Elysium rely heavily on story graphs to structure narrative progression.

In this guide, we will create a custom graph-based narrative editor inside Unity using:

  • ScriptableObjects to store conversation/quest data
  • A custom editor window for graph visualization
  • Nodes and ports (inputs/outputs)
  • Connectable edges
  • Story evaluation at runtime

1. What is a Story Graph?

A story graph is a node-based system where each node represents a narrative event:

  • Dialogue line
  • Choice
  • Condition check
  • Quest update
  • Cutscene trigger

Edges between nodes control story progression.

Start → Dialogue → Player Choice → Branch → Merge → End

2. Story Node Data (ScriptableObject)

Each story node needs:

  • ID
  • Node type (Dialogue, Choice, Event, Condition, etc.)
  • Text
  • Outputs (links to other nodes)

Here is a minimal ScriptableObject node:

using UnityEngine;
using System.Collections.Generic;

[CreateAssetMenu(menuName="Story/Node")]
public class StoryNode : ScriptableObject
{
    public string nodeID;

    [TextArea]
    public string text;

    public Vector2 editorPosition;

    public List<StoryOutput> outputs = new List<StoryOutput>();
}

[System.Serializable]
public class StoryOutput
{
    public string label;  
    public StoryNode next;
}

3. Story Graph Container

The entire story is stored inside a single StoryGraph asset:

[CreateAssetMenu(menuName="Story/Graph")]
public class StoryGraph : ScriptableObject
{
    public StoryNode startNode;
    public List<StoryNode> nodes = new List<StoryNode>();
}

4. Creating an Editor Window

We now create a custom window for editing the graph visually.

using UnityEditor;
using UnityEngine;

public class StoryGraphEditor : EditorWindow
{
    StoryGraph graph;
    Vector2 drag;
    Vector2 offset;

    [MenuItem("Tools/Story Graph Editor")]
    public static void OpenWindow()
    {
        StoryGraphEditor window = GetWindow<StoryGraphEditor>();
        window.titleContent = new GUIContent("Story Graph");
    }

    void OnGUI()
    {
        if (graph == null)
        {
            GUILayout.Label("No graph selected");
        }
        else
        {
            DrawGrid(20, 0.2f, Color.gray);
            DrawGrid(100, 0.4f, Color.gray);
            DrawNodes();
            DrawConnections();
            DrawToolbar();
            ProcessEvents(Event.current);
        }

        if (GUI.changed) Repaint();
    }
}

5. Drawing a Grid Background

Graph windows typically include a panning grid background.

void DrawGrid(float gridSpacing, float gridOpacity, Color gridColor)
{
    int width = Mathf.CeilToInt(position.width / gridSpacing);
    int height = Mathf.CeilToInt(position.height / gridSpacing);

    Handles.BeginGUI();
    Handles.color = new Color(gridColor.r, gridColor.g, gridColor.b, gridOpacity);

    offset += drag * 0.5f;

    for (int i = 0; i < width; i++)
    {
        Handles.DrawLine(new Vector3(gridSpacing * i, 0), new Vector3(gridSpacing * i, position.height));
    }
    for (int j = 0; j < height; j++)
    {
        Handles.DrawLine(new Vector3(0, gridSpacing * j), new Vector3(position.width, gridSpacing * j));
    }

    Handles.color = Color.white;
    Handles.EndGUI();
}

6. Node Rendering (Draggable Boxes)

Each node is drawn as a draggable box:

void DrawNodes()
{
    foreach (var node in graph.nodes)
    {
        GUI.Box(new Rect(node.editorPosition, new Vector2(200, 80)), node.text);

        // Make node draggable
        if (Event.current.type == EventType.MouseDrag &&
            new Rect(node.editorPosition, new Vector2(200, 80)).Contains(Event.current.mousePosition))
        {
            node.editorPosition += Event.current.delta;
            GUI.changed = true;
        }
    }
}

7. Drawing Connections Between Nodes

Connections are Bezier curves:

void DrawConnections()
{
    foreach (var node in graph.nodes)
    {
        foreach (var output in node.outputs)
        {
            if (output.next != null)
            {
                Vector3 start = node.editorPosition + new Vector2(200, 40);
                Vector3 end = output.next.editorPosition + new Vector2(0, 40);

                Handles.DrawBezier(
                    start,
                    end,
                    start + Vector3.right * 50,
                    end + Vector3.left * 50,
                    Color.white,
                    null,
                    3
                );
            }
        }
    }
}

8. Creating & Linking Nodes

Add a toolbar to create/remove nodes:

void DrawToolbar()
{
    GUILayout.BeginArea(new Rect(10, 10, 200, 200));
    if (GUILayout.Button("Add Node"))
    {
        CreateNode();
    }
    GUILayout.EndArea();
}

void CreateNode()
{
    StoryNode node = ScriptableObject.CreateInstance<StoryNode>();
    node.nodeID = System.Guid.NewGuid().ToString();
    node.editorPosition = Vector2.zero;

    graph.nodes.Add(node);
    AssetDatabase.AddObjectToAsset(node, graph);
    AssetDatabase.SaveAssets();
}

Linking is done by selecting an output and clicking another node.
Implementation is simple:

StoryNode pendingLink;

void ProcessEvents(Event e)
{
    if (e.type == EventType.MouseDown && e.button == 0)
    {
        foreach (var node in graph.nodes)
        {
            Rect r = new Rect(node.editorPosition, new Vector2(200, 80));
            if (r.Contains(e.mousePosition))
            {
                if (pendingLink == null)
                    pendingLink = node;
                else
                {
                    pendingLink.outputs.Add(new StoryOutput{ next = node });
                    pendingLink = null;
                }
            }
        }
    }
}

9. Evaluating the Story at Runtime

Runtime evaluation is extremely simple:

public class StoryPlayer
{
    StoryNode current;

    public StoryPlayer(StoryGraph graph)
    {
        current = graph.startNode;
    }

    public StoryNode GetCurrentNode()
    {
        return current;
    }

    public void Choose(int index)
    {
        current = current.outputs[index].next;
    }
}

This creates a fully working dialogue/quest branching system.


10. Extensions (AAA Narrative Tools)

  • Choice conditions (inventory, stats, flags)
  • Dialogue audio previews
  • Tags and quest states
  • Cutscene triggers
  • Localization support
  • Play mode simulation inside the editor
  • Minimap overview of the graph
  • Comment boxes and color coding
  • Auto-arrange graph layout

These additions turn the tool into something close to Articy Draft or Ink.


11. Summary

A custom Story Graph Editor is an essential tool for narrative-driven games.
By building it with ScriptableObjects and a custom editor window, you gain full control over:

  • Dialogue systems
  • Quest trees
  • Cinematic triggers
  • Branching choices
  • Complex narrative states

This system is extendable, designer-friendly, and integrates seamlessly with Unity’s asset workflow.

Leave a Reply

Your email address will not be published. Required fields are marked *


Skip to toolbar