Creating a TypeWriter Effect in Phaser.js v3

ยท

2 min read

I recently added some dialog options for my indie game, Dungeon Sweeper: A Knights Adventure and wanted to create a TypeWriter effect for the dialog text.

This technique works well for monospaced fonts and may not work for variable-width fonts.

Failed Attempts

My first attempt was to write out text one letter at a time. But this didn't work with text that wasn't aligned to the left or text that wrapped.

My second attempt was to use a non-space white space to replace all characters. This would help with the non-left text and word-wrapped text, but then I learned not all white space is created equal.

The Figure Space

I found an amazing webpage that helped me find the correct white space to use. The space I need is called a Figure Space and is the same width as a character.

The Code

I wanted to be able to create a function that accepts a Phaser.GameObjecte.Text and automatically applies the animation.

The function would maintain the state needed to run the animation.

The function would need to be awaitable, so the caller knows when the animation is complete.

The function works by first resetting the text to empty. Then every interval, it writes out the next letter. To maintain the same letter positions, the text is kept the same length. The unseen characters are replaced with invisible characters.

/**
 * Create typewriter animation for text
 * @param {Phaser.GameObjects.Text} target
 * @param {number} [speedInMs=25]
 * @returns {Promise<void>}
 */
export function animateText(target, speedInMs = 25) {
  // store original text
  const message = target.text;
  const invisibleMessage = message.replace(/[^ ]/g, "โ€‡");

  // clear text on screen
  target.text = "";

  // mutable state for visible text
  let visibleText = "";

  // use a Promise to wait for the animation to complete
  return new Promise((resolve) => {
    const timer = target.scene.time.addEvent({
      delay: speedInMs,
      loop: true,
      callback() {
        // if all characters are visible, stop the timer
        if (target.text === message) {
          timer.destroy();
          return resolve();
        }

        // add next character to visible text
        visibleText += message[visibleText.length];

        // right pad with invisibleText
        const invisibleText = invisibleMessage.substring(visibleText.length);

        // update text on screen
        target.text = visibleText + invisibleText;
      },
    });
  });
}

Summary

I'm pretty happy with how the animation turned out and I am now calling this from within my dialog function.

If you are interested in following the game's development, toss your email into the newsletter box below.

Cheers ๐Ÿป