Animated scoring sprites

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.

Setting up the scene

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.

The appearing animation

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.

Designing the particles

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:

  1. Set the Amount to around a dozen. I went with 12. That’s the number of particles that’ll spawn simultaneously.

  2. In the Time section,

  3. 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:

  1. As we’re making 2D particles, ensure that Flags -> Disable Z is checked.
  2. Set the Direction -> Spread to 180, so particles spawn going in every direction.
  3. Increase the Initial Velocity -> Velocity to 250 pixels per second. This gives the particles a good initial speed.
  4. Set the Orbit Velocity -> Velocity to 0.5 turns per second. As the name suggests, this property makes the particles circle around the emitter’s position.
  5. Set Damping -> Damping to 250 pixels per second. The property makes the particles slow down to a halt in one second.
  6. The particles are also huge by default, so we set the Scale -> Scale to 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.

Setting the right sprite with code

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.

Code reference

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)