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.
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.
With that, we can update the ScenePlayer
’s code:
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.
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.
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")