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!
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!
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")