Godot Quick Tip – Typing Text Effect

This is a text companion to a video tutorial. You can watch the video below. The source code can be found at the end of the article or in the video description.

A common trope in sci-fi/horror game and even some RPG style games is to utilise text that appears over time so that it looks like it’s being typed live on a keyboard. This effect, if done right can look amazing and really sell the mood of the game but often times it uses a technique that causes partially finished words to jump from one line to the next as they fill out and wrap onto the next line. While this is a relatively harmless effect and, in some ways, quite accurate to how text wrapping works in something like a word processor it can be a little jarring if you are trying to read the words as they appear, especially if you are speaking them out loud for example during a stream or lets play. It can also make the game feel a little less polished. I wanted to see if there was a way around this in Godot.

So the usual way people will create a typing text animation is to append a single character at a time to the label’s text field. This leads in some part to the jumping lines as the engine has no way to calculate the size of the upcoming text. I set about trying to write some fancy code that I thought would fix this… and it did but only in some circumstance. I will describe this solution at the end as it turns out Godot has a built-in method that actually works.

While I was trying to work out why my solution worked in some instances and not others I had a quick look at the Godot Label source code and I couldn’t find anything that jumped out to me as the reason why. I then thought it might be an issue with the Font code but nothing stuck out there either. Pondering over the inspector for clues as to where next to look, I remembered the Percent Visible field and looking at the tool hint “Useful to animate the text in the dialogue.” I decided to investigate it.

It turns out Percent Visible does exactly what I wanted. Now I’m not entirely sure why it works and my custom solution didn’t but I’m glad it does. So here’s the setup.

Note: While the field is called Visible PERCENT the value is actually a normalised float from 0.0 to 1.0 where 1.0 equates to 100%. While this makes sense programatically, I thought I would mention it just in case.

Note: This tutorial assumes you know the basics of Godot’s editor, scene structure and the various ways to connect signals to scripts. If you need help with this, feel free to contact me on social media.

Scene Setup

  • Create a Label node. Set its size (rect_size) and enable autowrap.
  • Either in code or in the editor, give it a bunch of text. I went with a weird horror-style dialogue of “Night. The darkness keeps me from sleep. It was supposed to have the opposite effect but I can’t stop thinking about what lurks beyond the black veil.” for no real reason other than I thought it would give a good selection of word lengths to try out.
  • Now create a Timer node and set it to a short timeout, 0.05-0.1 seconds seems like a good rate for a general typing effect.
  • Connect the Timer timeout signal to your script.

Script

extends Label

var pc: float

func _ready():
    percent_visible = 0.0
    pc = 1.0 / text.length() # percent to add to the label each time the timer times out - with 150 characters this will give you 0.006667 per update, enough for 1 character per update.
    $Timer.start()

func _on_Timer_timeout():
    percent_visible += pc
    if percent_visible >= 1.0:
        $Timer.stop()

You could move this code up to some other node and leave the Label “dumb” if you wish but it would work the same way. Another nice addition might be to add a function that starts the animation when the text is updated, allowing for longer dialogues:

extends Label

var pc: float

func _ready():
    percent_visible = 0.0

func set_text_and_start(new_text: String):
    set_text(new_text)
    start_animation()

func set_text(new_text: String):
    $Timer.stop() # Just to make sure we don't accidentally try to update
    text = new_text # Reassigns the label's text ready for animating
    percent_visible = 0.0 # Hides the text before animating
    yield(get_tree(), "idle_frame") # This is to make sure that the label has updated its settings with the new text information
    pc = 1.0 / text.length()

func start_animation():
    $Timer.start()

func _on_Timer_timeout():
    percent_visible += pc
    if percent_visible >= 1.0:
        $Timer.stop()

And that’s it! Simple! You may need to do something a little more fancy if you want to create a wall of text that doesn’t fit on screen but for simple, short dialogue or longer prose that can be split into screen-appropriate chunks this works very well.


My Custom Solution

If you’re interested, my custom solution was to split the text into an array of words, delimited by spaces. Each timer tick (or whatever trigger you want to use to animate the text) add a substring of the word, padded with trailing spaces to make up the rest of the characters in the word. For example: With the word Hello. You would append:

"H    " 

then on the next update you would append

"He   "
"Hel  "

and so on until the word is complete.

extends Label

var text_to_type = """Night. The darkness keeps me from sleep. It was supposed to have the opposite effect but I can't stop thinking about what lurks beyond the black veil."""

var words: PoolStringArray
var space_positions: PoolIntArray

var offset_position: int = 0
var cur_word: int = 0

var stored_text: String = ""

func _ready():
	set_text(text_to_type)

func set_text(new_text: String):
	words = new_text.split(" ", false)
	space_positions = PoolIntArray()
	for i in range(text.length()):
		if text.substr(i, 1) == " ":
			space_positions.append(i)
        $Timer.start()

func fill_word(word: String, position: int):
	var output = word.substr(0, position)
	var space_len = word.length() - position
	# Checking that the desired length is being padded
	#print("Padding with %s spaces on word %s" % [space_len, word])
	for i in range(space_len):
		output += " "
	return output

func _on_Timer_timeout():
	offset_position += 1
	var filled = fill_word(words[cur_word], offset_position)
	#print("\"" + filled + "\"") # Checking to see if the word was being padded correctly
	text = stored_text + fill_word(words[cur_word], offset_position)
	if offset_position == words[cur_word].length():
		stored_text += words[cur_word] # Add the current word to stored text
		cur_word += 1
		offset_position = 0 # Reset to the start of the next word
		
		# Add a space between words if there are more words incoming.
		if cur_word < words.size():
			stored_text += " "

	if cur_word == words.size():
		$Timer.stop()

The general idea of this is to set the text to a previously stored text variable and then append each character from each word to the label’s text field, followed by enough blank characters to fill out the rest of the word length. When a word is completed it is added to the stored text so that each update, only the characters from the incomplete word are appended.

Weirdly, this doesn’t work any better than appending each character manually. I was a little disappointed until I thought it might be an issue with the autowrap function stripping trailing spaces. I then tried it with periods (.) instead of spaces and it worked as expected! As I said at the start of the article, I briefly looked at the source code to work out why but I couldn’t. However I will say pre-filling the space of a word with a . or * or other (random) symbol does give a certain visual style to the typing text animation and this is why I have put this little excerpt into THIS tutorial rather than writing a separate general-interest or mini-tutorial of its own. If you’re after a hacker messaging system typing effect this could well give you what you’re looking for.

As I write this I do wonder if yielding for an idle frame after updating the text may fix for the formatting issue as it would allow the autowrap to update based on the new text but this is likely wishful thinking as it doesn’t explain why padding with “actual text” works fine. I have since tried this and it doesn’t help. A final thought might be to use a combination of percent_visible and replacing the rest of the word with the filling characters but I think that will be overengineering a solution to a problem that already has a good enough answer in the form of percent_visible.

Anyway I hope you found this a quick and useful tip on setting up smooth animated text effects in Godot, that look a little more polished than just appending text one character at a time.

You can find the source code on Github.

Support This Site

If you enjoyed this content consider giving a tip on Ko-fi to help us keep producing content alongside our products.

You may also like...