In the previous lesson, we set up our track menu. In this one, we’ll populate our TrackTiles with multiple tracks to pick from.
And to do so, we need a list of track and corresponding metadata, like which audio file corresponds to a given track, its BPM, icon, and so on.
We’ll create a custom resource type, TrackData
, and store instances of it in an array on the TrackTiles node.
In the RhythmGame/Tracks
directory, right-click to create a new script and name it TrackData.gd
.
There, we define several exported properties, which we’ll edit in the Inspector. We will create a TrackData
resource for each track and store it in the TrackTiles node.
We also define a function to convert the resource to a dictionary as this is the data format our rhythm game expects.
class_name TrackData extends Resource ## The track's title. export var label := "Track Name" ## The audio file for this soundtrack. export (String, FILE, "*.ogg") var stream ## The track's Beats Per Minute. export var bpm := 1 ## A texture to display in the track selection menu. export var icon: Texture ## The track's author. export var artist := "Artist" ## Converts the properties to a dictionary, to pass the track's data to the rhythm game. func as_dict() -> Dictionary: return {"name": label, "stream": stream, "bpm": bpm, "icon": icon, "artist": artist}
With the script ready, we create a resource for each of our available tracks.
In the Cephalopod/
directory, create a new resource of type TrackData
. Double-click the file to open it in the Inspector and fill all five fields.
To copy the Stream’s file path, you can right-click Cephalopod.ogg
and select Copy Path.
You can repeat the operation for any other soundtrack you might have worked on.
Next, we want to display the available tracks to the player. To do so, we will generate a row of track tile instances.
First, you want to delete the existing TrackTile instance we had as a child of the TrackTiles node.
Your scene tree should now look like this.
Then, attach a new script to the TackTiles node with the following code.
extends Node2D # We will add an array of `TrackData` resources. Each resource will lead to # displaying one tile on the screen. export (Array, Resource) var tracks: Array # The `TrackTile` scene, which we will instantiate once for each element in the # `tracks` array. export var track_tile_scene: PackedScene # The horizontal separation between the icons in pixels. export var separation := 450 # We use the following property to keep track of `TrackTile` instances. # We'll need it in the next lesson to implement motion var _track_tiles := [] # When the node's ready, we generate tiles. # In the next lesson, we'll also scale them and tweak their opacity. func _ready() -> void: _generate_tiles() # This function generates track tile instances and places them in a row. func _generate_tiles() -> void: # We offset each tile's position by this value. var offset := 0.0 for i in tracks.size(): # Our `tracks` array contains a list of `TrackData` resources, which we # individually store in the instanced `TrackTile` below. var track_data: TrackData = tracks[i] # We create and track tile instance for each soundtrack. var track_tile: TrackTile = track_tile_scene.instance() # We store the `TrackData` in the tile, as, moving forward, we will need # to retrieve it from the UITrackSelector's script to play the game. You # will see how in the next lesson. track_tile.track_data = track_data _track_tiles.append(track_tile) # For each instance, we increment the offset to separate the various # track icons. track_tile.position = Vector2(offset, 0) offset += separation add_child(track_tile)
Above, we use a currently undefined TrackTile
type. We need to add a script to the TrackTile
node next to define the track_data
property.
Open the scene TrackTile.tscn
and attach a new script to the root node.
class_name TrackTile extends Area2D var track_data: TrackData setget set_track_data # When updating the `track_data` property, we assign its icon to the sprite. func set_track_data(value: TrackData) -> void: track_data = value $Sprite.texture = track_data.icon
With that, you can head back to the UITrackSelector
scene, select TrackTiles, and drag the TrackTile.tscn
file onto its Track Tile Scene property.
In case the exported properties don’t appear and you see an error in the Output bottom panel, try restarting the editor.
We also want to expand the Tracks array and drag-and-drop the TrackData
resources we created onto it, like cephalopod.tres
.
In case you only have one, you can add it to the array multiple times to see the code in action.
You can now run the scene to see your tiles automatically generated.
We’ll improve the tiles’ look in the next lesson.
In this series, we register and load the available tracks manually.
If you’re going to commercialize such a game, you might want to write code that automatically detects and loads available songs, like games like Osu! and Stepmania do.
Here, we focused interested in showing you how to implement time-sensitive gameplay and the touch interface.
We still lack audio playback and the ability to start the game. Let’s add a script to UITrackSelector to do so.
extends Control # We store the currently selected track's data to have it handy when the # player presses the go button. var _current_track_data: TrackData onready var _track_name := $TrackName onready var _stream := $AudioStreamPlayer onready var _animation_player := $AnimationPlayer # This function updates the track name displayed on screen and plays it # soundtrack, using our fade animations for smooth playback. func update_track_info(track_tile: TrackTile) -> void: _current_track_data = track_tile.track_data _track_name.text = _current_track_data.label # Here is where we play the animations. We first play `fade_out_track`, wait # for it to finish using `yield`, then load the new track, play it starting # at 30 seconds, and fade it in. _animation_player.play("fade_out_track") yield(_animation_player, "animation_finished") _stream.stream = load(_current_track_data.stream) _stream.play(30.0) _animation_player.play("fade_in_track")
We can add a temporary call to update_track_info()
in the _ready()
function to test sound playback. We will call update_track_info()
through a signal in the next lesson.
# Be sure to remove this temporary code after testing # Here, we manually grab the first `TrackTile` scene instance. func _ready() -> void: update_track_info($TrackCarousel/TrackTiles.get_child(0))
To wrap this up, we connect the GoButton’s pressed
signal to the root node.
In the callback function, we emit a new Events.track_selected
signal, which will cause the game to start.
Then, we delete the interface to prevent any more interaction with it.
func _on_GoButton_pressed() -> void: Events.emit_signal("track_selected", _current_track_data.as_dict()) queue_free()
Finally, we add the signal to Events.gd
.
signal track_selected(msg)
We now want to instantiate our UITrackSelected
interface in the RhythmGameDemo
scene as part of the UI canvas layer.
Open the RhythmGameDemo
scene and add an instance of the UITrackSelector
scene as a child of UI.
Make it visible, and hide UITrackPlaying. This way, the game will start with the track selection menu visible.
Currently, the Synchronizer
script starts playing a soundtrack automatically. We have to update its code to wait for the player to press the “Go” button.
Open Synchronizer.gd
and replace the _ready()
function’s code to listen to the Events.track_selected
signal. We also add a new _load_track()
function that sets up the BPM and other values received with the signal, before calling play_audio()
.
func _ready() -> void: Events.connect("track_selected", self, "_load_track") func _load_track(msg: Dictionary) -> void: _stream.stream = load(msg.stream) bpm = msg.bpm _bps = 60.0 / bpm _hbps = _bps * 0.5 play_audio()
You can now play the selected track by playing the game and clicking on “Go”.
Of course, we cannot change the selected track by clicking and dragging or using our finger on a touchscreen.
We will code this feature in the next lesson. See you there!
Here are all the new code files added in this lesson.
TrackData.gd
class_name TrackData extends Resource export var label := "Track Name" export (String, FILE, "*.ogg") var stream export var bpm := 1 export var icon: Texture export var artist := "Artist" func as_dict() -> Dictionary: return {"name": label, "stream": stream, "bpm": bpm, "icon": icon, "artist": artist}
TrackTiles.gd
extends Node2D export (Array, Resource) var tracks: Array export var track_tile_scene: PackedScene export var separation := 450 var _track_tiles := [] func _ready() -> void: _generate_tiles() func _generate_tiles() -> void: var offset := 0.0 for i in tracks.size(): var track_data: TrackData = tracks[i] var track_tile: TrackTile = track_tile_scene.instance() track_tile.track_data = track_data _track_tiles.append(track_tile) track_tile.position = Vector2(offset, 0) offset += separation add_child(track_tile)
TrackTile.gd
class_name TrackTile extends Area2D var track_data: TrackData setget set_track_data func set_track_data(value: TrackData) -> void: track_data = value $Sprite.texture = track_data.icon
UITrackSelector.gd
extends Control var _current_track_data: TrackData onready var _track_name := $TrackName onready var _stream := $AudioStreamPlayer onready var _animation_player := $AnimationPlayer func update_track_info(track_tile: TrackTile) -> void: _current_track_data = track_tile.track_data _track_name.text = _current_track_data.label _animation_player.play("fade_out_track") yield(_animation_player, "animation_finished") _stream.stream = load(_current_track_data.stream) _stream.play(30.0) _animation_player.play("fade_in_track") func _on_GoButton_pressed() -> void: Events.emit_signal("track_selected", _current_track_data.as_dict()) queue_free()
Events.gd
extends Node signal beat_incremented(msg) signal scored(msg) signal track_finished signal track_selected(msg)
Synchronizer.gd
extends Node export var bpm := 124 var _bps := 60.0 / bpm var _hbps := _bps * 0.5 var _last_half_beat := 0 onready var _stream := $AudioStreamPlayer func _ready() -> void: Events.connect("track_selected", self, "_load_track") func play_audio() -> void: var time_delay := AudioServer.get_time_to_next_mix() + AudioServer.get_output_latency() yield(get_tree().create_timer(time_delay), "timeout") _stream.play() func _process(_delta: float) -> void: var time: float = ( _stream.get_playback_position() + AudioServer.get_time_since_last_mix() - AudioServer.get_output_latency() ) var half_beat := int(time / _hbps) if half_beat > _last_half_beat: _last_half_beat = half_beat Events.emit_signal("beat_incremented", {"half_beat" : half_beat, "bps": _bps}) func _load_track(msg: Dictionary) -> void: _stream.stream = load(msg.stream) bpm = msg.bpm _bps = 60.0 / bpm _hbps = _bps * 0.5 play_audio()