Everybody loves that RPG text-typing effect whereby your game's dialogue is spelled out in the textbox letter-by-letter rather than appearing all in one lump. However, this can cause conflicts with the word wrapping on your text object.
This is a mistake I see in a lot of indie games - the visual bug whereby text starts typing out across the box, but then the word-wrapping kicks in mid-word and causes the entire last word of the line to 'jump' down to the line below. It looks like this:
It's pretty gross - you'd never see that happen in Zelda or Final Fantasy, would you? A few years ago I posted a solution on the Construct 2 forums which has since been removed, but I have continued to get questions about it from time to time - so I thought I would take the time to write up a full explanation.
My solution assumes you will be using a SpriteFont rather than the plain Text plugin (which is horrible for games anyway, due to possible rendering and font discrepancies on the user's machine), and works by processing your text to automatically insert line breaks every x characters before it is rendered to the screen. It works best for monospaced fonts with consistent character widths.
The reason is that you will need to know how many characters to allow per line - the first step is to give your SpriteFont an instance variable named charsPerLine. This is where we will tell the game engine how long each line should be - for me, it was 39 characters. In my example, my SpriteFont is named MainSpritefont.
If your game doesn't use a monospaced font, and all the character widths are different, you can still use this method, but you'll need to work out how many characters is a good rule of thumb for your font - perhaps by counting the number of characters in a string containing a lot of M's and W's (usually the widest letters in most fonts). By calculating a worst-case scenario, you can come up with a good character limit.
My Construct 2 Typewriter Text With Word Wrap
The following system makes use of Rex's Text Typing behaviour, which is available here. Essentially, what my solution does is it splits your string into individual words and then reassembles it, checking each word and counting characters to see if it makes a given line too long.
First of all, you will need to create a Global Variable called textToProcess. This is where we will put the initial, unprocessed text.
This is the function:
I'm going to go into detail and break down exactly how and why it works, but you can simply replicate this as-is to create the same effect in your game.
If you're interested to understand how the thing works... read on!
How does this function work?
First of all, there are three Local Variables - Assembler, charsToIgnore and whatIndex.
- Assembler is where the function will put the finished, processed string together as it runs.
- charsToIgnore is necessary because the function needs to know how many characters are in the current line of processed text. However, len() will only return the entire length of the text assembled so far. Therefore, every time we finish a line and insert a line break, we will add that number of characters to this variable - causing the system to ignore characters on previously completed lines.
- whatIndex is needed because the function moves word-by-word through your text assessing whether or not a line break is required - this variable simply keeps track of which word it's currently on.
The While loop explained
The first sub-event of the function establishes a While loop that splits your string into individual words using tokencount(). A while loop is a programming concept that means that while a certain condition or set of conditions is true, the code will loop around again and again until it becomes not true.
Tokens are incredibly useful in Construct 2, and let you split a string into chunks by using any character you like as a divider. Therefore, tokencount(textToProcess," ") splits your string into tokens every time it encounters a space and then counts them - giving us a count of how many words are in your string.
For example, "This is an example, everybody!" gets split into "This", "is", "an", "example," "everybody!" and gets counted as five tokens. The While loop checks to see if the number of words is greater or equal to the whatIndex variable. Because whatIndex will be incremented every time the function checks a word, this whole While loop basically means: while the total number of words in the string is greater or equal to the index of the word we're on - for example, if there are nine words, and we're on word five, the While loop is upheld as true and the further sub-events are executed. If it is false, that means all of the words have been processed and the loop has ended.
Now we come to the next sub-event, with the most complicated-looking condition of them all:
len(Assembler)-charsToIgnore+len(tokenat(textToProcess,whatIndex," ")) ≤ MainSpritefont.charsPerLine
I promise it's not as scary as it looks! Let's break it down:
- len(Assembler) - how many characters are in the assembled, processed text so far.
- charsToIgnore - we covered this earlier (it's the character count of completed lines in the processed text that we don't want to consider).
- len(tokenat(textToProcess,whatIndex," ")) - looks complicated, but it simply counts the characters in the current word in your string. If we were on word three of the five-word string "I really like green bananas", we would get the word "like" and the character count would therefore output as 4.
- MainSpritefont.charsPerLine - we covered this earlier, too - it's the maximum character-per-line count we selected for your SpriteFont.
In even simpler wording, we're testing the character count of a given word in the string to see if we can add it to the current line without going over our character limit. That's all it does. Then, if this condition is true, the events simply add the word to the processed output in the Assembler and increment the whatIndex counter by 1 - allowing us to progress onto the next word and continue for another round of the While loop.
Still with me? It gets easier, I promise...
Next, we have an Else condition. This means that if the previous condition was not true, the system does these events instead. In this particular case, that equates to "the current word can not be added to the assembled string without going over the characters-per-line limit".
This is the part we've all been waiting for - the key thing we need this whole function to do. The system has identified that a line will become too long, and we need to insert a line break at the end of the Assembler to make the word wrap effect behave properly. This is done via the system expression newline.
Next, we need to tell our function that the characters of the line we've just completed should not be counted any more - that line is finished, and we can move on. To do this, we set the value of charsToIgnore to the current character count of Assembler via len(Assembler).
The rest of the actions triggered by the Else condition are identical to earlier, and simply start the process of building another line afresh.
After the While loop
Now we come to this condition:
tokencount(textToProcess," ") < whatIndex
This condition tests to see if we've processed all the words. When the word count of our initial textToProcess is less than the number of words the function has attempted to process, the entire operation is complete - every available word has been evaluated.
Now, we wrap everything up by doing the following:
- Send the completed, processed text in the Assembler back to textToProcess.
- Reset whatIndex to 0 for next time.
- Reset Assembler to an empty string, "", for next time.
- Reset charsToIgnore to 0 for next time.
- Finally, send the finished text in textToProcess to the SpriteFont and spell it out on-screen with the Text Typing plugin! (I have set the typing speed of one character per 0.013 seconds in my example, but you can set this to whatever you like).
Enjoy your typing text effect!
Now all you need to do to call this effect any time you want is to
- Put the text you want to type into the textToProcess variable, and
- Call the function processNewlines().
I hope this post has solved your Construct 2 textbox problems - if not, please feel free to ask questions in the comments below or approach me on Twitter! I promise I'll do my best to help!