Creating patterns

It’s time to put the theory into practice and make some patterns! In this lesson, we’ll:

Let’s get to it.

Placer scenes

First up, we’ll create the scene to represent HitBeat placeholders.

Create a new scene with a Node2D as the root and name it PlacerHitBit. Add a Sprite and a LabelCustom instance named OrderNumber. We’ll use it to display the order in which the elements will play in the editor.

Save it as res://RhythmGame/Editor/PlacerHitBeat.tscn and attach a script to the root node.

We will add custom scene tree icons to our placer scenes to make it easier to see which placer scenes are in the scene tree.

Here’s a screenshot from the final project so you can see how the different placer scene instances will look.

To give a node a custom icon like so, you need to attach a script to it, define its class name with the class_name keyword, and provide a path to an image.

Let’s dive into the PlacerHitBeat.gd to add the icon and some properties. Open the root node’s script and give it the following code.

# We use the `tool` mode to run this script's code in the editor.
tool
extends Node2D
# Here's how you give nodes a custom icon. You define the class name, add a
# comma, and provide a path to an image file.
class_name PlacerHitBeat, "res://RhythmGame/Editor/placer_hit_beat_icon.svg"

# This property holds the scene to instance instead of the placeholder at
# runtime. For the PlacerHitBeat, this will be the HitBeat scene.
# You could make this a constant instead.
# I went with an exported variable to avoid hard-coding the path.
export (PackedScene) var scene
# The note's duration in half-beats. Any number greater than 1 will add rests to
# the stack to delay the next instruction.
export (int, 1, 4) var duration := 2 setget set_duration

# The number to display in the editor and on the instanced HitBeat at runtime.
var _order_number := 1


# This virtual function is called by Godot every time the node enters the scene
# tree.
func _enter_tree() -> void:
    # There, we set the node's order and update the label's text.
    # The `Node.get_index()` tells us a node's position in the scene tree
    # relative to other siblings.
    # It starts at `0`, which is why we add `1` to the value.
    _order_number = get_index() + 1
    $OrderNumber.text = str(_order_number)


# All placer scenes will have this function. The HitSpawner will call it to get
# a dictionary to add to the stack.
func get_data() -> Dictionary:
    return {
        scene = scene,
        order_number = _order_number,
        global_position = global_position,
        duration = duration
    }


# Whenever we set the duration, we also want to update the sprite's frame.
func set_duration(amount: int) -> void:
    duration = amount
    $Sprite.frame = duration - 1

If the node’s icon doesn’t update in the Scene dock, you may have to restart the editor.

Select the PlacerHitBeat and assign the HitBeat scene to its Scene property in the Inspector.

For the Sprite node, assign the image icon_beats.png to its Texture. The image is a sprite sheet with four frames, so we set its Animation > Hframes to 4.

We’ll update the frame based on the duration of the PlacerHitBeat.

The sprites are based on musical notation and represent a duration of:

  1. Half-beat.
  2. One beat.
  3. One and a half beat.
  4. Two beats.

That’s the PlacerHitBeat done. Next, we’ll create a similar scene to represent rests.

Creating a scene for rests

The PlacerRest scene is very similar to the PlacerHitBeat.

To save time creating it, find the PlacerHitBeat.tscn file in the FileSystem dock and duplicate it.

Name the new file PlacerRest and open it.

Set the Sprite node’s Texture to placer_rest_icon.svg.

Then, remove the script from the PlacerRest node and attach a new script to it.

Here’s the complete script. The notable differences are:

Aside from that, the code’s similar to PlacerHitBeat.gd, we could even use a parent class for it.

tool
extends Node2D
class_name PlacerRest, "res://RhythmGame/Editor/placer_rest_icon.svg"

export (int, 1, 4) var duration := 2 setget set_duration

var _order_number := 1


func _enter_tree() -> void:
    _order_number = get_index() + 1
    $OrderNumber.text = str(_order_number)


func get_data() -> Dictionary:
    return {duration = duration}


func set_duration(amount : int) -> void:
    duration = amount
    $Sprite.frame = duration - 1

Designing a simple pattern

Now we have the pieces we need, let’s get creative and design a pattern for the Cephalopod track.

Create a new scene with a Node2D as the root. Save it as res://RhythmGame/Tracks/Cephalopod/Cephalopod.tscn.

We keep anything related to a track in its folder along, including this pattern. This makes modifying and finding things easier in larger projects.

As we talked about in the previous lesson, we want patterns to be made up of chunks of PlacerHitBeat and PlacerRest scenes, resulting in a tree structure like the following:

