Adding rollers to patterns

In this final lesson, we’ll create a placer scene that we can use to design curves for rollers.

The placer scene for the HitRoller follows the same idea for other placer scenes: we keep track of essential properties such as position, duration, and so on.

This time, however, we also need a curve. We’ll use a Path2D for that.

Create a new scene with a Path2D node named PlacerHitRoller.

Like other placer scenes, we need a sprite and label to represent the starting point. You can merge those (or copy them) from PlacerHitBeat.tscn.

Create a straight curve with two points. We’ll use this as a base to modify every time we add the placer to a pattern.

Enabling snapping helps align the first point at (0, 0) for the start position to be consistent with other placer scenes in the grid.

The node’s Curve is a resource, so every instance of the scene will share the same curve by default. Instead, we want each instance to have a unique one.

In the Inspector, expand the Curve and turn on Resource -> Local To Scene.

Save the scene in the RhythmGame/Editor/ directory and attach a script to the root node.

The script follows the same idea as the PlacerHitBeat. We have a unique class name and icon.

tool
extends Path2D
class_name PlacerRoller, "res://RhythmGame/Editor/placer_hit_roller_icon.svg"

## The scene to instantiate at runtime instead of this placeholder.
export (PackedScene) var scene
## The roller's duration in half-beats.
export (int, 1, 4) var duration := 4 setget set_duration

var _order_number := 1


# When changing the duration, we update the sprite accordingly.
func set_duration(amount: int) -> void:
    duration = amount
    $Sprite.frame = duration - 1

As before, we set the order number to tell at a glance what order the elements will be displayed to the player.

func _enter_tree() -> void:
    _order_number = get_index() + 1
    $OrderNumber.text = str(_order_number)
    # We make sure the sprite aligns with the first point of the curve.
    # Although it should be (0, 0).
    $Sprite.global_position = to_global(curve.get_point_position(0))

The information this placer returns through its get_data() method is near identical to the PlacerHitBeat, with the addition of the curve.

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

Finally, we need to draw a circle to show where the roller will end up at the curve’s end.

As this script uses Godot’s tool mode, the drawing will update in the editor as we design the curve.

func _draw() -> void:
    draw_circle(curve.get_point_position(curve.get_point_count() - 1), 75.0, Color.black)

Be sure to load the HitRoller.tscn scene in the inspector.

Finally, we need to use the placer’s curve when instantiating a HitRoller.

Open up HitRoller.gd and add the line to the setup method.

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

With that, we have everything we need!

Altering roller curves

To add a roller to a pattern, open the corresponding scene, like Cephalopod.tscn, instantiate the PlacerHitRoller, and click and drag on control points to shape the curve.

I like to snap the end point to the grid. You can click on the curve to add a point and shift-click a point to alter the tangents and shape the curve.

You should also always add a rest immediately after a roller. Otherwise, the next HitBeat will need to be tapped at the same time as the end of the roller movement, which is not possible.

I hope you enjoyed this series.

I’d love to see how you use this course. Will you take what you’ve learned to create your own unique rhythm game?

Will you design interesting patterns to use for your own songs? Either way, please send them our way!

Thank you for reading. Happy coding!

Code reference

PlacerHitRoller.gd

tool
extends Path2D
class_name PlacerRoller, "res://RhythmGame/Editor/placer_hit_roller_icon.svg"

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

var _order_number := 1


func _enter_tree() -> void:
    _order_number = get_index() + 1
    $OrderNumber.text = str(_order_number)
    $Sprite.global_position = to_global(curve.get_point_position(0))


func _draw() -> void:
    draw_circle(curve.get_point_position(curve.get_point_count() - 1), 75.0, Color.black)


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


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

HitRoller.gd

extends Path2D

var order_number := 0 setget set_order_number

var _beat_delay := 4.0

onready var _sprite_start := $SpriteStart
onready var _label_start := $SpriteStart/Label
onready var _sprite_end := $SpriteEnd
onready var _label_end := $SpriteEnd/Label
onready var _animation_player := $AnimationPlayer
onready var _growing_line := $GrowingLine2D
onready var _roller := $RollerFollow/Roller
onready var _roller_follow := $RollerFollow


func _ready() -> void:
    _animation_player.play("show")
    var test_data := {
        global_position = global_position, order_number = 1, color = 0, bps = 1, duration = 4
    }
    setup(test_data)


func setup(data: Dictionary) -> void:
    var curve_points := curve.get_baked_points()
    _sprite_start.position = curve_points[0]
    _sprite_end.position = curve_points[curve_points.size() - 1]

    self.order_number = data.order_number
    global_position = data.global_position

    _sprite_start.frame = data.color
    _sprite_end.frame = data.color

    _growing_line.setup(curve_points)
    _growing_line.start()

    _roller.setup(data.bps, data.duration, data.color)

    var roller_path_delay = data.bps * _beat_delay
    var roller_path_duration = data.bps * data.duration / 2.0
    _roller_follow.start_movement(roller_path_delay, roller_path_duration)

    curve = data.curve


func set_order_number(number: int) -> void:
    order_number = number
    _label_start.text = str(number)
    _label_end.text = str(number + 1)


func destroy() -> void:
    _animation_player.play("destroy")