Scene fade animations

In this lesson, we will add some more fade animations to the game.

When testing the previous lesson, we saw that we needed to make the text box fade in for the scene to play as expected. We will define an animation that fades the scene from black and then makes the text box appear using a function. We will also add the corresponding command to the ScenePlayer’s run_scene() function to trigger scene fades from data.

Let’s start by designing the fade animations.

Fading the scene from and to black

To globally fade the scene, we could animate the scene player’s Modulate property. It does the job but doesn’t look too great.

In this image, notice how you can see the window’s frame behind Sophia’s body.

This happens because the Modulate property does not affect the image globally. Instead, it modulates every child individually. So when you get the opacity down to 50%, every sprite is half-transparent, and you can see the background through the characters.

The solution to fade to black is instead to add an extra node that will cover the entire game.

To do that, in the scene player scene, add a ColorRect as a child of the ScenePlayer. It should be at the bottom of the scene to appear in front of everything.

Apply Layout -> Full Rect to it so it spans the whole viewport. Then set its Color to pure black.

We can then modify its Modulate alpha or directly animate its Color to make it opaque or transparent.

We add an AnimationPlayer to the scene. In the final project, I called it FadeAnimationPlayer.

In it, we create two animations: fade_in and fade_out. They both only have two keys that target the ColorRect node.

The fade_in animates the Modulate color from an opaque white to a transparent one. It fades the scene in from black by making the ColorRect disappear.

The fade_out does that in reverse; it animates the Modulate property from a transparent white to an opaque one.

I set both animations to a duration of 0.7 seconds. A one-second fade, the default animation length, is a little too slow to my taste.

I also invite you to create a new setup animation that resets the ColorRect’s Modulate to an opaque white, so the screen is entirely black by default. You also want to turn on the auto-play button to the right of the animation drop-down menu. This way, the value will reset automatically at the start of the game.

Adding transitions to the ScenePlayer

With that, we can update the ScenePlayer’s code:

  1. We define two functions to make the scene and text box appear and disappear.
  2. We add a new transition command that teammates can trigger from the scene data.

Open ScenePlayer.gd and define a new transition_finished signal and the following functions.

The two functions below animate the color rectangle and the text box. They pause until the end of every animation using yield() to play them sequentially.

## Emitted when a transition animation, like a fade in or fade out of the whole
## scene ended. This allows the node to wait for its animations to finish before
## triggering other events.
signal transition_finished

onready var _anim_player: AnimationPlayer = $FadeAnimationPlayer


# You can use functions and `yield()` to play animations in sequence.
func _appear_async() -> void:
    _anim_player.play("fade_in")
    yield(_anim_player, "animation_finished")
    yield(_text_box.fade_in_async(), "completed")
    # We'll use the signal in a second in `run_scene()`
    emit_signal("transition_finished")


func _disappear_async() -> void:
    yield(_text_box.fade_out_async(), "completed")
    _anim_player.play("fade_out")
    yield(_anim_player, "animation_finished")
    emit_signal("transition_finished")

The two functions are mostly the same but in reverse. The “appear” transition first fades the scene in, then shows the text box. The disappear function fades the text box out first, then the scene.

Triggering transitions with data

To trigger these transitions from the scene’s data, we want to define some simple identifier that maps to our function names. If we ever rename _appear_async(), we don’t want to change the data.

The data should be of the form {"transition": "animation_name"}.

We define a new constant that maps an identifier to a function name. I’ve chosen fade_in and fade_out.

If a scene node contains a key named transition, we will use the call() function to call our _appear_async() and _disappear_async() functions.

## Maps keys from a scene node to a corresponding transition animation function
## to call.
const TRANSITIONS := {
    fade_in = "_appear_async",
    fade_out = "_disappear_async",
}

We then update the run_scene() function to play transitions.

Add the following code after the if "line" in node: block.

#func run_scene() -> void:
    #...
    #while key != KEY_END_OF_SCENE:
        #if "line" in node:
            #...

        elif "transition" in node:
            # Here is where we call one of the transition functions using the
            # built-in call() function. It uses the name of a function as a text
            # string and calls it.
            call(TRANSITIONS[node.transition])
            yield(self, "transition_finished")
            key = node.next
        #else:
        #   key = node.next

Using an elif block, we either display a line of text or play a scene transition. We don’t allow for both simultaneously.

To test the code, you can add two nodes to _scene_data with the transition key, like so.

var _scene_data := {
    3: {
        transition = "fade_in",
        next = 0
    },
    0: {
        character = "sophia",
        side = "left",
        animation = "enter",
        line = "Hi there! My name's Sophia.",
        next = 1
        },
    1: {
        character = "dani",
        side = "right",
        animation = "enter",
        next = 2
        },
    2: {
        character = "dani",
        line = "Hey, I'm Dani.",
        next = 4
        },
    4: {
        transition = "fade_out",
        next = -1
        }
    }

Notice how the keys aren’t in order anymore, and this doesn’t cause any issues. If you were to create an editor for this kind of data, you could let the engine generate unique integers for your keys, using String.hash(), or whatever function. But as we’re writing them by hand, for now, plain numbers are convenient.

To finish the test, we call run_scene() in the _ready() callback.

func _ready() -> void:
    run_scene()

And this is it for this lesson. In the next one, we will design and code the choice selector. It will display choices directly in the text box.

Code reference

Here is the complete ScenePlayer.gd so far.

class_name ScenePlayer
extends Node

signal scene_finished
signal transition_finished

const KEY_END_OF_SCENE := -1
const TRANSITIONS := {
    fade_in = "_appear_async",
    fade_out = "_disappear_async",
}

var _scene_data := {}

onready var _text_box := $TextBox
onready var _character_displayer := $CharacterDisplayer
onready var _background := $Background
onready var _anim_player: AnimationPlayer = $FadeAnimationPlayer


func run_scene() -> void:
    var key = _scene_data.keys()[0]
    while key != KEY_END_OF_SCENE:
        var node: Dictionary = _scene_data[key]

        var character: Character = (
            ResourceDB.get_character(node.character)
            if "character" in node
            else ResourceDB.get_narrator()
        )

        if "background" in node:
            var bg: Background = ResourceDB.get_background(node.background)
            _background.texture = bg.texture

        if "character" in node:
            var side: String = node.side if "side" in node else CharacterDisplayer.SIDE.LEFT
            var animation: String = node.get("animation", "")
            var expression: String = node.get("expression", "")
            _character_displayer.display(character, side, expression, animation)
            if not "line" in node:
                yield(_character_displayer, "display_finished")

        if "line" in node:
            _text_box.display(node.line, character.display_name)
            yield(_text_box, "next_requested")
            key = node.next
        elif "transition" in node:
            call(TRANSITIONS[node.transition])
            yield(self, "transition_finished")
            key = node.next
        else:
            key = node.next

    _character_displayer.hide()
    emit_signal("scene_finished")


func _appear_async() -> void:
    _anim_player.play("fade_in")
    yield(_anim_player, "animation_finished")
    yield(_text_box.fade_in_async(), "completed")
    emit_signal("transition_finished")


func _disappear_async() -> void:
    yield(_text_box.fade_out_async(), "completed")
    _anim_player.play("fade_out")
    yield(_anim_player, "animation_finished")
    emit_signal("transition_finished")