Above, I’m using several Node2D to group parts of the track and quickly toggle their visibility. We’ll ignore them when reading the scene and only take the PlacerHitBeat and PlacerRest instances into account.

Create several Node2D children. You can name them however you want, or use numbers, like the above.

We’ll use the scene hierarchy to play the beats anyway.

Although not strictly needed, I’m going to use the editor’s snapping functionality to help me position placers uniformly.

You can turn on grid snapping by clicking the icon in the top-left or by pressing Shift G (in Godot 3.3).

I’m only concerned about using a grid, so I’ll disable all the smart snapping features.

Then we can open the Configure Snap… window to set up grid snapping.

Having a grid of 96 pixels gives some padding to the HitBeat scenes, which have a radius of 75 pixels.

It also allows us to cluster placers as in the following example, which I like to do for shorter duration HitBeat scenes as the player will need to tap them quickly.

Now we have our snapping set up, we can work on designing chunks.

Under the first Node2D, which I named Intro, add two PlacerRest scene instances as children.

The rests’ position doesn’t matter, only the duration and order within the scene tree to generate a pattern stack correctly.

These two scenes will represent the track’s start before we want HitBeat scenes to appear. We need to give the player some time to listen to the track and get a sense for the rhythm before giving them beats to tap.

Set the duration of the PlacerRest scenes to 4 and 3, respectively. This will give us seven half-beats before the next chunk.

The HitSpawner will add an extra half-beat delay giving us a complete bar.

Note: also remember that our HitBeat needs a delay of four whole-beats before they should be tapped, to display the animated ring.

Hide the Intro node, and we’ll move on to the remaining chunks.

All other chunks will have a total of 16 worth of half-beats or 8 beats. This represents two bars of music and is enough to work with to create some interesting patterns while also not cluttering up the screen.

Here’s an example of my 0 chunk.

In the editor, you’ll see a blue line representing the 1920 by 1080 screen we defined in the project settings. I’ve highlighted it with yellow to make it more obvious. As long as placer scenes are in this area, they’ll appear in the game.

Looking down the scene tree, we have:

This setup will result in the following gameplay sequence:

Continue designing as many chunks as you’d like before we move on to loading the patterns.

I also invite you to add patterns to any other tracks by creating a dedicated scene for them.

Generating stacks from patterns

We have our patterns, but to have them display in-game, now we need to turn the scenes’ content into data for the the HitSpawner to process.

First, we’ll create a scene to keep all the patterns in one place. Create a new Node2D scene and name the root node Patterns. Save it as res://RhythmGame/Editor/Patterns.tscn.

Instance each pattern you created as children, like Cephalopod.tscn, save the scene, then go to the RhythmGameDemo scene.

Instance the Patterns scene as a child of HitSpawner so it can easily access it.

Open up HitSpawner.gd and let’s make some changes.

# We add a dictionary to store each stack of instructions.
# Doing so allows us to pair the stack with the name of the track, which is a
# string.
var _stacks = {}

onready var patterns = $Patterns


func _ready() -> void:
    # ...
    # We generate all of the stacks at the start of the game.
    _generate_stacks()
    _select_stack({name : "Cephalopod"})


# Before, we had one track, but now we select the correct stack from the _stacks
# dictionary. In the next lesson we'll create a track selection UI which will
# connect to this method which is why it has a dictionary as a parameter.
func _select_stack(msg: Dictionary) -> void:
    _stack_current = _stacks[msg.name].stack

Before we look at the _generate_stacks() function, let’s think about what we want it to do.

For each pattern we’ve created, we want to look at each chunk. We add data from each PlacerHitBeat and PlacerRest to the track’s stack for each chunk.

When the duration of a PlacerHitBeat or PlacerRest is greater than 1, we need to add empty instructions to the stack so the next instruction is processed at the correct time.

If we didn’t do this, we’d have HitBeat scenes instanced every half-beat, no matter what their duration was.

# Reads each child of the Pattern node and gets an array of data from them.
func _generate_stacks() -> void:
    for pattern in patterns.get_children():
        # Create a new key pair in the _stacks dictionary.
        _stacks[pattern.name] = []
        for chunk in pattern.get_children():
            # We assign a random color to each section of the track.
            var sprite_frame := round(rand_range(0, 5))
            # We get data from each placer instance and append it to the stack.
            for placer in chunk.get_children():
                var hit_beat_data: Dictionary = placer.get_data()
                hit_beat_data.color = sprite_frame
                _stacks[pattern.name].append(hit_beat_data)

                # Add additional rests if needed.
                for _i in range(hit_beat_data.duration - 1):
                    _stacks[pattern.name].append({})

    # Free the patterns scene as it's not needed in-game.
    patterns.queue_free()

