Quick answer: Store all dialogue in a JSON file with nodes containing speaker, text, portrait frame, and branching choices. Load it at startup, display one node at a time in a dialogue box UI, reveal text with a typewriter effect using a timer, and let the player advance or pick choices that jump to different nodes. Separate data from display logic so you can add localization and new conversations without touching event sheets.

Dialogue systems are the backbone of story-driven games, RPGs, and visual novels. Even action games use them for quest givers and shopkeepers. Construct 3 does not ship with a dialogue plugin, but by combining JSON data, text objects, timers, and a simple state machine, you can build a flexible conversation engine that handles typewriter effects, branching paths, NPC portraits, and multiple languages.

Structuring Dialogue Data in JSON

The most important decision is how you structure your conversations. A node-based JSON format gives you maximum flexibility. Each conversation is an array of nodes. Each node has a unique ID, a speaker name, a portrait animation frame, the dialogue text, and optionally an array of player choices that branch to other nodes.

// dialogue/shopkeeper.json
{
  "id": "shopkeeper_greeting",
  "nodes": [
    {
      "id": "start",
      "speaker": "Martha",
      "portrait": 2,
      "text": "Welcome, traveler! Looking to buy something, or just browsing?",
      "choices": [
        { "text": "Show me what you have.", "next": "show_shop" },
        { "text": "Any rumors lately?", "next": "rumors" },
        { "text": "Just passing through.", "next": "goodbye" }
      ]
    },
    {
      "id": "show_shop",
      "speaker": "Martha",
      "portrait": 3,
      "text": "Take a look! Fresh stock arrived this morning.",
      "action": "open_shop"
    },
    {
      "id": "rumors",
      "speaker": "Martha",
      "portrait": 4,
      "text": "I heard strange noises from the old mine. Folks say it is haunted...",
      "next": "rumors_2"
    },
    {
      "id": "rumors_2",
      "speaker": "Martha",
      "portrait": 2,
      "text": "If you are brave enough to investigate, I will make it worth your while.",
      "action": "start_quest_mine"
    },
    {
      "id": "goodbye",
      "speaker": "Martha",
      "portrait": 2,
      "text": "Safe travels! Come back anytime."
    }
  ]
}

Load dialogue files using AJAX at the start of the game or on demand when the player enters a new area. Store them in a global dictionary keyed by conversation ID.

Building the Dialogue Box UI

Create a dedicated UI layer for the dialogue box. You need these objects: a 9-patch or sprite for the dialogue panel background, a Text object for the speaker name, a Text object for the dialogue text, a Sprite for the NPC portrait, and a container of Button sprites for choices (create them dynamically or use a pool of 3–4 pre-placed buttons that you show/hide).

// Event sheet: Show a dialogue node
// Function "ShowDialogueNode" - parameter: NodeID (string)

System: Set global variable CurrentNodeID to NodeID
System: Set global variable FullText to
  GetNodeProperty(CurrentConversation, NodeID, "text")
System: Set global variable CharIndex to 0
System: Set global variable IsTyping to 1

// Set speaker name and portrait
txtSpeakerName: Set text to
  GetNodeProperty(CurrentConversation, NodeID, "speaker")
sprPortrait: Set animation frame to
  GetNodeProperty(CurrentConversation, NodeID, "portrait")

// Hide choice buttons initially
btnChoice: Set visible to False

// Show the dialogue panel
DialoguePanel: Set visible to True
txtDialogue: Set text to ""

The Typewriter Text Effect

The typewriter effect reveals text one character at a time, giving the impression that the character is speaking. Use an Every X seconds event or a Timer behavior. A speed of 0.03 seconds per character feels natural for most games.

// Event sheet: Typewriter effect

System: Every 0.03 seconds
  System: IsTyping = 1
    System: Add 1 to CharIndex
    txtDialogue: Set text to left(FullText, CharIndex)

    // Play a subtle blip sound every few characters
    System: CharIndex % 3 = 0
      Audio: Play "text_blip" volume -15 dB

    // Check if done typing
    System: CharIndex >= len(FullText)
      System: Set IsTyping to 0
      Functions: Call "ShowChoicesIfAny"

// Skip typewriter on click/tap
Mouse: On click
  System: IsTyping = 1
    // Reveal all text immediately
    System: Set CharIndex to len(FullText)
    txtDialogue: Set text to FullText
    System: Set IsTyping to 0
    Functions: Call "ShowChoicesIfAny"
  Else
    // Text fully revealed, advance to next node
    Functions: Call "AdvanceDialogue"

Branching Choices and Actions

When typing finishes and the current node has choices, display them as clickable buttons. If the node has no choices but has a next key, advance automatically on player click. If neither exists, close the dialogue.

// Scripting: Display choices and handle selection
function showChoices(node) {
  if (!node.choices || node.choices.length === 0) return;

  const buttons = runtime.objects.btnChoice.getAllInstances();

  for (let i = 0; i < buttons.length; i++) {
    if (i < node.choices.length) {
      buttons[i].isVisible = true;
      buttons[i].instVars.ChoiceIndex = i;
      buttons[i].instVars.NextNode = node.choices[i].next;
      // Set the text on the button's child Text object
      const label = buttons[i].getChildAt(0);
      label.text = node.choices[i].text;
    } else {
      buttons[i].isVisible = false;
    }
  }
}

// Event sheet: Handle choice click
btnChoice: On clicked
  Functions: Call "ShowDialogueNode" (btnChoice.NextNode)

// Event sheet: Handle actions triggered by dialogue
// Function "HandleDialogueAction" - parameter: ActionName (string)
System: Compare ActionName = "open_shop"
  Functions: Call "OpenShopUI"

System: Compare ActionName = "start_quest_mine"
  System: Set global variable Quest_Mine to "active"
  Functions: Call "ShowQuestNotification" ("Investigate the Old Mine")

The action field in your JSON lets dialogue trigger game events — opening shops, starting quests, giving items, playing cutscenes — without hard-coding any logic into the conversation data itself.

Adding Localization Support

To support multiple languages, restructure your dialogue files by language. Instead of embedding all languages in one file, create separate files per language. This keeps file sizes small and makes it easy for translators to work on individual files.

// dialogue/en/shopkeeper.json
{
  "start": {
    "speaker": "Martha",
    "text": "Welcome, traveler! Looking to buy something?"
  }
}

// dialogue/es/shopkeeper.json
{
  "start": {
    "speaker": "Martha",
    "text": "Bienvenido, viajero! Buscas algo para comprar?"
  }
}

// Scripting: Load the right language file
const lang = runtime.globalVars.Language || "en";
const response = await fetch(`dialogue/${lang}/shopkeeper.json`);
const dialogueData = await response.json();

// Portraits, actions, and branching structure stay in
// a shared metadata file - only text is localized

Keep non-text data (portraits, actions, branching structure) in a shared metadata file that all languages reference. Only the display text needs translation. Store the player’s language preference in Local Storage so it persists between sessions.

"Design your dialogue system data-first. If adding a new NPC conversation requires changing event sheets, your system is too tightly coupled. All conversation content should live in JSON files that non-programmers can edit."

Related Issues

If your dialogue text does not display correctly after loading from JSON, see Fix: Construct 3 AJAX Request CORS Error for issues with loading external files. If the typewriter effect runs too fast or skips frames, check Fix: Construct 3 Performance Low FPS Lag. For problems with dialogue UI elements disappearing between layouts, see Fix: Construct 3 Object Not Found After Layout Change. And if button click events fire inconsistently on mobile, check Fix: Construct 3 Touch Input Not Working on Mobile.

Data-driven dialogue scales. Hard-coded dialogue breaks.