
Godot Genre Rhythm
- 127 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-genre-rhythm for development tasks
About
godot-genre-rhythm: A skill for development. This provides functionality for development workflows.
- godot-genre-rhythm
Godot Genre Rhythm by the numbers
- 127 all-time installs (skills.sh)
- +8 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #2,750 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-genre-rhythmAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 127 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-genre-rhythm for development tasks
Files
Genre: Rhythm
Expert blueprint for rhythm games emphasizing audio-visual synchronization and flow state.
NEVER Do (Expert Anti-Patterns)
Audio Sync & Logic
- NEVER use
Time.get_ticks_msec()for rhythm sync; strictly use `AudioServer.get_time_since_last_mix()` combined with latency offsets for sub-frame accuracy. - NEVER process song logic in
_process(); strictly use `_physics_process()` or a conductor loop to ensure deterministic timing regardless of render frames. - NEVER use
_process()to capture hit inputs; strictly use `_input(event)` to record the exact timestamp of the button press event. - NEVER scale engine time_scale for song speed; strictly use `AudioStreamPlayer.pitch_scale` to adjust speed and avoid globally breaking physics logic.
- NEVER neglect Audio Latency calibration; strictly provide a tool for players to adjust for hardware/Bluetooth delays (~30-100ms) to prevent "unplayable" sync issues.
- NEVER use standard
_processdelta for note-to-audio sync; strictly use the Hardware Clock viaAudioServer.get_playback_position() + AudioServer.get_time_since_last_mix()for sub-frame accuracy. - NEVER move thousands of note sprites on the CPU; strictly use a Shader-Based Highway (UV scrolling) to offload track movement to the GPU.
- NEVER use
yieldorawaitfor beat timing; strictly use a sample-accurate Delta Accumulator tied to the audio clock. - NEVER assume a constant BPM; strictly build your conductor to handle a Tempo Map for complex track changes.
Feedback & Performance
- NEVER judge inputs based on world position (pixels); strictly judge against the Song's Elapsed Time (ms) to ensure consistency across resolutions.
- NEVER play hit sounds with static pitch; strictly add ±5% Random Pitch Variation to hit sounds to avoid the "machine gun" effect.
- NEVER use tight timing windows (e.g., <25ms) for all players; strictly use Wider Windows for Beginners to prevent immediate frustration.
- NEVER instantiate note nodes every beat; strictly use Object Pooling to recycle note instances and prevent GC spikes during dense tracks.
- NEVER use standard Area2D signals for rhythmic hits; strictly Poll Inputs in the conductor loop to compare against target timestamps.
- NEVER calculate FFT for visualization on the main thread; strictly use AudioEffectSpectrumAnalyzerInstance for optimized engine-side analysis.
- NEVER allow note spamming/mashing; strictly penalize misses or break combos to maintain the game's integrity.
- NEVER use
load()dynamically during gameplay; strictly use ResourceLoader.load_threaded_request() to avoid thread stalling. - NEVER forget to pause the conductor/ highway; strictly sync with the audio player's pause state to prevent notes from scrolling while the music is stopped.
---
🛠 Expert Components (scripts/)
Original Expert Patterns
- rhythm_conductor.gd - High-precision BPM/beat tracker with latency compensation logic.
Modular Components
- input_judge_logic.gd - Hit-window validation (Perfect/Good/Miss).
- latency_calibrator.gd - A/V offset measurement utility.
- note_object_pool.gd - High-frequency recycling for dense highways.
- audio_spectrum_analyzer.gd - Optimized engine-side frequency extraction.
- dynamic_bpm_handler.gd - Tempo map and fractional beat support.
- note_lane_manager.gd - Spawning routes and variable scroll speed control.
---
Core Loop
Music Plays → Notes Appear → Player Inputs → Timing Judged → Score/Feedback → Combo Builds
Skill Chain
godot-project-foundations, godot-input-handling, sound-manager, animation, ui-framework
---
Audio Synchronization
THE most critical aspect - notes MUST align perfectly with audio.
Music Time System
class_name MusicConductor
extends Node
signal beat(beat_number: int)
signal measure(measure_number: int)
@export var bpm := 120.0
@export var music: AudioStream
var seconds_per_beat: float
var song_position: float = 0.0 # In seconds
var song_position_in_beats: float = 0.0
var last_reported_beat: int = 0
@onready var audio_player: AudioStreamPlayer
func _ready() -> void:
seconds_per_beat = 60.0 / bpm
audio_player.stream = music
func _process(_delta: float) -> void:
# Get precise audio position with latency compensation
song_position = audio_player.get_playback_position() + AudioServer.get_time_since_last_mix()
# Convert to beats
song_position_in_beats = song_position / seconds_per_beat
# Emit beat signals
var current_beat := int(song_position_in_beats)
if current_beat > last_reported_beat:
beat.emit(current_beat)
if current_beat % 4 == 0:
measure.emit(current_beat / 4)
last_reported_beat = current_beat
func start_song() -> void:
audio_player.play()
song_position = 0.0
last_reported_beat = 0
func beats_to_seconds(beats: float) -> float:
return beats * seconds_per_beat
func seconds_to_beats(secs: float) -> float:
return secs / seconds_per_beat---
Note System
Note Data Structure
class_name NoteData
extends Resource
@export var beat_time: float # When to hit (in beats)
@export var lane: int # Which input lane (0-3 for 4-key, etc.)
@export var note_type: NoteType
@export var hold_duration: float = 0.0 # For hold notes (in beats)
enum NoteType { TAP, HOLD, SLIDE, FLICK }Chart/Beatmap Loading
class_name ChartLoader
extends Node
func load_chart(chart_path: String) -> Array[NoteData]:
var notes: Array[NoteData] = []
var file := FileAccess.open(chart_path, FileAccess.READ)
while not file.eof_reached():
var line := file.get_line()
if line.is_empty() or line.begins_with("#"):
continue
var parts := line.split(",")
var note := NoteData.new()
note.beat_time = float(parts[0])
note.lane = int(parts[1])
note.note_type = NoteType.get(parts[2]) if parts.size() > 2 else NoteType.TAP
note.hold_duration = float(parts[3]) if parts.size() > 3 else 0.0
notes.append(note)
notes.sort_custom(func(a, b): return a.beat_time < b.beat_time)
return notes---
Note Highway / Receptor
class_name NoteHighway
extends Control
@export var scroll_speed := 500.0 # Pixels per second
@export var hit_position_y := 100.0 # From bottom
@export var note_scene: PackedScene
@export var look_ahead_beats := 4.0
var active_notes: Array[NoteVisual] = []
var chart: Array[NoteData]
var next_note_index: int = 0
func _process(_delta: float) -> void:
spawn_upcoming_notes()
update_note_positions()
func spawn_upcoming_notes() -> void:
var look_ahead_time := MusicConductor.song_position_in_beats + look_ahead_beats
while next_note_index < chart.size():
var note_data := chart[next_note_index]
if note_data.beat_time > look_ahead_time:
break
var note_visual := note_scene.instantiate() as NoteVisual
note_visual.setup(note_data)
note_visual.position.x = get_lane_x(note_data.lane)
add_child(note_visual)
active_notes.append(note_visual)
next_note_index += 1
func update_note_positions() -> void:
for note in active_notes:
var beats_until_hit := note.data.beat_time - MusicConductor.song_position_in_beats
var seconds_until_hit := MusicConductor.beats_to_seconds(beats_until_hit)
# Note scrolls down from top
note.position.y = (size.y - hit_position_y) - (seconds_until_hit * scroll_speed)
# Remove if too far past
if note.position.y > size.y + 100:
if not note.was_hit:
register_miss(note.data)
note.queue_free()
active_notes.erase(note)---
Timing Judgment
class_name JudgmentSystem
extends Node
signal note_judged(judgment: Judgment, note: NoteData)
enum Judgment { PERFECT, GREAT, GOOD, BAD, MISS }
# Timing windows in milliseconds (symmetric around hit time)
const WINDOWS := {
Judgment.PERFECT: 25.0,
Judgment.GREAT: 50.0,
Judgment.GOOD: 100.0,
Judgment.BAD: 150.0
}
func judge_input(input_time: float, note_time: float) -> Judgment:
var difference := abs(input_time - note_time) * 1000.0 # ms
if difference <= WINDOWS[Judgment.PERFECT]:
return Judgment.PERFECT
elif difference <= WINDOWS[Judgment.GREAT]:
return Judgment.GREAT
elif difference <= WINDOWS[Judgment.GOOD]:
return Judgment.GOOD
elif difference <= WINDOWS[Judgment.BAD]:
return Judgment.BAD
else:
return Judgment.MISS
func get_timing_offset(input_time: float, note_time: float) -> float:
# Positive = late, Negative = early
return (input_time - note_time) * 1000.0---
Scoring System
class_name RhythmScoring
extends Node
signal score_changed(new_score: int)
signal combo_changed(new_combo: int)
signal combo_broken
const JUDGMENT_SCORES := {
Judgment.PERFECT: 100,
Judgment.GREAT: 75,
Judgment.GOOD: 50,
Judgment.BAD: 25,
Judgment.MISS: 0
}
const COMBO_MULTIPLIER_THRESHOLDS := {
10: 1.5,
25: 2.0,
50: 2.5,
100: 3.0
}
var score: int = 0
var combo: int = 0
var max_combo: int = 0
func register_judgment(judgment: Judgment) -> void:
if judgment == Judgment.MISS:
if combo > 0:
combo_broken.emit()
combo = 0
else:
combo += 1
max_combo = max(max_combo, combo)
var base_score := JUDGMENT_SCORES[judgment]
var multiplier := get_combo_multiplier()
var earned := int(base_score * multiplier)
score += earned
score_changed.emit(score)
combo_changed.emit(combo)
func get_combo_multiplier() -> float:
var mult := 1.0
for threshold in COMBO_MULTIPLIER_THRESHOLDS:
if combo >= threshold:
mult = COMBO_MULTIPLIER_THRESHOLDS[threshold]
return mult---
Input Processing
class_name RhythmInput
extends Node
@export var lane_actions: Array[StringName] = [
&"lane_0", &"lane_1", &"lane_2", &"lane_3"
]
var held_notes: Dictionary = {} # lane: NoteData for hold notes
func _input(event: InputEvent) -> void:
for i in lane_actions.size():
if event.is_action_pressed(lane_actions[i]):
process_lane_press(i)
elif event.is_action_released(lane_actions[i]):
process_lane_release(i)
func process_lane_press(lane: int) -> void:
var current_time := MusicConductor.song_position
var closest_note := find_closest_note_in_lane(lane, current_time)
if closest_note:
var note_time := MusicConductor.beats_to_seconds(closest_note.beat_time)
var judgment := JudgmentSystem.judge_input(current_time, note_time)
if judgment != Judgment.MISS:
hit_note(closest_note, judgment)
if closest_note.note_type == NoteType.HOLD:
held_notes[lane] = closest_note
func process_lane_release(lane: int) -> void:
if held_notes.has(lane):
var hold_note := held_notes[lane]
var hold_end_time := hold_note.beat_time + hold_note.hold_duration
var current_beat := MusicConductor.song_position_in_beats
# Check if released at correct time
if abs(current_beat - hold_end_time) < 0.25: # Quarter beat tolerance
complete_hold_note(hold_note)
else:
drop_hold_note(hold_note)
held_notes.erase(lane)---
Visual Feedback
func show_judgment_splash(judgment: Judgment, position: Vector2) -> void:
var splash := judgment_sprites[judgment].instantiate()
splash.position = position
add_child(splash)
var tween := create_tween()
tween.tween_property(splash, "scale", Vector2(1.2, 1.2), 0.1)
tween.tween_property(splash, "scale", Vector2(1.0, 1.0), 0.1)
tween.tween_property(splash, "modulate:a", 0.0, 0.3)
tween.tween_callback(splash.queue_free)
func pulse_receptor(lane: int, judgment: Judgment) -> void:
var receptor := lane_receptors[lane]
receptor.modulate = judgment_colors[judgment]
var tween := create_tween()
tween.tween_property(receptor, "modulate", Color.WHITE, 0.15)---
Common Pitfalls
| Pitfall | Solution |
|---|---|
| Audio desync | Use AudioServer.get_time_since_last_mix() latency compensation |
| Unfair judgment | Generous windows at low difficulty, offset calibration |
| Notes bunched visually | Adjust scroll speed or spawn timing |
| Hold notes janky | Separate hold body and tail rendering |
| Frame drops cause misses | Decouple input from framerate |
---
Godot-Specific Tips
1. Audio latency: Calibrate with AudioServer and custom offset 2. Input polling: Use _input not _process for precise timing 3. Shaders: UV scrolling for note highways 4. Particles: Use GPUParticles2D for hit effects
3. Hardware-Synced Latency Calibration
Calculate precise offsets by compensating for OS/Hardware latency.
# latency_calibrator.gd
func _record_tap(expected_time: float):
# Obtain precise hardware-synced audio position
var mix_time = audio_player.get_playback_position() + AudioServer.get_time_since_last_mix()
# Compensate for OS/Hardware output latency
var true_audio_time = mix_time - AudioServer.get_output_latency()
var offset = true_audio_time - expected_time
save_offset(offset)4. Ghost-Note Detection (Anti-Spam)
Penalize inputs that don't correlate to an active note using _unhandled_input.
# input_manager.gd
func _unhandled_input(event: InputEvent):
# Consume action only on initial press
if event.is_action_pressed("rhythm_hit", false, true):
if active_notes_in_window.is_empty():
# Anti-Cheat: Penalize spamming
ghost_note_detected.emit()
else:
_evaluate_hit()
get_viewport().set_input_as_handled()5. Shader-Based Note Highway
Ultra-smooth scrolling using UV manipulation on the GPU to bypass CPU bottlenecks.
// highway.gdshader
shader_type canvas_item;
uniform float scroll_speed = 1.0;
void fragment() {
vec2 scrolled_uv = UV;
// Offset Y over time to simulate movement
scrolled_uv.y -= TIME * scroll_speed;
COLOR = texture(TEXTURE, scrolled_uv);
}Reference
- Master Skill: godot-master
# audio_spectrum_analyzer.gd
extends Node
class_name AudioSpectrumAnalyzer
# FFT-Based Visual Reactive Data
# Uses Godot's optimized spectrum analyzer effect for visuals.
var spectrum_analyzer: AudioEffectSpectrumAnalyzerInstance
func _ready() -> void:
# Pattern: Get the instance from an existing Bus effect.
spectrum_analyzer = AudioServer.get_bus_effect_instance(0, 0) # Master bus, index 0
func get_magnitude(from_hz: float, to_hz: float) -> float:
if spectrum_analyzer:
var magnitude = spectrum_analyzer.get_magnitude_for_frequency_range(from_hz, to_hz)
return magnitude.length()
return 0.0
# beat_synced_animator.gd
extends Node
class_name BeatSyncedAnimator
# Tweening Tied to Conductor Pulses
# Synchronizes visual bounces/scales with the conductor's beat.
func pulse_node(target: CanvasItem) -> void:
var tween = target.create_tween()
# Pattern: Use TRANS_ELASTIC or TRANS_BACK for rhythmic bounce feel.
target.scale = Vector2(1.2, 1.2)
tween.tween_property(target, "scale", Vector2.ONE, 0.15).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
# dynamic_bpm_handler.gd
extends Node
class_name DynamicBPMHandler
# Handling Mid-Song Tempo Changes
# Adjusts beat calculation based on tempo map markers.
struct BPMMarker:
var beat: float
var time: float
var bpm: float
var bpm_markers: Array[BPMMarker] = []
func get_beat_at_time(song_time: float) -> float:
# Pattern: Find the most recent BPM marker and calculate beat offset.
var current_marker = bpm_markers[0]
for marker in bpm_markers:
if marker.time <= song_time:
current_marker = marker
else:
break
var elapsed = song_time - current_marker.time
return current_marker.beat + (elapsed * (current_marker.bpm / 60.0))
# input_judge_logic.gd
extends Node
class_name InputJudgeLogic
# Window-Based Hit Validation
# Compares input timing against target beat time with specific windows.
@export var window_perfect := 0.05 # 50ms
@export var window_good := 0.1 # 100ms
@export var window_ok := 0.15 # 150ms
enum HitResult { NONE, PERFECT, GOOD, OK, MISS }
func judge_hit(current_time: float, target_time: float) -> HitResult:
var diff = abs(current_time - target_time)
# Pattern: Sequential window checks from smallest to largest.
if diff <= window_perfect: return HitResult.PERFECT
if diff <= window_good: return HitResult.GOOD
if diff <= window_ok: return HitResult.OK
return HitResult.MISS
# latency_calibrator.gd
extends Node
class_name LatencyCalibrator
# Audio-Visual Offset Correction
# Allows players to adjust the delay between sound and visuals.
var user_offset := 0.0 # Stored in user config
func start_calibration_test() -> void:
# Measure delta between visual flash and player keypress.
pass
func apply_offset(conductor: RhythmConductor) -> void:
# Pattern: Update the conductor's internal offset with calibrated value.
conductor.offset = user_offset
# note_lane_manager.gd
extends Node
class_name NoteLaneManager
# Routing and Spawning Logic
# Manages multiple lanes for rhythm note distribution.
@export var lane_count := 4
@export var spawn_distance := 1000.0
@export var scroll_speed := 500.0 # Pixels per second
func spawn_note_in_lane(lane_idx: int, target_time: float) -> void:
# Logic for calculating initial position based on target hit time.
pass
# note_object_pool.gd
extends Node
class_name NoteObjectPool
# High-Frequency Entity Recycling
# Pre-instantiates notes to avoid frame drops during busy sequences.
@export var note_scene: PackedScene
@export var pool_size := 50
var _pool: Array[Node] = []
func _ready() -> void:
for i in pool_size:
var note = note_scene.instantiate()
note.hide()
note.process_mode = PROCESS_MODE_DISABLED
add_child(note)
_pool.append(note)
func get_note() -> Node:
# Pattern: Fetch disabled nodes from the pool and reactive them.
for note in _pool:
if not note.visible:
note.show()
note.process_mode = PROCESS_MODE_INHERIT
return note
return null
extends Node
class_name NoteOrchestrator
## Expert Note Orchestrator (Godot 4.6).
## Spawns and positions notes purely based on timeline math.
@export var conductor: RhythmConductor
@export var bpm: float = 120.0
@export var scroll_speed: float = 500.0 # Pixels per beat
func _process(_delta: float) -> void:
var current_beat = conductor.get_current_beat(bpm)
_update_note_positions(current_beat)
func _update_note_positions(current_beat: float) -> void:
# Group: "active_notes"
for note in get_tree().get_nodes_in_group("active_notes"):
var beats_away = note.target_beat - current_beat
# Interpolate visual position based on time distance
note.position.y = beats_away * scroll_speed
## [SKILL NOTICE]: Use 'beats_away' to position notes. This ensures
## visual consistency regardless of framerate or lag spikes.
# rhythm_conductor.gd
extends Node
class_name RhythmConductor
# High-Precision BPM Tracking using AudioServer
# Accounts for output latency and mix offsets to ensure perfect sync.
@export var bpm := 120.0
@export var offset := 0.0 # Manual calibration offset
var time_begin := 0.0
var time_delay := 0.0
func _ready() -> void:
time_begin = Time.get_ticks_usec()
time_delay = AudioServer.get_time_to_next_mix() + AudioServer.get_output_latency()
func get_current_time() -> float:
# Pattern: Use AudioServer-derived time for exact rhythm sync.
var current_time = (Time.get_ticks_usec() - time_begin) / 1000000.0
current_time -= time_delay
return max(0.0, current_time + offset)
func get_current_beat() -> float:
return get_current_time() * (bpm / 60.0)
extends Node
class_name RhythmScoringSystem
## Expert Rhythm Scoring (Godot 4.6).
## Precise hit detection using millisecond offsets.
const PERFECT_LIMIT = 0.02 # 20ms
const GREAT_LIMIT = 0.05 # 50ms
const GOOD_LIMIT = 0.10 # 100ms
func evaluate_hit(conductor_time: float, target_time: float) -> String:
var offset = abs(conductor_time - target_time)
if offset <= PERFECT_LIMIT: return "PERFECT"
if offset <= GREAT_LIMIT: return "GREAT"
if offset <= GOOD_LIMIT: return "GOOD"
return "MISS"
## [SKILL NOTICE]: Use absolute time difference for scoring.
## Millisecond accuracy is the industry standard for rhythm games.
# rhythm_ui_feedback.gd
extends Control
class_name RhythmUIFeedback
# Visual Response for Hit Quality
# Spawns transient labels indicating Early/Late/Perfect status.
@export var feedback_colors: Dictionary = {
"PERFECT": Color.GOLD,
"GOOD": Color.CYAN,
"OK": Color.GREEN,
"MISS": Color.RED
}
func show_feedback(result_name: String) -> void:
var label = Label.new()
label.text = result_name
label.modulate = feedback_colors.get(result_name, Color.WHITE)
add_child(label)
var tween = create_tween()
# Pattern: Fade out and move up simultaneously.
tween.set_parallel(true)
tween.tween_property(label, "position:y", label.position.y - 50, 0.4)
tween.tween_property(label, "modulate:a", 0.0, 0.4)
tween.chain().tween_callback(label.queue_free)
# score_combo_manager.gd
extends Node
class_name ScoreComboManager
# Functional Reduction for Multipliers
# Manages combo-based scoring with persistent state.
var score := 0
var combo := 0
var max_combo := 0
func add_hit(multiplier: int) -> void:
combo += 1
max_combo = max(combo, max_combo)
# Pattern: Scale score by combo milestones.
var combo_bonus = 1.0 + (floor(combo / 10.0) * 0.1)
score += int(100 * multiplier * combo_bonus)
func reset_combo() -> void:
combo = 0