Hello! Welcome back to our journey into creating a JRPG.
In our last lesson, we designed the "blueprint" for our dialogue system: a flexible JSON data format that can represent complex, branching conversations with character portraits and conditional logic. We established the data model for our dialogue.
Today, we'll bring that blueprint to life. This lesson focuses on the learning outcome: Implement a dialogue UI with a typewriter text effect and support for player choices. We will build the "view" and "controller" for our dialogue system—the visual components the player interacts with. Given your background in front-end development, you can think of this as building a stateful UI component that receives data (our dialogue JSON) as a "prop," renders it to the screen, and handles user input to update its state.
1. The Anatomy of a Dialogue UI Component
We'll approach this by conceptualizing a DialogueUI class or object. This component will be responsible for everything related to displaying dialogue on the screen. Its core responsibilities are:
- Drawing the Window: Creating the container for text and portraits.
- Rendering Text: Displaying the dialogue string with a classic "typewriter" effect.
- Handling Progression: Responding to player input to advance text or skip the typewriter effect.
- Managing Choices: Displaying a list of options and allowing the player to select one.
Let's start with the visual foundation: the dialogue box itself.
2. Drawing the Dialogue Window
At its simplest, a dialogue window is a rectangle drawn on the canvas. However, for that classic JRPG feel, we often want a stylized window with a border. A common and efficient technique for this is using a 9-slice sprite.
A 9-slice sprite is an image that's divided into a 3x3 grid. When you scale the window, the corners remain unscaled, the top and bottom edges are stretched horizontally, the side edges are stretched vertically, and the center is stretched in both directions. This prevents the borders from looking distorted. This is conceptually similar to the border-image property in CSS.
The following video demonstrates how to set up the parameters for a text box and draw it using this scalable method.
Branching Dialog System with Effects in GMS 2! (Part 1: Setup and Typing Effect)
In this video from 'Branching Dialog System with Effects in GMS 2!', Peyton Burnham explains how to set up the variables for a text box and render it using a 9-slice sprite. The concepts are directly applicable to a canvas-based renderer.
Please watch two segments: 'Setting up the Text Box Object and Variables' (6:48 - 9:57): Focus on the variables being defined: text_box_width, text_box_height, border, and line_sep. This is the basic configuration for our dialogue window. 'Drawing the Text Box Background and Text' (26:46 - 30:00): Pay attention to the logic for drawing the scalable background (draw_sprite_ext). You don't need to worry about the GML-specific function names, but grasp the concept of using the sprite's original dimensions versus the target dimensions to calculate the scale.
In our JavaScript canvas implementation, the logic for a DialogueUI would look something like this:
class DialogueUI {
constructor() {
// Dimensions and position on the screen
this.x = 50;
this.y = 400;
this.width = 700;
this.height = 150;
this.padding = 20;
}
draw(canvasContext) {
// Draw a simple box for now
canvasContext.fillStyle = 'blue';
canvasContext.fillRect(this.x, this.y, this.width, this.height);
canvasContext.strokeStyle = 'white';
canvasContext.lineWidth = 4;
canvasContext.strokeRect(this.x, this.y, this.width, this.height);
// Later, this would be replaced with 9-slice logic
// for drawing a more detailed window sprite.
}
}
3. Implementing the Typewriter Effect
The typewriter effect, where text appears character by character, is an iconic feature of JRPGs. The implementation is a straightforward piece of state management. We need a counter to track how many characters of the current line are visible.
In each frame of our game loop, we increment this counter and then draw a substring of the full text.
How do a make a Dialogue System?
For a very clear and concise explanation of the core typewriter logic, a forum post by user FrostyCat on the GameMaker forums is an excellent resource. It cuts through the noise and focuses on the essential counting variables.
Read through the post by FrostyCat. Focus on the variables textIndex and currentTextProgress. See how currentTextProgress is incremented in the Step event (our update function) and then used with string_copy in the Draw GUI event (our draw function) to create the effect. string_copy is analogous to JavaScript's substring or slice method.
Let's adapt this logic into our DialogueUI class. We'll introduce a state for managing the active line of dialogue.
class DialogueUI {
constructor() {
// ... previous properties
this.activeLine = "Halt! Who goes there?";
this.visibleCharacters = 0;
this.typewriterSpeed = 50; // Characters per second
}
// This method is called from the main game loop
update(deltaTime) {
// Increment the number of visible characters over time
if (this.visibleCharacters < this.activeLine.length) {
this.visibleCharacters += this.typewriterSpeed * deltaTime;
}
}
draw(canvasContext) {
// ... draw the box
// Get the substring to display
const textToDraw = this.activeLine.substring(0, this.visibleCharacters);
// Set font properties
canvasContext.fillStyle = 'white';
canvasContext.font = '24px sans-serif';
// Draw the text inside the box with padding
canvasContext.fillText(
textToDraw,
this.x + this.padding,
this.y + this.padding + 24 // +24 for font size
);
}
}
Notice we're using deltaTime (the time elapsed since the last frame), which we implemented in Module 1. This ensures the text reveals at a consistent speed regardless of the game's frame rate.
4. Handling Player Input and Progression
A dialogue system isn't just about display; it's interactive. The player must be able to advance the conversation. A common design pattern is:
- If the player presses the "confirm" button while the text is still typing, the effect is skipped, and the entire line is revealed instantly.
- If the player presses the button after the line is fully visible, the system advances to the next line of dialogue (or presents choices).
This video provides a practical demonstration of implementing this exact logic.
Branching Dialog System with Effects in GMS 2! (Part 1: Setup and Typing Effect)
Let's return to Peyton Burnham's video, which has an excellent segment on handling this dual-purpose player input.
Watch the section 'Player Input for Page Progression' (20:28 - 24:54). Observe the if/else logic that checks if the text is still 'typing'. If it is, input skips the animation; if not, it advances to the next page or closes the box.
To implement this, our DialogueUI needs to track its state more formally (e.g., TYPING, WAITING_FOR_NEXT), and our update method needs to check for player input.
Here's a sketch of the logic:
// In our input handler, we'd have a flag like...
let confirmButtonPressed = false;
// Inside DialogueUI.update(deltaTime)
// Check if the current line is fully revealed
const isLineComplete = this.visibleCharacters >= this.activeLine.length;
if (confirmButtonPressed) {
if (isLineComplete) {
// Advance to the next line
this.goToNextLine();
} else {
// Skip the typewriter effect
this.visibleCharacters = this.activeLine.length;
}
}
5. Displaying and Handling Choices
This is where our UI becomes truly interactive. When our dialogue data presents a choices array, the UI must:
- Stop displaying text.
- Render a list of the available choices.
- Display a cursor to indicate the player's current selection.
- Allow the player to move the cursor up and down.
- On confirmation, report the chosen branch back to the system.
Chatterbox: Branching Dialogue for Game Maker Studio 2
Implementing a choice menu involves a different UI state. The Chatterbox library tutorial provides a clear, logical walkthrough of how to handle this, which we can adapt.
Read the section titled 'Wait, What About Branches?'. Focus on the logic used to: Check if options are available (ChatterboxGetOptionCount). Loop through the options to draw each one on the screen. Handle keyboard input ('1', '2', etc.) to make a selection (ChatterboxSelect). We'll adapt this to a cursor-based selection.
Based on that logic, we can add a CHOICES state to our DialogueUI.
Conceptual Implementation:
- State Management: Our
DialogueUIwill have amodeproperty, which can beTYPING,IDLE(line finished), orCHOICES. - Data: When showing choices, the UI will need access to the
choicesarray from our dialogue data node and an internalselectedChoiceIndex. - Input Handling: In
CHOICESmode, theupdatemethod will listen forup,down, andconfirminputs.Up/downwill modifyselectedChoiceIndex, making sure it wraps around.Confirmwill trigger the selection. - Rendering: In the
drawmethod, if themodeisCHOICES:- It will iterate over the
choicesarray. - For each choice, it will draw the text (e.g.,
choice.text). - If the loop index matches
selectedChoiceIndex, it will also draw a cursor (e.g., a>sprite or character) next to the text.
- It will iterate over the
Test your understanding!
Imagine you have implemented the choice rendering logic. Your draw method now includes this pseudocode:
if (this.mode === 'CHOICES') {
this.activeChoices.forEach((choice, index) => {
// Draw the text for choice.text at a specific y-position
drawText(choice.text, x, y_position_for_index(index));
if (index === this.selectedChoiceIndex) {
// How do you draw the cursor?
// ???
}
});
}
How would you complete the // ??? section to draw a simple > character as a cursor to the left of the selected choice's text?
Show answer
You would use the drawText function again, but position the > character slightly to the left of the choice text.
if (index === this.selectedChoiceIndex) {
// Draw the cursor just to the left of the choice text
const cursorX = x - 20; // 20 pixels to the left
const cursorY = y_position_for_index(index);
drawText(">", cursorX, cursorY);
}
This simple addition provides clear visual feedback to the player about their current selection.
Conclusion
Congratulations! We've just mapped out the complete implementation of a dialogue UI. We've moved from the abstract data structure of the last lesson to the concrete visual and interactive elements that the player will experience.
Key Takeaways:
- A dialogue UI is a stateful component that can be in different modes:
TYPING,IDLE(waiting for input), orCHOICES. - The typewriter effect is achieved by using a time-based counter to reveal a progressively larger substring of the full text line.
- Player input is context-sensitive: it can either skip the typewriter animation or advance the dialogue to the next stage.
- Displaying choices involves rendering a list from the data, tracking a selected index with a visual cursor, and handling input to navigate and confirm a selection.
Preview of the next lesson:
We now have two powerful but separate pieces: a data format for dialogue trees and a UI component capable of rendering them. In the next lesson, "Integrate the dialogue system with the event system to show text and handle choice-based branching," we will finally connect them. We will create a DialogueManager that loads the data, controls our new DialogueUI, and hooks into the master EventInterpreter we built in Module 3. This will allow an event command like ShowDialogue("GuardEncounter") to kick off a fully interactive conversation.
Can't find a good explanation? Sign up and we'll make it for you
Sign up