Tue Oct 08 2024

Formatted Text

Formatted text systems in games, and how to program them

Textboxes in video games come in all shapes and sizes. Well, most of the time, they're rectangles, and they take up roughly 1/3rd of the screen. Bad start.

But the important part isn't their fancy designs or slick animations -- it's their text. Whether it appears all at once, or letter by letter, it most likely has some sort of formatting.

UNDERTALE uses typewriter text, which shows a letter one at a time.
And hey, look, that text is yellow!

The Naïve Approach

I often see tutorials make a system where text gets parsed every draw. This is very expensive, especially during render time, where frames matter.

It goes something like this:

  • We have some sort of default state, like the text being white, the font size, etc.
  • We loop through each character to draw, according to the state
  • If we encounter a modifier, change the state instead
  • Keep drawing each character (unless they're part of a modifier of course)

This comes with a few flaws:

  1. Code duplication -- You may have to copy you modifier handling to both be in the update/step code and the draw/render code to make letter-by-letter text skip past modifiers,
  2. Slow code -- String manipulation is costly, and you don't want to be doing that very often, especially not a hundred times per render frame.
  3. Spaghetti code -- Handling modifiers and drawing characters both in the render function might lead to some ugly code.

So... the current examples out there aren't that great. What should we do instead?


The Node System

No, not JavaScript.

The node system is a way to represent text as a list of nodes, where each node is a character or a modifier. This way, we can easily loop through each node and draw them accordingly.

An example of a modifier node's structure is as follows:

local node = {
    type = "modifier",
    command = "color",
    arguments = "#ff00ff"
}

This node would change the color of the following text to a magenta. Additionally, a text node would look like this:

local node = {
    type = "character",
    character = "H"
}

This node would draw the character "H" in the current state. If this node follows the previous modifier node, it would be drawn in magenta.

Constructing the Nodes

Node construction can be as simple as you want it to be. A regex, a simple loop through the string, or even a custom parser. In Kristal's text system, we chose to loop through the string.

Regex is most likely the most efficient way to do this, but not everyone knows regex, and I like to keep things simple anyway.

Processing the Nodes

Once you have your nodes, you can loop through them and draw them accordingly. If your node is a character, draw it. If it's a modifier, change the state accordingly. Simple, right?

Kristal's Text System

Talking about Kristal for a bit, we have a text system that uses this node system -- this is actually where I came up with the idea. In the function linked above, textToNodes, it takes an input string and returns a list of nodes. But there's also some extra features inside of it.

In it, there's hardcoded handling for the bind modifier, which can either return normal text, or work as a modifier, depending on whether you're using controller or not.

Ralsei from DELTARUNE saying "Kris, you need to press [C] to open the menu!"
[bind:menu] really working wonders here.

Conclusion

This is very unfinished. I ran out of energy. I want to work on other things now thank you :D

Come back later for more probably!