Typewriter Effect for Text and BitmapText in Phaser 3

Create a typewriter effect for web font and bitmap text

by on 6 minute read


Are you making a story-driven game like an RPG or interactive novel?

Having text show up one character at a time is common in those types of games and makes it feel like the characters are talking!

Maybe you've come across tutorials for doing this that are no longer up to date or don't work with Phaser 3?

Well, you are in luck!

In this article, we will show you how to create a typewriter effect for Text and BitmapText GameObjects for Phaser 3.

Scene Set-Up

All our example code is going to be within the context of this demo Scene:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import Phaser from 'phaser'

export default class TypewriterDemo extends Phaser.Scene
{
	constructor()
	{
		super('typewriter-demo')
	}

	preload()
	{

	}

	create()
	{

	}
}

We'll be using modern JavaScript and a project based on the phaser3-parcel-template.

If you are not familiar with using modern JavaScript with Phaser 3 then we have a free book to help you get started!

Learn to make an Infinite Jumper in Phaser 3 with modern JavaScript!

Drop your email into the box below to get this free 60+ page book and join our newsletter.

Learn more about the book here.

Typewriter for Text

The core concept behind this effect is to add 1 character from a string to a Text object at a consistent interval until all the characters are added.

Let's start by creating a Text object:

1
2
3
4
create()
{
	this.label = this.add.text(100, 100, '')
}

We use a class property called label to store the created Text GameObject.

Notice that we are passing in an empty string to start.

Next, let's make a method that will take a string and display each character one at a time:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/**
 *
 * @param {string} text
 */
typewriteText(text)
{
	const length = text.length
	let i = 0
	this.time.addEvent({
		callback: () => {
			this.label.text += text[i]
			++i
		},
		repeat: length - 1,
		delay: 200
	})
}

We use this.time.addEvent to have a function called every 200 milliseconds that repeats length - 1 times.

The length variable is the number of characters from the given text string.

This will let us append 1 character from text to this.label.text each time the function is called.

The variable i is used to keep track of which character to append to this.label.text each time the function is called.

Call this method from create() to see the typewriter effect in action:

1
2
3
4
5
6
create()
{
	this.label = this.add.text(100, 100, '')

	this.typewriteText('Hello, World!')
}

Typewriter for Text that Wraps

In some cases, the above implementation will work fine for text that wraps but there can be times when a word is getting typed out and then suddenly wraps once it reaches the bounds of the text box.

A better approach is to go to a new line if a word being typed out is going to be wrapped.

Let's do that in a new method called typewriteTextWrapped(text):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/**
 *
 * @param {string} text
 */
typewriteTextWrapped(text)
{
	const lines = this.label.getWrappedText(text)
	const wrappedText = lines.join('\n')

	this.typewriteText(wrappedText)
}

Notice that this method reuses typewriteText() after it generates a string with line breaks in the appropriate places.

We start by calling the getWrappedText() method on this.label to get each wrapped line as an entry in a list called lines.

Then we simply join the array using a line break (\n) to create a string that wraps properly when given to this.typewriteText(wrappedText).

Try it out by setting a width on this.label and then calling this.typewriteTextWrapped():

1
2
3
4
5
6
7
create()
{
	this.label = this.add.text(100, 100, '')
		.setWordWrapWidth(100)

	this.typewriteTextWrapped('Hello, World!')
}

In practice, you'll likely always want to use the version that wraps so you can merge these two methods into one.

Typewriter for BitmapText

The core concepts will be the same for BitmapText but the specifics will be a little bit different from Text

We will be using some properties that are not officially documented but is used internally by Phaser.

Let's start by loading a bitmap font:

1
2
3
4
preload()
{
	this.load.bitmapFont('american-typewriter', 'assets/american-typewriter.png', 'assets/american-typewriter.fnt')
}

We used bmGlyph for Mac to generate the font assets and chose the “Sparrow / Starling” publish option.

There's also BMFont for Windows, Glyph Designer for Mac, and a web-based one called Littera.

Next, let's create a BitmapText GameObject:

1
2
3
4
5
create()
{
	this.bitmapLabel = this.add.bitmapText(100, 100, 'american-typewriter', '')
			.setMaxWidth(500)
}

We added a class property called bitmapLabel and then set it to a BitmapText instance that uses the font we loaded.

Then we call setMaxWidth(500) to designate a width for wrapping.

To perform the typewriter effect logic, let's create a typewriteBitmapText() method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 *
 * @param {string} text
 */
typewriteBitmapText(text)
{
	this.bitmapLabel.setText(text)

	const bounds = this.bitmapLabel.getTextBounds(false)
	const wrappedText = bounds['wrappedText'] || text

	this.bitmapLabel.setText('')

	const length = wrappedText.length
	let i = 0
	this.time.addEvent({
		callback: () => {
			this.bitmapLabel.text += wrappedText[i]
			++i
		},
		repeat: length - 1,
		delay: 200
	})
}

The second half of this method should be familiar as it is virtually identical to the typewriteText() method we created earlier.

The main difference is the first 4 lines where we start by seting the text with this.bitmapLabel.setText(text) and then use getTextBounds(false) to get the information we need.

You can log bounds to the console to see what properties it has. The official docs for BitmapTextSize has just 2 properties but you'll see that there's more.

The one that interests us is wrappedText as it contains a string with line breaks placed at the appropriate places.

Because it is not officially documented, we store it into a local wrappedText variable using key access and the logical OR operator on line 10.

This means the local wrappedText variable will be set to text if bounds.wrappedText does not exist. We access the property as a key to avoid VS Code showing us an error but that is optional.

Try it out by calling the method in create():

1
2
3
4
5
6
7
create()
{
	this.bitmapLabel = this.add.bitmapText(100, 100, 'american-typewriter', '')
			.setMaxWidth(500)

	this.typewriteBitmapText('Hello, World!')
}

One thing to note is that we are setting and then clearing out the text on lines 7 and 12 in typewriteBitmapText()

It would be better if we did not have to do this but we did not see another way to get the necessary wrapped text information.

Let us know in the comments below if you know of another way to do this!

Next Steps

You now know the core concept for creating a typewriter effect for Text and BitmapText. You can apply the same concepts for DynamicBitmapText!

Feel free to adjust the delay property from 200 milliseconds to something slower or faster depending on your game.

Be sure to sign up for our newsletter so you don't miss any future Phaser 3 game development tips and techniques!

Drop your email into the box below.

Don't miss any Phaser 3 game development content

Subscribers get exclusive tips & techniques not found on the blog!

Join our newsletter. It's free. We don't spam. Spamming is for jerks.

Phaser 3 text bitmap text typewriter

Want tips and techniques more suited for you?


You may also like...


Video Guides


Beginner Guides


Articles Recommended For You

Memory Match in Modern Javascript with Phaser 3 - Part 6

by on

If you've got the basics of Phaser 3 in modern JavaScript down then it might be time to try making something a bit more …

8 minute read

Memory Match in Modern Javascript with Phaser 3 - Part 5

by on

If you've got the basics of Phaser 3 in modern JavaScript down then it might be time to try making something a bit more …

6 minute read

Memory Match in Modern Javascript with Phaser 3 - Part 4

by on

If you've got the basics of Phaser 3 in modern JavaScript down then it might be time to try making something a bit more …

7 minute read

Memory Match in Modern Javascript with Phaser 3 - Part 3

by on

If you've got the basics of Phaser 3 in modern JavaScript down then it might be time to try making something a bit more …

10 minute read

Didn't find what you were looking for?


comments powered by Disqus