Create your own
Lesson illustration

Flexible Dialogue Data Format

Hello and welcome back!

In the previous module, we built a powerful, data-driven event system capable of managing stateful objects like treasure chests and handling map transitions. We established that these interactive elements are composed of triggers, conditions, and lists of commands processed by an EventInterpreter.

This lesson kicks off a new module, "Dialogue and Cutscene System." We'll leverage the foundation we've built to create one of the most character-defining features of any JRPG: the dialogue system. Our goal for this lesson is to fulfill the learning outcome: Design a flexible data format for dialogue, supporting multiple pages, character portraits, and branching choices.

Just as you design JSON schemas or API contracts in front-end development to ensure data is structured, predictable, and scalable, we will now architect the data structure that will drive every conversation in our game.

1. The Anatomy of JRPG Dialogue

Before we design the data format, let's establish what we're building. At its heart, a dialogue system presents text to the player, often accompanied by visual aids to enhance the storytelling.

Examples of JRPG Dialogue Boxes with Character Portraits
A collection of dialogue box styles common in JRPGs. Notice the variations in shape, position, and the inclusion of character portraits (busts), which are crucial for conveying emotion and identifying the speaker.

As the image shows, a typical dialogue involves:

  • A dialogue box (or window) where text is displayed.
  • The text itself, often revealed over time with a "typewriter" effect.
  • A speaker indicator, which can be a name label or a character portrait.
  • Sometimes, player choices that influence the conversation's direction.
JRPG Dialogue Options Interface
This pixel-art mock-up clearly shows the key components in action: a speaker ("Old Man"), a portrait, a line of text, and a set of choices ("YES"/"NO") for the player.

Our data format needs to be flexible enough to describe all these elements for any conversation in the game.

2. Dialogue as a Data Pipeline

In a professional game development environment, dialogue isn't just a simple text file. It's part of a complex production pipeline involving writers, programmers, localization teams, and even voice actors. Thinking about this pipeline helps us design a robust data structure from the start.

A Dialogue Pipeline

The article 'A Dialogue Pipeline' by Wild Winter provides a high-level, professional perspective on managing dialogue data in a game project. It introduces concepts that are critical for a scalable system, moving beyond just storing the text.

Please read the following three sections: Start with 'Start with Localisation' and read through 'Line IDs'. Focus on the concept of a unique Line ID for every single line of text. Next, read the section 'Dialogue Flow'. This distinguishes between the content of a line and the structure of the conversation. Finally, read 'Look Who’s Texting?'. This covers how to associate a line of dialogue with a specific character.

The key takeaways from that reading are:

  1. Separation of Concerns: We must distinguish between the dialogue's flow (the structure of the conversation, its branches, and order) and its content (the actual text, character, portrait).
  2. Unique Line IDs: Every line of text needs a unique, stable identifier (e.g., town_guard_greeting_01). This is non-negotiable for managing assets like voice-over files and translations.
  3. Character Association: The data must clearly define who is speaking. This is usually done with a characterId that links to a separate database of characters.

With these architectural principles in mind, let's start designing our format.

3. Designing the Dialogue Tree Format

Instead of scripting dialogue with a long, nested list of commands in our event files, a cleaner approach is to use a dedicated format for dialogue, much like the YARN or Ink systems mentioned in the article. An event will simply trigger a dialogue tree, and a dedicated DialoguePlayer will handle the rest.

We can represent an entire game's dialogue in a single JSON file, or split it into multiple files for better organization (e.g., town_01_dialogue.json).

Let's design a structure based on nodes. A dialogue tree is a collection of nodes, and a conversation is a path through them.

// dialogue_data.json
{
  "GuardEncounter": {
    "startNode": "Greeting",
    "nodes": {
      "Greeting": {
        // ... node content goes here
      },
      "AskBusiness": {
        // ... node content goes here
      }
      // ... etc
    }
  }
}
  • The top-level key (GuardEncounter) is the ID of the dialogue tree. This is what an event would call, e.g., start_dialogue("GuardEncounter").
  • startNode tells the DialoguePlayer where to begin.
  • nodes is an object containing all the possible states or "pages" of the conversation.

