Coffee-Break Tutorial: Easy Typewriter Dialogue (GML)


Coffee-Break Tutorial: Easy Typewriter Dialogue (GML)

Today's coffee-break tutorial covers a great effect that is useful in a wide variety of situations. Making a dialogue object that has lines of text that appear a letter at a time, just like it was being written on a typewriter! How to make an effect like this is a recurrent question we see come up on the forums and other social platforms, and while there are a number of great assets out there that will permit you to do this. We thought we would show you how this can be done in the simplest form, so you can learn and have base to build your own text systems.

NOTE: This tutorial is for people that use GML. If you prefer to use DnD to make your games, we have a companion article for you here.


GETTING STARTED

For this tutorial, you will need to create a new project in GameMaker Studio 2 as we'll be starting from scratch. In this project you should add a new font asset and give it a name (in this tutorial we'll be calling it simply fnt_dialog), and then add a new object asset and name it obj_dialog):

Project Setup Font And Object

We'll be adding all our code into the dialog object and the idea is that we'll be creating an array of text, where each item in the array is a complete sentence or paragraph of text, and then we'll be taking that text and drawing only a section of it each step, advancing the section to show a new letter little by little to create the typewriter effect. So, let's get started...


THE STRING_WRAP FUNCTION

Before we add any code into our dialog object, we need to first define a function that we'll need to deal with wrapping text that is too long for the space we want to draw it in. Now, GameMaker Studio 2 has a GML function draw_text_ext that can be used to automatically wrap text when it overflows a given length, and in many cases that will be perfectly fine to use. However, since we will be drawing our text a letter at a time, this function causes an issue where words will start to appear on one line, then "jump" to the next as the GML function detects that they are now longer than the maximum width. You can see this happening in the following GIF:

Draw_text_ext Overflow Problem

To prevent this from happening, we are going to take our input string then run it through a function that will add in the required line breaks based on the position of spaces in the text. This will mean that the string will be pre-formatted to go to a new line before it's even drawn.

You should make a new Script asset for this and while you can name it anything you wish, for the sake of this tutorial, we'll call it the same as the function it's going to contain: string_wrap. In the script you'll see that there is already a base function definition prepared for use and so we'll edit it to look like this:

/// @function                   string_wrap(text, width);
/// @param  {string}    text    The text to wrap
/// @param  {real}      width   The maximum width of the text before a line break is inserted
/// @description        Take a string and add line breaks so that it doesn't overflow the maximum width

function string_wrap(_text, _width)
{
var _text_wrapped = "";
var _space = -1;
var _char_pos = 1;
while (string_length(_text) >= _char_pos)
    {
    if (string_width(string_copy(_text, 1, _char_pos)) > _width)
        {
        if (_space != -1)
            {
            _text_wrapped += string_copy(_text, 1, _space) + "\n";
            _text = string_copy(_text, _space + 1, string_length(_text) - (_space));
            _char_pos = 1;
            _space = -1;
            }
        }
    if (string_char_at(_text,_char_pos) == " ")
        {
        _space = _char_pos;
        }
    _char_pos += 1;
    }
if (string_length(_text) > 0)
    {
    _text_wrapped += _text;
    }
return _text_wrapped;
}

All this script is doing is looping through the given string a letter at a time and checking to see if the length of the string after each letter is added to it overflows the permitted text width, while also storing the position of the last space found between words in a variable. If the text does overflow the given width, then the function adds a line break ("\n") into the text at the last space, then continues on, and it will do this for the entire length of the given text, adding as many line breaks as required.


THE CREATE EVENT

With that done, we can turn our attention to the dialog object and actually start to code our typewriter effect. To start with we need to add a Create Event to set up some variables that our object will need. We'll need quite a few variables to control things, so we've broken it into sections of variables to make it easier, so add a Create Event to the object now, and then add this first set of variables:

text[0] = "Hello World";
text[1] = "This is a really, really, really, long and silly string to test that the line wrapping works okay.";

This first blog defines an array which is what we'll use to contain each of our lines of dialogue. Here we're adding in two lines, but you can add more as you wish, just make sure that you increment the array position for each line. Now add the following:

text_current = 0;
text_last = 1;
text_width = 300;
text_x = 32;
text_y = 32;

