wlog

The ‘Jumping Words’ Problem

animation

In many games, particularly those without voice-acting, text spoken by a character is revealed letter-by-letter. It’s a neat way to give the player the feeling that the text on the screen is being said by somebody in real-time.

It’s also very easy to implement: when you draw the text to the screen, take a substring of the appropriate length, and draw the substring instead. The substring’s length can be a counter that ticks up by one letter per frame, or at some other rate. Here’s a demo using some dialogue from our game Prim Reaper (with art and writing by Hekateras): press the ‘play’ button to see the animation.

portrait_primrose

Unfortunately, there’s a problem with the easy way. It’s not a big problem, and most people probably won’t care about it, but I do! Watch what happens when the text is long enough to wrap to the next line:

portrait_sage

Notice how the words “dubious” and “batch” start appearing at the end of the line, then jump to the next line when more letters are shown. When we naively draw the substring and let it wrap automatically, those words are truncated and there’s no way to know where they would wrap if the letters were all there.

It would look nicer if partially-revealed words didn’t jump around during the animation. To achieve that, we need to figure out in advance where the text needs to wrap. The simplest solution is to “pre-wrap” the string before drawing it, by inserting newlines at the appropriate places. Here’s a JavaScript function that does that:

function wrapText(text, width) {
    // build an array of lines
    const out = [];
    
    let line = '';
    for(const word of text.split(/\s+/)) {
        if(line === '') {
            line = word;
        } else {
            const longerLine = line + ' ' + word;
            if(measureTextWidth(longerLine) > width) {
                // the new line would be too long; wrap here
                out.push(line);
                line = word;
            } else {
                line = longerLine;
            }
        }
    }
    
    // lines only get pushed when they wrap;
    // still need to add the last line to the array
    if(line !== '') {
        out.push(line);
    }
    
    // join the lines together, with newlines where it wraps
    return out.join('\n');
}

For this to work, we need some function measureTextWidth which determines how wide a given string will appear. This will normally be available from the same API which draws the text; for example, on a HTML canvas it’s measureText(line).width. Alternatively, if you’re using a monospace font, it could be as easy as line.length * LETTER_WIDTH.

OK, so this solution isn’t perfect:

But it’s simple, and good enough for game dialogue. Here’s what it looks like in action:

portrait_sage

A tiny improvement? Yes. Worth it? Also yes.

In our actual game, I do something like the above, but not exactly. It turns out that on an HTML canvas, it’s much faster to call drawImage for every individual letter, than to call fillText just once. So all of the text is rendered using bitmap fonts, and since the fonts we use aren’t monospace, I need to compute coordinates for each letter in the string.

It makes sense to do that just once for the whole string and store the results. So the string still gets wrapped only once, but then instead of taking a substring of length k on each frame, I just draw the first k letters at their correct coordinates. The coordinates don’t have to be recomputed for each k, and that naturally avoids the jumping words problem.


Matching 2D Patterns