3.1. Content of a Node: Lines and Portraits

Each node contains the actual content to be displayed. A simple node consists of an array of lines. This naturally supports multi-page dialogue; the player clicks to advance through the array.

Each line object needs to contain:

  • lineId: The unique identifier we discussed.
  • characterId: The ID of the speaking character (e.g., "hero", "guard_01").
  • portrait: The name of the expression to show (e.g., "normal", "angry").
  • text: The string of dialogue.

Here's our Greeting node fleshed out:

"Greeting": {
  "lines": [
    {
      "lineId": "guard_greet_01",
      "characterId": "guard_01",
      "portrait": "stern",
      "text": "Halt! Who goes there?"
    },
    {
      "lineId": "hero_greet_reply_01",
      "characterId": "hero",
      "portrait": "neutral",
      "text": "Just a traveler."
    }
  ],
  // ... choices would go here
}

This structure is already quite powerful. It supports multiple pages of text and changing speakers and portraits with each line.

3.2. Branching the Conversation: Choices

The most interesting conversations involve player agency. A node can end with a choices array. Each choice object specifies the text to display to the player and the targetNode to jump to.

This concept is well-explained in the following video, which breaks a dialogue system down into its core components.

How to make the Perfect Dialogue System

The video 'How to make the Perfect Dialogue System' by Apox Fox provides a great breakdown of a dialogue system's components and explains a simple but effective way to handle branching.

Please watch two key segments: 'Three Main Scripts' (2:37 - 4:10): This explains the concept of a DialogLine (our line object) and a DialogueActivator (our node/tree). This reinforces the structure we're designing. 'Dialogue Trees and Branching Choices' (6:55 - 8:00): Focus on the explanation of how branching is achieved by skipping to different line numbers. In our node-based design, this is equivalent to jumping to a different named node.

Let's add choices to our Greeting node. After the hero replies, the guard asks for their business, and the player must choose how to respond.

"Greeting": {
  "lines": [
    { "lineId": "guard_greet_01", "characterId": "guard_01", "portrait": "stern", "text": "Halt! Who goes there?" },
    { "lineId": "hero_greet_reply_01", "characterId": "hero", "portrait": "neutral", "text": "Just a traveler." },
    { "lineId": "guard_ask_business_01", "characterId": "guard_01", "portrait": "suspicious", "text": "A traveler, eh? State your business in the capital." }
  ],
  "choices": [
    { "text": "I'm on a mission for the King.", "targetNode": "KingBusiness" },
    { "text": "I'm just sightseeing.", "targetNode": "Sightseeing" },
    { "text": "It's none of your business.", "targetNode": "RudeReply" }
  ]
}

Now, we define the nodes (KingBusiness, Sightseeing, RudeReply) that these choices lead to. Some of these might end the conversation (by having no choices array), while others could lead to further branches.

Here is the complete dialogue tree in our proposed format:

{
  "GuardEncounter": {
    "startNode": "Greeting",
    "nodes": {
      "Greeting": {
        "lines": [
          { "lineId": "guard_greet_01", "characterId": "guard_01", "portrait": "stern", "text": "Halt! Who goes there?" },
          { "lineId": "hero_greet_reply_01", "characterId": "hero", "portrait": "neutral", "text": "Just a traveler." },
          { "lineId": "guard_ask_business_01", "characterId": "guard_01", "portrait": "suspicious", "text": "A traveler, eh? State your business in the capital." }
        ],
        "choices": [
          { "text": "I'm on a mission for the King.", "targetNode": "KingBusiness" },
          { "text": "I'm just sightseeing.", "targetNode": "Sightseeing" },
          { "text": "It's none of your business.", "targetNode": "RudeReply" }
        ]
      },
      "KingBusiness": {
        "lines": [
          { "lineId": "guard_king_reply_01", "characterId": "guard_01", "portrait": "surprised", "text": "The King? Apologies, sir. The castle is straight ahead." }
        ]
      },
      "Sightseeing": {
        "lines": [
          { "lineId": "guard_sightseeing_reply_01", "characterId": "guard_01", "portrait": "neutral", "text": "Very well. Enjoy your visit, but stay out of trouble." }
        ]
      },
      "RudeReply": {
        "lines": [
          { "lineId": "guard_rude_reply_01", "characterId": "guard_01", "portrait": "angry", "text": "Watch your tongue! One more word out of you and you'll be seeing the inside of a cell." }
        ]
      }
    }
  }
}
Test your understanding!

