Currently, we have HitBeat
scenes that spawn randomly across the screen.
All the gameplay functionality is there; they spawn in time with the track’s beats-per-minute, the player can time their taps for a variable score, and we have nice visuals and feedback.
We just need to structure this chaos!
In this lesson, we’ll create a system within the Godot editor which we’ll use to design patterns for the player.
These patterns are made up of HitBeat
instances that appear at a specific time and position on the screen. We’ll then design a simple pattern for the Cephalopod track.
I’ll leave creating patterns for the other tracks to you.
We could create a custom-made plugin to use within Godot where we could draw the desired pattern. This would be a good investment if you decided to develop a commercial rhythm game.
But this is also time-consuming and beyond this tutorial’s scope. For now, Godot scenes will do just fine.
By designing patterns, we can make the player more engaged with the music. For example, we could design an ascending pattern in the editor that corresponds to the music rising in pitch.
Mirroring the music track in this way is what makes rhythm games so fun! It’s up to us to fit these HitBeat
patterns with the music, but we have to create the tools to do it first.
Thanks to the Synchronizer
, we already have a marker for each half-beat as a track plays.
To determine whether we instance a HitBeat
on any given half-beat, we’ll have a stack (array) of instructions stored in the HitSpawner
.
Every time the Events.beat_incremented
signal fires, the HitSpawner
will grab the next instruction and either instance a HitBeat
or do nothing.
We’ll also use scenes to visually design patterns and create unique stacks for each track. But first, let’s hard-code a simple pattern so you can see how this stack approach works.
The first thing we want to do is create and generate an array procedurally. Open up HitSpawner.gd
and add the following code.
The _generate_stack()
method is temporary, for testing purposes.
# An array of dictionaries representing instructions on what to spawn. var _stack_current = [] func _ready() -> void: #... # Add a temporary call to `_generate_stack()` _generate_stack() # Here, we populate the stack. # Every even half-beat, we add a color and position information. # On the odd half-beats, we add an empty dictionary to the stack. func _generate_stack() -> void: # We'll generate 5 buttons to tap aligned diagonally. for i in range(10): # An empty dictionary means "spawn nothing." var hit_beat_data = {} # Every even half-beat, we calcualte a new color and position. if i % 2 == 1: hit_beat_data.color = int(rand_range(0, 5)) hit_beat_data.global_position = i * Vector2(100, 100) # And we append every dictionary to the stack. _stack_current.append(hit_beat_data)
Now we have a stack, we’re going to rewrite _spawn_beat()
to pop dictionaries from the _stack_current
and process them. Replace the existing method with the following.
func _spawn_beat(msg: Dictionary) -> void: if not enabled: return # After we've processed everything in the stack, we disable the HitSpawner # We also emit a signal which we can use later to show an end screen if _stack_current.empty(): enabled = false Events.emit_signal("track_finished") return # pop_front() grabs the front dictionary from the stack and also removes it var hit_beat_data: Dictionary = _stack_current.pop_front() # If the dictionary has no global_position, we skip instancing if not hit_beat_data.has("global_position"): return # Otherwise we add extra information as we did before and instance a HitBeat hit_beat_data.bps = msg.bps hit_beat_data.half_beat = msg.half_beat var new_beat: Node = hit_beat.instance() add_child(new_beat) new_beat.setup(hit_beat_data)
We use a new signal on Events
in the code above. We have to define it in Events.gd
.
signal track_finished
If you run the game, you’ll see we’ve managed to remove random positions in favor of a simple pattern the player can interact with.
You can now delete the _generate_stack()
function and the call to it. We used it only to illustrate and test the new approach to spawning HitBeat
instances.
You could use code like this to programmatically create interesting patterns. To do so, you’d want to use functions like _generate_stack()
.
Instead, though, we’ll use scenes to do so visually moving forward. In rhythm games, while you can use procedural generation for gameplay, hand-crafted levels tend to work best.
Let’s think about how we’d represent timing in the Godot editor.
Say we want to instance a new HitBeat
every beat. The first four instances would look like this in musical notation, which we could represent in the editor using similar placeholder sprites.
While the sheet music notation only represents timing, the sprites over the purple background will allow us to store each note’s position relative to one another.
We also need a way to represent the empty dictionaries from our example. Musically, these are called rests. In music notation, we typically represent them with a zig-zagging symbol (the symbol depends on the rest’s duration).
Likewise, we’ll use a similar sprite to represent those.
Functionally, each placeholder rest will fill the stack with several empty dictionaries, based on how many half-beats we want the player to rest.
Below, you can see the symbol for half-beat rests, as well as notes that are one half-beat long, called half notes.
In the next lesson, we’ll create new PlacerHitBeat
and PlacerRest
scenes such that we can modify their duration and have placeholder sprites update automatically within the editor.
This will give us a good visual representation of musical bars at a glance. For example, we can see the above has a mix of whole and half nodes scenes while also having half-beat rests.
To order the notes, we’ll take advantage of Godot’s scene tree.
To illustrate this, let’s take the Cephalopod pattern scene in the finished rhythm game demo.
In the Scene dock, we have:
PlacerHitBeat
and PlacerRest
scenes.With this structure, we can systematically move down the children of Cephalopod and extract the information from each note or rest found there.
We split the track into chunks to make designing the overall track’s gameplay easier. That way, we can turn the visibility of entire sections of the track on or off.
Once we have all of our individual patterns for each track designed, we’ll instantiate them as children to a Patterns
scene which the HitSpawner
will use to gather and build the stack for each track.
Now we know how it will work, let’s get creating! In the next lesson, we’ll create the PlacerHitBeat
and PlacerRest
scenes and design a short pattern for the Cephalopod track.
HitSpawner.gd
extends Node export var enabled := true export var hit_beat: PackedScene var _stack_current = [] func _ready() -> void: Events.connect("beat_incremented", self, "_spawn_beat") func _spawn_beat(msg: Dictionary) -> void: if not enabled: return if _stack_current.empty(): enabled = false Events.emit_signal("track_finished") return var hit_beat_data: Dictionary = _stack_current.pop_front() if not hit_beat_data.has("global_position"): return hit_beat_data.bps = msg.bps hit_beat_data.half_beat = msg.half_beat var new_beat: Node = hit_beat.instance() add_child(new_beat) new_beat.setup(hit_beat_data)
Events.gd
extends Node signal beat_incremented(msg) signal scored(msg) signal track_finished