The trail we saw in the previous lesson works for a static sprite. What if we want a trail following an animated character?
Using particles, if we change their texture, the texture of every particle already emitted changes at once.
In this tutorial, we’re going to see how to make a ghost trail as in 2D Castlevania games.
You are going to learn to:
Let’s create a new scene with a Sprite as root, rename it to FadingSprite. Then add a Tween as its child.
Drag the alienGreen_stand.png image to the Texture as a placeholder. To give it a ghostly look, we can change the FadingSprite > Modulation color to a semi-transparent blue.
Time to code! Attach a script to the FadingSprite. We need the FadingSprite to fade over a lifetime
to full transparency then delete itself when the fade finishes.
Let’s export a lifetime
variable to set its value through the inspector and create a reference for the Tween; let’s call it _tween
.
extends Sprite export var lifetime := 0.5 onready var _tween := $Tween
Now for the fade, let’s encapsulate this behavior in a fade
method. It optionally receives a duration
argument, but it takes the lifetime
value by default.
For that, we can use the Tween and animate the FadingSprite > Modulation using the interpolate_property
. We need to store a transparent version of the FadingSprite > Modulation color to interpolate to. Then, using the duration
argument for the interpolation duration.
func fade(duration := lifetime) -> void: # Creates a full transparent color based on the Sprite's Modulation var transparent := modulate transparent.a = 0.0 # Interpolates from the current modulate to the transparent version _tween.interpolate_property(self, "modulate", modulate, transparent, duration) _tween.start() # Waits the interpolation finish then deletes itself yield(_tween, "tween_all_completed") queue_free()
To delete the sprite as soon as the _tween
finishes the interpolation, we can yield
until the _tween
emits its tween_all_completed
signal.
Since we want the FadingSprite to fade as soon as it enters the scene, we can call the fade()
method on the _ready()
callback.
# Sprite that automatically fades itself extends Sprite export var lifetime := 0.5 onready var _tween := $Tween func _ready() -> void: fade() func fade(duration := lifetime) -> void: # Creates a full transparent color based on the Sprite's Modulation var transparent := modulate transparent.a = 0.0 # Interpolates from the current modulate to the transparent version _tween.interpolate_property(self, "modulate", modulate, transparent, duration) _tween.start() # Waits the interpolation finish and deletes itself yield(_tween, "tween_all_completed") queue_free()
The FadingSprite is ready. Now, we need to spawn instances of it behind the player’s character.
For our effect, we need to create copies of FadingSprites every second to follow the character’s movement, just like in the Particles2D approach. We also want to control when ghosts can be created to sync them with the character’s movement and animations.
Create a new scene and add a Sprite as root. This is the sprite we are going to use for the player’s graphics. Rename it as AnimatedGhostTrail.
Add a Timer as a child of the AnimatedGhostTrail. We are going to use the Timer to spawn a new ghost, so let’s call it GhostSpawnTimer.
Let’s use the alienGreen_stand.png image as a placeholder for the AnimatedGhostTrail > Texture property as well.
Time to code again, attach a new GDScript to the AnimatedGhostTrail, and open it.
Start with a preload
of the FadingSprite PackedScene file at res://GhostTrail/FadingSprite.tscn
so we can instance it anytime in our script.
Using preload()
, we read the FadingSprite PackedScene file from disk and have it ready at compile-time, unlike load()
which fetches the resource at run-time when the line executes. To prevent performance issues, it’s a good practice to store Resources in constants and load them with preload()
whenever possible.
Then, export
an int
variable so we can set the amount
of ghosts we want to spawn per second. Also, export
a bool
variable, so we can tell if when the AnimatedGhostTrail is_emitting
and create a reference for the GhostSpawnTimer, call it _timer
.
# Spawns copies of FadingSprites per second extends sprite const FadingSprite: PackedScene = preload("res://GhostTrail/FadingSprite.tscn") # The number of ghosts instanced per second export var amount := 10.0 # If `true`, creates `amount` instances of FadingSprite every second export var is_emitting := false setget set_is_emitting onready var _timer := $GhostSpawnTimer
The AnimatedGhostTrail main behavior is to create FadingSprite instances that mirror the current AnimatedGhostTrail properties.
In Godot, we can instance scenes using the PackedScene.instance()
method. Since the FadingSprite
constant, we’ve created a PackedScene we can call instance()
on it and store it in a new ghost
variable.
func _spawn_ghost() -> void: # Creates an instance of the FadingSprite PackedScene var ghost := FadingSprite.instance()
Now we are going to sync the new ghost
properties to the AnimatedGhostTrail’s. The most important one is its texture.
func _spawn_ghost() -> void: # Creates an instance of the FadingSprite PackedScene var ghost := FadingSprite.instance() # Sync ghost instance's properties to match AnimatedGhostTrail's ghost.texture = texture ghost.offset = offset ghost.flip_h = flip_h ghost.flip_v = flip_v ghost.global_position = global_position
In Godot, you need to add a node to the scene tree manually. To add the ghost
to the scene, we need to make it a child of another node. The best choice at this point is the AnimatedGhostTrail itself, so let’s call add_child(ghost)
after syncing the ghost
properties.
To keep the ghosts
in place as the AnimatedGhostTrail moves we need to prevent the AnimatedGhostTrail transform from affecting the ghost
. For that we can call set_as_toplevel(true)
on each every new ghost
as soon as we add it to the SceneTree. The _spawn_ghost
method should look like this:
func _spawn_ghost() -> void: # Creates an instance of the FadingSprite PackedScene var ghost := FadingSprite.instance() # Sync ghost instance's properties to match AnimatedGhostTrail's ghost.texture = texture ghost.offset = offset ghost.flip_h = flip_h ghost.flip_v = flip_v ghost.global_position = global_position # Adds the synced ghost to the scene add_child(ghost) # Prevents AnimatedGhostTrail's transform from affecting the `ghost` ghost.set_as_toplevel(true)
Now is time to design how the AnimatedGhostTrail triggers the _spawn_ghost
behavior.
Every time we set is_emitting
to true
, we also start the _timer
. It should timeout amount
times per second. We also spawn a new ghost as soon as is_emitting
becomes true
.
The _timer
should stop when is_emitting
is false
. Let’s encapsulate this behavior in the set_is_emitting
method to ensure this always happens the AnimatedGhostTrail > Is Emitting changes.
func set_is_emitting(value: bool) -> void: is_emitting = value if not is_inside_tree(): yield(self, "ready") if is_emitting: _spawn_ghost() # Sets the `_timer` to timeout `amount` times per second _timer.start(1.0 / amount) else: _timer.stop()
When the _timer
emits is timeout
signal we need spawn a new ghost. So let’s connect the GhostSpawnTimer timeout
signal to the _on_Timer_timeout
callback:
func _on_GhostSpawnTimer_timeout():
_spawn_ghost()
And with that, we have our AnimatedGhostTrail working. You can check out the complete AnimatedGhostTrail script in the GhostTrail/AnimatedGhostTrail.gd file.
In our demo scene, we set the AnimatedGhostTrail > Is Emitting to true
whenever the character jumps, and we set it to false
when the character reaches the floor. But since the Is Emitting is available in the Inspector dock, we can create animations that toggle it as we want to use keyframes.