In this lesson, we will start adding visual feedback for the player. We will add nice sprites that pop up and indicate how much score you earned by tapping a hit beat.
In the next lesson, we will design a shrinking ring to indicate the perfect timing to tap one of the many buttons on the screen.
Let’s get started with the scoring sprites.
We will first create a scene that contains a sprite and particles.
Create a new scene with a Position2D
node at the root named VFXScore. You can save it in res://RhythmGame/VFX/
.
We want to display a sprite, animate it appearing, and optionally display particles when the player makes a perfect score.
So add a Sprite node, an AnimationPlayer, and a Particles2D node to the scene.
Assign the VFX/hit_sprites.png
image to the sprite’s Texture property. It contains four frames, so we set the Animation -> Hframes to 4
.
By default, you should see the “miss” image.
Let us now design the appearing animation.
To do so, you want to select the AnimationPlayer and create a new animation named “show”.
This animation should last 1.5
seconds and be set to autoplay, so it plays as soon we add a new VFXScore
instance to the scene tree.
We’ll animate the sprite’s Scale and Modulate properties to make it pop as it appears.
For the scale, I created three keyframes at the start of the animation so the sprite would briefly scale up and back down. This brief scale animation lasts only 0.2
seconds.
The first and third keyframes both have a value of (1, 1)
while the second has a value of (1.3, 1.3)
, causing the sprite to scale up.
For the modulate color, I made the sprite fade out starting from the 0.7
second mark to the end of the animation.
As a result, the sprite has a short scale animation and stays visible for a while before fading out.
This gives the player plenty of time to get visual feedback on their timing.
We also want to free the instance at the end of the animation automatically.
To do so, click Add Track -> Call Method Track and add a method track targeting the VFXScore node.
Add a key at the end of the animation calling the queue_free
method.
Your animation should look like the setup below.
Before coding the script, we want to design the particles. They are here to differentiate a perfect score from other sprites.
Select the Particles2D node, and let’s start by setting its main properties.
We’re going to have a burst of orbs radiating from the emission point.
To do so, we want to:
Set the Amount to around a dozen. I went with 12
. That’s the number of particles that’ll spawn simultaneously.
In the Time section,
1
. This is what makes all the particles spawn at the same time.1
to slightly randomize the particles’ spawn time.You can finally set the Textures -> Texture to sparkle.png
, a glowing circle.
Now, we need to assign a shader to the particles to control their animation. Set the Process Material -> Material to a new ParticlesMaterial
(like every other material in Godot, it relies on a built-in shader).
Click the resource to expand it.
You can use the following settings to make the orbs radiate from the system’s origin:
180
, so particles spawn going in every direction.250
pixels per second. This gives the particles a good initial speed.0.5
turns per second. As the name suggests, this property makes the particles circle around the emitter’s position.250
pixels per second. The property makes the particles slow down to a halt in one second.0.2
and assign a new CurveTexture
to the Scale Curve. I designed the curve to make the particles shrink as they reach the end of their lifetime.If you click the Emitting property at the top of the inspector, you should see a burst of particles radiating from the omission point. You can click the checkbox anytime to test the particles’ look.
The next step is to attach a script to the VFXScore node.
Depending on the score the player gained tapping a given hit beat, we want to change the sprite’s frame and possibly play the particles’ animation.
Add a script to the root node with the following code.
extends Position2D # We define three constants to name the other three available frames in our # sprite sheet. # We don't include the "miss" image as it's the one displayed by default and we # don't need to set it from the script. const FRAME_OK = 1 const FRAME_GREAT = 2 const FRAME_PERFECT = 3 onready var _sprite := $Sprite onready var _particles := $Particles2D # Setting the node as top-level makes its transform independent from its parent # node. This means we can place it anywhere in the scene tree, and its position # will always be global, that is, relative to the world's origin. func _ready() -> void: set_as_toplevel(true) # This function positions the node and maps a given score to the corresponding # sprite. func setup(global_pos: Vector2, score : int) -> void: global_position = global_pos if score >= 10: _sprite.frame = FRAME_PERFECT # When the player makes a perfect score, we also emit particles. _particles.emitting = true elif score >= 5: _sprite.frame = FRAME_GREAT elif score >= 3: _sprite.frame = FRAME_OK
Back to our RhythmGameDemo
scene, we hook on the Events.scored
signal and instantiate our VFXScore
scene in the signal callback.
Attach a new RhythmGameDemo.gd
script to the game scene’s root node.
extends Node2D # Using the Inspector, we provide our source scene to instantiate it when the # player taps a button. export var sprite_fx: PackedScene # We connect to the `Events.scored` signal. func _ready() -> void: Events.connect("scored", self, "_create_score_fx") # The `scored` signal comes with a `msg` dictionary which gives us the global # position of the button the player touched and how many points they gained. # We create a new instance of our VFX sprite, position it, add it as a child. # We change its sprite by calling its `setup()` function. func _create_score_fx(msg: Dictionary) -> void: var new_sprite_fx := sprite_fx.instance() # We need to the instance to the tree first, otherwise, we'll get an error. # This is because the call to `setup()` accesses child nodes. add_child(new_sprite_fx) new_sprite_fx.setup(msg.position, msg.score)
The last thing we have to do is assign a scene to instantiate to the RhythmGameDemo node’s Sprite Fx property. Drag and drop VFXScore.tscn
onto it.
You can now test the game and tap the hit beats in rhythm to see the sprites appear.
It’s still quite difficult to know when to click the buttons, so you’ll likely see a majority of “miss”. Which is why, in the next lesson, we will design a ring that will slowly shrink and tell us exactly when to tap.
Here are the new scripts added in this lesson.
VFXScore.gd
extends Position2D const FRAME_OK = 1 const FRAME_GREAT = 2 const FRAME_PERFECT = 3 onready var _sprite := $Sprite onready var _particles := $Particles2D func _ready() -> void: set_as_toplevel(true) func setup(global_pos: Vector2, score : int) -> void: global_position = global_pos if score >= 10: _sprite.frame = FRAME_PERFECT _particles.emitting = true elif score >= 5: _sprite.frame = FRAME_GREAT elif score >= 3: _sprite.frame = FRAME_OK
RhythmGameDemo.gd
extends Node2D export var sprite_fx: PackedScene func _ready() -> void: Events.connect("scored", self, "_create_score_fx") func _create_score_fx(msg: Dictionary) -> void: var new_sprite_fx := sprite_fx.instance() add_child(new_sprite_fx) new_sprite_fx.setup(msg.position, msg.score)