The last thing we’ll do is alter HitBeat.gd to show the order_number instead of the half-beat it spawned on.

In HitBeat.setup(), replace the line self.order_number = data.half_beat with the following:

func setup(data: Dictionary) -> void:
    #...
    self.order_number = data.order_number

If you run the game now, you’ll see your patterns for each track you display in time with the music.

You may find you need to alter the timing of some to get the player to tap when you want them to.

In the next lesson, we’ll add an end screen that will guide the player back to the track-selection menu when the stack has depleted.

Code reference

PlacerHitBeat.gd

tool
extends Node2D
class_name PlacerHitBeat, "res://RhythmGame/Editor/placer_hit_beat_icon.svg"

export (PackedScene) var scene
export (int, 1, 4) var duration := 2 setget set_duration

var _order_number := 1


func _enter_tree() -> void:
    _order_number = get_index() + 1
    $OrderNumber.text = str(_order_number)


func get_data() -> Dictionary:
    return {
        scene = scene,
        order_number = _order_number,
        global_position = global_position,
        duration = duration
    }


func set_duration(amount: int) -> void:
    duration = amount
    $Sprite.frame = duration - 1

PlacerRest.gd

tool
extends Node2D
class_name PlacerRest, "res://RhythmGame/Editor/placer_rest_icon.svg"

export (int, 1, 4) var duration := 2 setget set_duration

var _order_number := 1


func _enter_tree() -> void:
    _order_number = get_index() + 1
    $OrderNumber.text = str(_order_number)


func get_data() -> Dictionary:
    return {duration = duration}


func set_duration(amount : int) -> void:
    duration = amount
    $Sprite.frame = duration - 1

HitSpawner.gd

extends Node

export var enabled := true
export var hit_beat: PackedScene

var _stack_current = []
var _stacks = {}

onready var patterns = $Patterns


func _ready() -> void:
    Events.connect("beat_incremented", self, "_spawn_beat")
    _generate_stacks()
    _select_stack({name = "Cephalopod"})


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)


func _select_stack(msg: Dictionary) -> void:
    _stack_current = _stacks[msg.name]


func _generate_stacks() -> void:
    for pattern in patterns.get_children():
        _stacks[pattern.name] = []
        for chunk in pattern.get_children():
            var sprite_frame := round(rand_range(0, 5))
            for placer in chunk.get_children():
                var hit_beat_data: Dictionary = placer.get_data()
                hit_beat_data.color = sprite_frame
                _stacks[pattern.name].append(hit_beat_data)

                for _i in range(hit_beat_data.duration - 1):
                    _stacks[pattern.name].append({})

    patterns.queue_free()

HitBeat.gd

extends Node2D

var order_number := 0 setget set_order_number

var _beat_hit := false

var _score_perfect := 10
var _score_great := 5
var _score_ok := 3

var _radius_start := 150.0
var _radius_perfect := 70.0
var _radius := _radius_start

var _offset_perfect := 4
var _offset_great := 8
var _offset_ok := 16

var _beat_delay := 4.0
var _speed := 0.0


onready var _animation_player := $AnimationPlayer
onready var _sprite := $Sprite
onready var _touch_area := $Area2D
onready var _label := $LabelCustom
onready var _target_circle := $TargetCircle


func _ready() -> void:
    _animation_player.play("show")


func setup(data: Dictionary) -> void:
    self.order_number = data.order_number
    global_position = data.global_position
    _sprite.frame = data.color
    _speed = 1.0 / data.bps / _beat_delay
    _target_circle.setup(_radius_start, _radius_perfect, data.bps, _beat_delay)


func _process(delta: float) -> void:
    if _beat_hit:
        return

    _radius -= delta * (_radius_start - _radius_perfect) * _speed

    if _radius <= _radius_perfect - _offset_perfect:
        _touch_area.collision_layer = 0

        Events.emit_signal("scored", {"score": 0, "position": global_position})
        _animation_player.play("destroy")
        _beat_hit = true


func set_order_number(number: int) -> void:
    order_number = number
    _label.text = str(order_number)


func _on_Area2D_input_event(_viewport, event, _shape_idx) -> void:
    if event.is_action_pressed("touch"):
        _beat_hit = true
        _touch_area.collision_layer = 0
        _animation_player.play("destroy")
        Events.emit_signal("scored", {"score": _get_score(), "position": global_position})


func _get_score() -> int:
    if abs(_radius_perfect - _radius) < _offset_perfect:
        return _score_perfect
    elif abs(_radius_perfect - _radius) < _offset_great:
        return _score_great
    elif abs(_radius_perfect - _radius) < _offset_ok:
        return _score_ok
    return 0