Using the format above, how would you add a conditional choice to the Greeting node that only appears if the player has a flag has_royal_seal set to true? The choice should say "I carry a royal seal." and lead to a new node called SealConfirmed.

Show answer

You would add a condition property to the new choice object within the choices array.

// Inside the "Greeting" node's choices array:
"choices": [
  { 
    "text": "I carry a royal seal.", 
    "targetNode": "SealConfirmed",
    "condition": { "flag": "has_royal_seal", "value": true }
  },
  { "text": "I'm on a mission for the King.", "targetNode": "KingBusiness" },
  { "text": "I'm just sightseeing.", "targetNode": "Sightseeing" },
  { "text": "It's none of your business.", "targetNode": "RudeReply" }
]

// And you would define the new node:
"SealConfirmed": {
  "lines": [
    { 
      "lineId": "guard_seal_reply_01", 
      "characterId": "guard_01", 
      "portrait": "respectful", 
      "text": "A royal seal! My utmost apologies for the interruption. Please, proceed." 
    }
  ]
}

This shows how our dialogue system can interface with the game flag system we designed in the previous module.

4. Advanced Formatting and Embedded Commands

Classic JRPGs often use special codes within their text to add emphasis or display dynamic information. The YARN syntax provides a great model for this, using <<commands>> embedded in the text.

The Dialogue Tree extension

The GDevelop documentation for its Dialogue Tree extension explains the YARN syntax. This is a writer-friendly format that is also easy for a program to parse.

Please read the introduction and the three main sections describing the line types: '1. Text line type', '2. <> line type', and '3. Option line type'. Pay special attention to the examples for commands like <<avatar ant>> and conditional logic with <<if>>.

We can adopt a similar convention. Our DialoguePlayer could be responsible for parsing these special tags within a text string:

  • Variables: "Hello, <<var:playerName>>! Welcome to <<var:townName>>."
    • The DialoguePlayer would replace these with the current values from the game state.
  • Text Effects: "This is <<color:red>>important!<<color:default>>"
    • This would temporarily change the text color for the word "important".
  • Pauses: "Wait for it... <<pause:500>> ...NOW!"
    • The typewriter effect would pause for 500ms.
  • Game Actions: "The earth begins to <<shake>>rumble..."
    • When the parser hits <<shake>>, it could call back to the main game engine to trigger a screen shake effect without interrupting the dialogue flow.

This makes the data format extremely powerful, allowing writers and designers to script mini-cutscenes directly within the dialogue data.

Conclusion

We have now designed a comprehensive and flexible data format for our JRPG's dialogue system. By separating content from flow and using a node-based structure, we can create complex, branching conversations that can react to the game's state.

Key Takeaways:

  • A robust dialogue format separates the flow (nodes and choices) from the content (text, character, portrait).
  • A node-based graph is a powerful and intuitive way to represent branching conversations.
  • Each line of text should have a unique lineId for production purposes (localization, audio).
  • Branching is handled by a choices array at the end of a node, where each choice points to a targetNode.
  • Conditional choices can be implemented by adding a condition property that checks game flags.
  • Embedded commands (e.g., <<command>>) within the text allow for dynamic content and integration with other game systems like visual effects and sound.

Preview of the next lesson:
We have the blueprint (the data format). In the next lesson, "Implement a dialogue UI with a typewriter text effect and support for player choices," we will move from architecture to implementation. We'll start building the "view" layer of our dialogue system—the on-screen window that will take our JSON data and present it to the player, complete with portraits and the classic typewriter text reveal.

Can't find a good explanation? Sign up and we'll make it for you

Sign up