This next set of variables initialises all the controller variables that will be required to draw our text using the typewriter effect. These variables are as follows:

  • text_current : this will be used to tell GameMaker what the current text array position to use for the text.

  • text_last : this will be used to tell GameMaker what the last array position is, in this case 1 as we have 2 positions - 0 and 1 - in our array. For an array with position 0, 1, 2, and 3, then this variable would be set to 3.

  • text_width : this is the maximum width that we want the text to be drawn before it wraps to a new line

  • text_x : the X position where we want to draw the text in the room

  • text_y : the Y position where we want to draw the text in the room

The final set of variables to add looks like this:

char_current = 1;
char_speed = 0.25;

These two variables will control the currently last character of the text being drawn, and the timing of when each character is drawn. By changing the variable char_speedyou can change how fast or slow the text appears on the screen.

The last thing to do in our Create Event is to call the function we created earlier on the first line of text we'll be drawing so that it wraps correctly within the given width:

text[text_current] = string_wrap(text[text_current], text_width);

KEY PRESS "SPACE" EVENT

Most typewriter effects permit you to skip the effect in some way, and so we'll be adding this into our own example. We want the skip effect to work in two different ways:

  • If the current line hasn't finished being drawn, then pressing skip will draw the whole line right to the end instantly

  • If the current line has finished being drawn, then pressing skip will go to the next line and start drawing that

We'll do this using the Space key on the keyboard, but you can use any input method you want as the code we'll show will be the same. So, add a Keyboard Pressed - Space event to the dialog object now. First, we'll check the length of the whole string being drawn against the current position of what is actually being drawn, like this:

var _len = string_length(text[text_current]);
if (char_current < _len)
    {
    char_current = _len;
    }

All this does is set the currently last character being drawn to the last character in the whole string, so the whole string will instantly be displayed. If the whole string is already being displayed, then we need to use an elseand add into that the following:

else
    {
    text_current += 1;
    if (text_current > text_last)
        {
        room_restart();
        }
    else
        {
        text[text_current] = string_wrap(text[text_current], text_width);
        char_current = 0;
        }
    }

Here we advance the variable that tells GameMaker which array position to get the text from, and then we check to see if we've advanced outside of the size of the array. If we have, then we restart the room, but that's just for this tutorial. Normally you'd then destroy the dialog object or have your game perform some action here. If there is still more text in the array, then we call our string_wrapfunction to add line breaks to the next line to be displayed and reset the currently last character to the start of the string ready to display it letter-by-letter again.


THE DRAW EVENT

The draw event is probably the simplest of all to set up and we'll start by setting how we want the text to be displayed by choosing a font, position and colour. Add a Draw Event now and give it this code to prepare the text formatting:

draw_set_font(fnt_dialog);
draw_set_halign(fa_left);
draw_set_valign(fa_top);
draw_set_colour(c_white);

With that done, it's time to actually draw our dialogue text! First, we need to check the current last character of the text being drawn, and if it's not the last character of the whole text, advance it by the speed that we set in the create event:

var _len = string_length(text[text_current]);
if (char_current < _len)
    {
    char_current += char_speed;
    }

To draw the text, we simply use the string_copy() function to copy the section of the total text from the beginning to the current last character and then draw that, like this:

var _str = string_copy(text[text_current], 1, char_current);
draw_text(text_x, text_y,  _str);

SUMMARY

You can add the dialog object to a room now and run the game and the results should be something like this:

The final effect in all its glory

With this, you now have a dialog object that you can use at any time in a game! How do you ask? Well, all you need to do is set the different variables as the object is created. For example, in out test project, you can create a controller object and give it a key or mouse event, and in that event, you can add something like this:

var _inst = instance_create_layer(x, y, layer, obj_dialog);
with(_inst)
    {
    text[0] = "This is some text";
    text[1] = "This is more text";
    text[2] = "This is yet more text";
    text[3] = "I talk too much!";
    text_last = 3;
    text_width = 150;
    text_x = x;
    text_y = y;
    text[text_current] = string_wrap(text[text_current], text_width);
    }

Here we are creating a dialog instance and then using the with command to set the text, number of lines, and the width and position of the dialog object. You could even create a new function that does all this for you using this code as a template.

That's it for this Coffee-break Tutorial, and we hope it's been useful to you!

Happy GameMaking!