04.animating-text-with-tween-animation

Animating the text display with tweens

Games feel much better with animations. In games, we often use computer-generated animations called tweens, short for in-betweens.

We give the computer a desired starting and end state and let it animate in-between.

For example, we could tell the computer to start by showing no text and end with the whole text visible, letting the computer animate the transition.

All the frames between the start and the end are the tween animation.

Using tweens

Godot comes with a node dedicated to tween animation: the Tween node.

Select the Slideshow node and add a new Tween node as a child.

Then, head back to the Slideshow script by clicking the script icon in the Scene dock.

We want to get the Tween node in an onready variable and use it to animate the text.

onready var tween := $Tween

Placing the animation code

We need a place to run our animation code. We want to animate the text every time we go to the next slide, so there are three places we could pick from:

  1. The advance() function, which takes us to the next slide.
  2. The show_slide() function, which displays the new slide.
  3. And the set_text() function, which updates the displayed text.

Which would you pick?

You will have to make this kind of decision all the time as a developer. It’s not always easy as there are often several valid options.

In this case, we’ll use the set_text() function because we’re animating the text exclusively.

Setting up the tween

To set up a tween animation, we call Tween.interpolate_property(). This function takes five arguments:

  1. The node to animate. Here, it’ll be our label.
  2. The property to animate as a String.
  3. The starting value.
  4. The end value.
  5. The animation’s duration in seconds.

The Label node comes with a percent_visible property that controls the percentage of the text displayed on the screen.

When the value is 0.0, no text is visible. When the value is 1.0, you can see all the text.

We use it to animate the text whenever we update it, like so.

func set_text(new_text: String) -> void:
    # ...
    # We want the text to display at a fixed rate. This division ensures that
    # the text will appear at a rate of 60 characters per second.
    var duration := new_text.length() / 60.0
    tween.interpolate_property(label, "percent_visible", 0.0, 1.0, duration)
    tween.start()

You can press F6 to run the scene and see the text animate.

The finishing touch

Whenever we press the button, even if the text is still animating, we skip to the next slide. That’s not nice, as the player could miss a lot of the text.

If the text is still animating, pressing the button should show it all. And when the text is entirely visible, pressing it should advance to the next slide.

We can achieve that by adding a condition to the advance() function.

func advance() -> void:
    # If the animation is still in progress, we seek to the end.
    if tween.is_active():
        tween.seek(INF)
    # Otherwise, we run through our previous condition.
    else:
        if slide_index == last_slide_index:
            get_tree().quit()
        else:
            show_slide(slide_index + 1)

The last four lines are the same code as in the previous lesson.

On top of that, we now call Tween.is_active() to check if the animation is still running. If so, we call tween.seek() to advance to the end of the animation.

The value INF is a constant built into Godot that represents infinity. We can use it to go to the end of any animation reliably.

If you run the scene now, the text should advance more naturally when pressing the button.

Challenge: Party members list

Open the practice Party members list.

In this practice, you’ll reuse the same techniques you learned in the past two chapters to create party members’ displays.

In summary

This concludes the short slideshow series.

With it, you already know the core of displaying linear dialogues:

Two functions and some data, that’s all it takes. While we have a third function, set_text(), its code could be part of show_slide().

In the next series, we’ll go further with a branching dialogue system.

The code

Here’s the complete code for Slideshow.gd.

extends Control

var slides := [
    {
        "text": "In this series, you'll code a slideshow.",
        "texture": preload("common/backgrounds/community_garden.jpg"),
    },
    {
        "text":
        "You can use the techniques you'll see here for simple cut-scenes, linear conversations, and more.",
        "texture": preload("common/backgrounds/community_garden.jpg"),
    },
    {
        "text":
        "What you'll see here isn't limited to conversations. You'll learn the basics behind any sequence of events.",
        "texture": preload("common/backgrounds/dani_bedroom.jpg"),
    },
    {
        "text":
        'We will build upon this series to later create a branching dialogue system, a sort of "choose your own adventure" toy.',
        "texture": preload("common/backgrounds/dani_bedroom.jpg"),
    }
]

var slide_index := 0
var last_slide_index := slides.size() - 1

onready var label := $VBoxContainer/Label
onready var button := $VBoxContainer/Button

onready var texture_rect := $VBoxContainer/TextureRect
onready var tween := $Tween


func _ready() -> void:
    button.connect("pressed", self, "advance")
    show_slide(0)


func show_slide(new_slide_index: int) -> void:
    slide_index = new_slide_index

    var slide: Dictionary = slides[slide_index]
    set_text(slide["text"])
    texture_rect.texture = slide["texture"]
    if slide_index == last_slide_index:
        button.text = "Quit"


func advance() -> void:
    # If the animation is still in progress, we seek to the end.
    if tween.is_active():
        tween.seek(INF)
    # Otherwise, we run through our previous condition.
    else:
        if slide_index == last_slide_index:
            get_tree().quit()
        else:
            show_slide(slide_index + 1)


func set_text(new_text: String) -> void:
    label.text = new_text
    # We want the text to display at a fixed rate. This division ensures that
    # the text will appear at a rate of 60 characters per second.
    var duration := new_text.length() / 60.0
    tween.interpolate_property(label, "percent_visible", 0.0, 1.0, duration)
    tween.start()