mirror of
https://github.com/godotengine/godot-demo-projects.git
synced 2025-12-16 05:20:06 +01:00
Merge pull request #1197 from PizzaLovers007/rhythm-game
Add a simple rhythm game demo
This commit is contained in:
20
audio/rhythm_game/README.md
Normal file
20
audio/rhythm_game/README.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Rhythm Game
|
||||||
|
|
||||||
|
A simple rhythm game that utilizes strategies described in the article
|
||||||
|
["Sync the gameplay with audio and music"](https://docs.godotengine.org/en/stable/tutorials/audio/sync_with_audio.html).
|
||||||
|
|
||||||
|
Playback position jitter is resolved using the
|
||||||
|
[1€ filter](https://gery.casiez.net/1euro/) to achieve smooth note movements.
|
||||||
|
|
||||||
|
The metronome sound was recorded by Ludwig Peter Müller in December 2020 under
|
||||||
|
the "Creative Commons CC0 1.0 Universal" license.
|
||||||
|
|
||||||
|
Language: GDScript
|
||||||
|
|
||||||
|
Renderer: Compatibility
|
||||||
|
|
||||||
|
Check out this demo on the asset library: TBD
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|

|
||||||
160
audio/rhythm_game/game_state/conductor.gd
Normal file
160
audio/rhythm_game/game_state/conductor.gd
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
## Accurately tracks the current beat of a song.
|
||||||
|
class_name Conductor
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
## If [code]true[/code], the song is paused. Setting [member is_paused] to
|
||||||
|
## [code]false[/code] resumes the song.
|
||||||
|
@export var is_paused: bool = false:
|
||||||
|
get:
|
||||||
|
if player:
|
||||||
|
return player.stream_paused
|
||||||
|
return false
|
||||||
|
set(value):
|
||||||
|
if player:
|
||||||
|
player.stream_paused = value
|
||||||
|
|
||||||
|
@export_group("Nodes")
|
||||||
|
## The song player.
|
||||||
|
@export var player: AudioStreamPlayer
|
||||||
|
|
||||||
|
@export_group("Song Parameters")
|
||||||
|
## Beats per minute of the song.
|
||||||
|
@export var bpm: float = 100
|
||||||
|
## Offset (in milliseconds) of when the 1st beat of the song is in the audio
|
||||||
|
## file. [code]5000[/code] means the 1st beat happens 5 seconds into the track.
|
||||||
|
@export var first_beat_offset_ms: int = 0
|
||||||
|
|
||||||
|
@export_group("Filter Parameters")
|
||||||
|
## [code]cutoff[/code] for the 1€ filter. Decrease to reduce jitter.
|
||||||
|
@export var allowed_jitter: float = 0.1
|
||||||
|
## [code]beta[/code] for the 1€ filter. Increase to reduce lag.
|
||||||
|
@export var lag_reduction: float = 5
|
||||||
|
|
||||||
|
# Calling this is expensive, so cache the value. This should not change.
|
||||||
|
var _cached_output_latency: float = AudioServer.get_output_latency()
|
||||||
|
|
||||||
|
# General conductor state
|
||||||
|
var _is_playing: bool = false
|
||||||
|
|
||||||
|
# Audio thread state
|
||||||
|
var _song_time_audio: float = -100
|
||||||
|
|
||||||
|
# System time state
|
||||||
|
var _song_time_begin: float = 0
|
||||||
|
var _song_time_system: float = -100
|
||||||
|
|
||||||
|
# Filtered time state
|
||||||
|
var _filter: OneEuroFilter
|
||||||
|
var _filtered_audio_system_delta: float = 0
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
# Ensure that playback state is always updating, otherwise the smoothing
|
||||||
|
# filter causes issues.
|
||||||
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
if not _is_playing:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Handle a web bug where AudioServer.get_time_since_last_mix() occasionally
|
||||||
|
# returns unsigned 64-bit integer max value. This is likely due to minor
|
||||||
|
# timing issues between the main/audio threads, thus causing an underflow
|
||||||
|
# in the engine code.
|
||||||
|
var last_mix := AudioServer.get_time_since_last_mix()
|
||||||
|
if last_mix > 1000:
|
||||||
|
last_mix = 0
|
||||||
|
|
||||||
|
# First, calculate the song time using data from the audio thread. This
|
||||||
|
# value is very jittery, but will always match what the player is hearing.
|
||||||
|
_song_time_audio = (
|
||||||
|
player.get_playback_position()
|
||||||
|
# The 1st beat may not start at second 0 of the audio track. Compensate
|
||||||
|
# with an offset setting.
|
||||||
|
- first_beat_offset_ms / 1000.0
|
||||||
|
# For most platforms, the playback position value updates in chunks,
|
||||||
|
# with each chunk being one "mix". Smooth this out by adding in the time
|
||||||
|
# since the last chunk was processed.
|
||||||
|
+ last_mix
|
||||||
|
# Current processed audio is heard later.
|
||||||
|
- _cached_output_latency
|
||||||
|
)
|
||||||
|
|
||||||
|
# Next, calculate the song time using the system clock at render rate. This
|
||||||
|
# value is very stable, but can drift from the playing audio due to pausing,
|
||||||
|
# stuttering, etc.
|
||||||
|
_song_time_system = (Time.get_ticks_usec() / 1000000.0) - _song_time_begin
|
||||||
|
_song_time_system *= player.pitch_scale
|
||||||
|
|
||||||
|
# We don't do anything else here. Check _physics_process next.
|
||||||
|
|
||||||
|
|
||||||
|
func _physics_process(delta: float) -> void:
|
||||||
|
if not _is_playing:
|
||||||
|
return
|
||||||
|
|
||||||
|
# To have the best of both the audio-based time and system-based time, we
|
||||||
|
# apply a smoothing filter (1€ filter) on the delta between the two values,
|
||||||
|
# then add it to the system-based time. This allows us to have a stable
|
||||||
|
# value that is also always accurate to what the player hears.
|
||||||
|
#
|
||||||
|
# Notes:
|
||||||
|
# - The 1€ filter jitter reduction is more effective on values that don't
|
||||||
|
# change drastically between samples, so we filter on the delta (generally
|
||||||
|
# less variable between frames) rather than the time itself.
|
||||||
|
# - We run the filter step in _physics_process to reduce the variability of
|
||||||
|
# different systems' update rates. The filter params are specifically
|
||||||
|
# tuned for 60 UPS.
|
||||||
|
var audio_system_delta := _song_time_audio - _song_time_system
|
||||||
|
_filtered_audio_system_delta = _filter.filter(audio_system_delta, delta)
|
||||||
|
|
||||||
|
# Uncomment this to show the difference between raw and filtered time.
|
||||||
|
#var song_time := _song_time_system + _filtered_audio_system_delta
|
||||||
|
#print("Error: %+.1f ms" % [abs(song_time - _song_time_audio) * 1000.0])
|
||||||
|
|
||||||
|
|
||||||
|
func play() -> void:
|
||||||
|
var filter_args := {
|
||||||
|
"cutoff": allowed_jitter,
|
||||||
|
"beta": lag_reduction,
|
||||||
|
}
|
||||||
|
_filter = OneEuroFilter.new(filter_args)
|
||||||
|
|
||||||
|
player.play()
|
||||||
|
_is_playing = true
|
||||||
|
|
||||||
|
# Capture the start of the song using the system clock.
|
||||||
|
_song_time_begin = (
|
||||||
|
Time.get_ticks_usec() / 1000000.0
|
||||||
|
# The 1st beat may not start at second 0 of the audio track. Compensate
|
||||||
|
# with an offset setting.
|
||||||
|
+ first_beat_offset_ms / 1000.0
|
||||||
|
# Playback does not start immediately, but only when the next audio
|
||||||
|
# chunk is processed (the "mix" step). Add in the time until that
|
||||||
|
# happens.
|
||||||
|
+ AudioServer.get_time_to_next_mix()
|
||||||
|
# Add in additional output latency.
|
||||||
|
+ _cached_output_latency
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func stop() -> void:
|
||||||
|
player.stop()
|
||||||
|
_is_playing = false
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the current beat of the song.
|
||||||
|
func get_current_beat() -> float:
|
||||||
|
var song_time := _song_time_system + _filtered_audio_system_delta
|
||||||
|
return song_time / get_beat_duration()
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the current beat of the song without smoothing.
|
||||||
|
func get_current_beat_raw() -> float:
|
||||||
|
return _song_time_audio / get_beat_duration()
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the duration of one beat (in seconds).
|
||||||
|
func get_beat_duration() -> float:
|
||||||
|
return 60 / bpm
|
||||||
1
audio/rhythm_game/game_state/conductor.gd.uid
Normal file
1
audio/rhythm_game/game_state/conductor.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dxdm5hivq6xkf
|
||||||
25
audio/rhythm_game/game_state/metronome.gd
Normal file
25
audio/rhythm_game/game_state/metronome.gd
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
extends AudioStreamPlayer
|
||||||
|
|
||||||
|
@export var conductor: Conductor
|
||||||
|
|
||||||
|
var _playing: bool = false
|
||||||
|
var _last_beat: float = -17 # 16 beat count-in
|
||||||
|
var _cached_latency: float = AudioServer.get_output_latency()
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
if not _playing:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Note that this implementation is flawed since every tick is rounded to the
|
||||||
|
# next mix window (~11ms at the default 44100 Hz mix rate) due to Godot's
|
||||||
|
# audio mix buffer. Precise audio scheduling is requested in
|
||||||
|
# https://github.com/godotengine/godot-proposals/issues/1151.
|
||||||
|
var curr_beat := conductor.get_current_beat() + _cached_latency
|
||||||
|
if GlobalSettings.enable_metronome and floor(curr_beat) > floor(_last_beat):
|
||||||
|
play()
|
||||||
|
_last_beat = max(_last_beat, curr_beat)
|
||||||
|
|
||||||
|
|
||||||
|
func start() -> void:
|
||||||
|
_playing = true
|
||||||
1
audio/rhythm_game/game_state/metronome.gd.uid
Normal file
1
audio/rhythm_game/game_state/metronome.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://gd4p06mb2biq
|
||||||
153
audio/rhythm_game/game_state/note_manager.gd
Normal file
153
audio/rhythm_game/game_state/note_manager.gd
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
class_name NoteManager
|
||||||
|
extends Node2D
|
||||||
|
|
||||||
|
signal play_stats_updated(play_stats: PlayStats)
|
||||||
|
signal note_hit(beat: float, hit_type: Enums.HitType, hit_error: float)
|
||||||
|
signal song_finished(play_stats: PlayStats)
|
||||||
|
|
||||||
|
const NOTE_SCENE = preload("res://objects/note/note.tscn")
|
||||||
|
const HIT_MARGIN_PERFECT = 0.050
|
||||||
|
const HIT_MARGIN_GOOD = 0.150
|
||||||
|
const HIT_MARGIN_MISS = 0.300
|
||||||
|
|
||||||
|
@export var conductor: Conductor
|
||||||
|
@export var time_type: Enums.TimeType = Enums.TimeType.FILTERED
|
||||||
|
@export var chart: ChartData.Chart = ChartData.Chart.THE_COMEBACK
|
||||||
|
|
||||||
|
var _notes: Array[Note] = []
|
||||||
|
|
||||||
|
var _play_stats: PlayStats
|
||||||
|
var _hit_error_acc: float = 0.0
|
||||||
|
var _hit_count: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_play_stats = PlayStats.new()
|
||||||
|
_play_stats.changed.connect(
|
||||||
|
func() -> void:
|
||||||
|
play_stats_updated.emit(_play_stats)
|
||||||
|
)
|
||||||
|
|
||||||
|
var chart_data := ChartData.get_chart_data(chart)
|
||||||
|
|
||||||
|
var note_beats: Array[float] = []
|
||||||
|
for measure_i in range(chart_data.size()):
|
||||||
|
var measure: Array = chart_data[measure_i]
|
||||||
|
var subdivision := 1.0 / measure.size() * 4
|
||||||
|
for note_i: int in range(measure.size()):
|
||||||
|
var beat := measure_i * 4 + note_i * subdivision
|
||||||
|
if measure[note_i] == 1:
|
||||||
|
note_beats.append(beat)
|
||||||
|
|
||||||
|
for beat in note_beats:
|
||||||
|
var note := NOTE_SCENE.instantiate() as Note
|
||||||
|
note.beat = beat
|
||||||
|
note.conductor = conductor
|
||||||
|
note.update_beat(-100)
|
||||||
|
add_child(note)
|
||||||
|
_notes.append(note)
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
if _notes.is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
var curr_beat := _get_curr_beat()
|
||||||
|
for i in range(_notes.size()):
|
||||||
|
_notes[i].update_beat(curr_beat)
|
||||||
|
|
||||||
|
_miss_old_notes()
|
||||||
|
|
||||||
|
if Input.is_action_just_pressed("main_key"):
|
||||||
|
_handle_keypress()
|
||||||
|
|
||||||
|
if _notes.is_empty():
|
||||||
|
_finish_song()
|
||||||
|
|
||||||
|
|
||||||
|
func _miss_old_notes() -> void:
|
||||||
|
while not _notes.is_empty():
|
||||||
|
var note := _notes[0] as Note
|
||||||
|
var note_delta := _get_note_delta(note)
|
||||||
|
|
||||||
|
if note_delta > HIT_MARGIN_GOOD:
|
||||||
|
# Time is past the note's hit window, miss.
|
||||||
|
note.miss(false)
|
||||||
|
_notes.remove_at(0)
|
||||||
|
_play_stats.miss_count += 1
|
||||||
|
note_hit.emit(note.beat, Enums.HitType.MISS_LATE, note_delta)
|
||||||
|
else:
|
||||||
|
# Note is still hittable, so stop checking rest of the (later)
|
||||||
|
# notes.
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
func _handle_keypress() -> void:
|
||||||
|
var note := _notes[0] as Note
|
||||||
|
var hit_delta := _get_note_delta(note)
|
||||||
|
if hit_delta < -HIT_MARGIN_MISS:
|
||||||
|
# Note is not hittable, do nothing.
|
||||||
|
pass
|
||||||
|
elif -HIT_MARGIN_PERFECT <= hit_delta and hit_delta <= HIT_MARGIN_PERFECT:
|
||||||
|
# Hit on time, perfect.
|
||||||
|
note.hit_perfect()
|
||||||
|
_notes.remove_at(0)
|
||||||
|
_hit_error_acc += hit_delta
|
||||||
|
_hit_count += 1
|
||||||
|
_play_stats.perfect_count += 1
|
||||||
|
_play_stats.mean_hit_error = _hit_error_acc / _hit_count
|
||||||
|
note_hit.emit(note.beat, Enums.HitType.PERFECT, hit_delta)
|
||||||
|
elif -HIT_MARGIN_GOOD <= hit_delta and hit_delta <= HIT_MARGIN_GOOD:
|
||||||
|
# Hit slightly off time, good.
|
||||||
|
note.hit_good()
|
||||||
|
_notes.remove_at(0)
|
||||||
|
_hit_error_acc += hit_delta
|
||||||
|
_hit_count += 1
|
||||||
|
_play_stats.good_count += 1
|
||||||
|
_play_stats.mean_hit_error = _hit_error_acc / _hit_count
|
||||||
|
if hit_delta < 0:
|
||||||
|
note_hit.emit(note.beat, Enums.HitType.GOOD_EARLY, hit_delta)
|
||||||
|
else:
|
||||||
|
note_hit.emit(note.beat, Enums.HitType.GOOD_LATE, hit_delta)
|
||||||
|
elif -HIT_MARGIN_MISS <= hit_delta and hit_delta <= HIT_MARGIN_MISS:
|
||||||
|
# Hit way off time, miss.
|
||||||
|
note.miss()
|
||||||
|
_notes.remove_at(0)
|
||||||
|
_hit_error_acc += hit_delta
|
||||||
|
_hit_count += 1
|
||||||
|
_play_stats.miss_count += 1
|
||||||
|
_play_stats.mean_hit_error = _hit_error_acc / _hit_count
|
||||||
|
if hit_delta < 0:
|
||||||
|
note_hit.emit(note.beat, Enums.HitType.MISS_EARLY, hit_delta)
|
||||||
|
else:
|
||||||
|
note_hit.emit(note.beat, Enums.HitType.MISS_LATE, hit_delta)
|
||||||
|
|
||||||
|
|
||||||
|
func _finish_song() -> void:
|
||||||
|
song_finished.emit(_play_stats)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_note_delta(note: Note) -> float:
|
||||||
|
var curr_beat := _get_curr_beat()
|
||||||
|
var beat_delta := curr_beat - note.beat
|
||||||
|
return beat_delta * conductor.get_beat_duration()
|
||||||
|
|
||||||
|
|
||||||
|
func _get_curr_beat() -> float:
|
||||||
|
var curr_beat: float
|
||||||
|
match time_type:
|
||||||
|
Enums.TimeType.FILTERED:
|
||||||
|
curr_beat = conductor.get_current_beat()
|
||||||
|
Enums.TimeType.RAW:
|
||||||
|
curr_beat = conductor.get_current_beat_raw()
|
||||||
|
_:
|
||||||
|
assert(false, "Unknown TimeType: %s" % time_type)
|
||||||
|
curr_beat = conductor.get_current_beat()
|
||||||
|
|
||||||
|
# Adjust the timing for input delay. While this will shift the note
|
||||||
|
# positions such that "on time" does not line up visually with the guide
|
||||||
|
# sprite, the resulting visual is a lot smoother compared to readjusting the
|
||||||
|
# note position after hitting it.
|
||||||
|
curr_beat -= GlobalSettings.input_latency_ms / 1000.0 / conductor.get_beat_duration()
|
||||||
|
|
||||||
|
return curr_beat
|
||||||
1
audio/rhythm_game/game_state/note_manager.gd.uid
Normal file
1
audio/rhythm_game/game_state/note_manager.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://d0qi52a8nkb6o
|
||||||
23
audio/rhythm_game/game_state/play_stats.gd
Normal file
23
audio/rhythm_game/game_state/play_stats.gd
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
class_name PlayStats
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
@export var mean_hit_error: float = 0.0:
|
||||||
|
set(value):
|
||||||
|
if mean_hit_error != value:
|
||||||
|
mean_hit_error = value
|
||||||
|
emit_changed()
|
||||||
|
@export var perfect_count: int = 0:
|
||||||
|
set(value):
|
||||||
|
if perfect_count != value:
|
||||||
|
perfect_count = value
|
||||||
|
emit_changed()
|
||||||
|
@export var good_count: int = 0:
|
||||||
|
set(value):
|
||||||
|
if good_count != value:
|
||||||
|
good_count = value
|
||||||
|
emit_changed()
|
||||||
|
@export var miss_count: int = 0:
|
||||||
|
set(value):
|
||||||
|
if miss_count != value:
|
||||||
|
miss_count = value
|
||||||
|
emit_changed()
|
||||||
1
audio/rhythm_game/game_state/play_stats.gd.uid
Normal file
1
audio/rhythm_game/game_state/play_stats.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://d137fo6uik460
|
||||||
135
audio/rhythm_game/globals/chart_data.gd
Normal file
135
audio/rhythm_game/globals/chart_data.gd
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
class_name ChartData
|
||||||
|
|
||||||
|
enum Chart {
|
||||||
|
THE_COMEBACK = 0,
|
||||||
|
SYNC_TEST = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
const THE_COMEBACK_DATA: Array[Array] = [
|
||||||
|
[1,0,0,0, 0,0,0,0, 1,0,0,1, 0,0,1,0],
|
||||||
|
[0,0,0,0, 0,0,0,0, 1,0,0,1, 0,0,1,0],
|
||||||
|
[0,0,0,0, 0,0,0,0, 1,0,0,1, 0,0,1,0],
|
||||||
|
[0,0,0,0, 0,0,1,0, 0,0,0,0, 0,0,0,0],
|
||||||
|
|
||||||
|
[0,0,1,0, 0,0,1,1, 0,0,0,1, 0,0,1,1],
|
||||||
|
[0,0,0,0, 0,0,1,0, 1,1,0,1, 0,1,0,1],
|
||||||
|
[0,1,1,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
|
||||||
|
[0,0,1,0, 0,1,0,0, 1,0,0,1, 0,0,1,0],
|
||||||
|
|
||||||
|
[0,0,1,0, 0,0,1,1, 0,0,0,1, 0,0,1,1],
|
||||||
|
[0,0,0,0, 0,0,1,0, 1,1,0,1, 0,1,0,1],
|
||||||
|
[0,1,1,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
|
||||||
|
[0,0,1,0, 0,1,0,0, 1,0,0,1, 0,0,1,0],
|
||||||
|
|
||||||
|
[0,0,1,0, 0,0,1,1, 0,0,0,1, 0,0,1,1],
|
||||||
|
[0,0,0,0, 0,0,1,0, 1,1,0,1, 0,1,0,1],
|
||||||
|
[0,1,1,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
|
||||||
|
[0,0,1,0, 0,1,0,0, 1,0,0,1, 0,0,1,0],
|
||||||
|
|
||||||
|
[0,0,1,0, 0,0,1,1, 0,0,0,1, 0,0,1,1],
|
||||||
|
[0,0,0,0, 0,0,1,0, 1,1,0,1, 0,1,0,1],
|
||||||
|
[0,1,1,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
|
||||||
|
[0,0,1,0, 0,1,0,0, 1,0,1,0, 1,0,1,0],
|
||||||
|
|
||||||
|
[0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,1,1],
|
||||||
|
[1,0,1,1, 0,0,0,1, 0,0,0,0, 0,0,0,0],
|
||||||
|
[1,0,1,0, 0,0,1,0, 0,0,1,0, 0,0,1,0],
|
||||||
|
[0,0,1,0, 0,0,1,0, 0,0,1,0, 0,0,1,0],
|
||||||
|
|
||||||
|
[0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,1,1],
|
||||||
|
[1,0,1,1, 0,0,0,1, 0,0,0,0, 0,0,0,0],
|
||||||
|
[1,0,1,0, 0,0,1,0, 0,0,1,0, 0,0,1,0],
|
||||||
|
[0,0,1,0, 1,0,1,0, 0,0,0,0, 1,0,1,0],
|
||||||
|
|
||||||
|
[0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,1,1],
|
||||||
|
[1,0,1,1, 0,0,0,1, 0,0,0,0, 0,0,0,0],
|
||||||
|
[1,0,1,0, 0,0,1,0, 0,0,1,0, 0,0,1,0],
|
||||||
|
[0,0,1,0, 0,0,1,0, 0,0,1,0, 0,0,1,0],
|
||||||
|
|
||||||
|
[0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,1,1],
|
||||||
|
[1,0,1,1, 0,0,0,1, 0,0,0,0, 0,0,0,0],
|
||||||
|
[1,0,1,0, 0,0,1,0, 0,0,1,0, 0,0,1,0],
|
||||||
|
[0,0,1,0, 1,0,1,0, 0,0,0,0, 0,0,1,1],
|
||||||
|
|
||||||
|
[1,0,1,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
|
||||||
|
[0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,1,1],
|
||||||
|
[1,0,1,0, 0,0,0,0, 0,0,0,0, 1,0,1,0],
|
||||||
|
[0,0,0,0, 0,0,1,0, 0,0,0,0, 0,0,1,1],
|
||||||
|
|
||||||
|
[1,0,1,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
|
||||||
|
[0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,1,1],
|
||||||
|
[1,0,1,0, 0,0,0,0, 0,0,0,0, 1,0,1,0],
|
||||||
|
[0,0,0,0, 0,0,1,0, 0,0,0,0, 0,0,0,0],
|
||||||
|
|
||||||
|
[1,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
|
||||||
|
]
|
||||||
|
|
||||||
|
const SYNC_TEST_DATA: Array[Array] = [
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
[1,1,1,1],
|
||||||
|
|
||||||
|
[1,0,0,0],
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
static func get_chart_data(chart: Chart) -> Array[Array]:
|
||||||
|
match chart:
|
||||||
|
ChartData.Chart.THE_COMEBACK:
|
||||||
|
return ChartData.THE_COMEBACK_DATA
|
||||||
|
ChartData.Chart.SYNC_TEST:
|
||||||
|
return ChartData.SYNC_TEST_DATA
|
||||||
|
_:
|
||||||
|
assert(false, "Unknown chart: %d" % chart)
|
||||||
|
return ChartData.THE_COMEBACK_DATA
|
||||||
1
audio/rhythm_game/globals/chart_data.gd.uid
Normal file
1
audio/rhythm_game/globals/chart_data.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ciu0moyvmacic
|
||||||
15
audio/rhythm_game/globals/enums.gd
Normal file
15
audio/rhythm_game/globals/enums.gd
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
## Global enums.
|
||||||
|
class_name Enums
|
||||||
|
|
||||||
|
enum TimeType {
|
||||||
|
FILTERED,
|
||||||
|
RAW,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum HitType {
|
||||||
|
MISS_EARLY,
|
||||||
|
GOOD_EARLY,
|
||||||
|
PERFECT,
|
||||||
|
GOOD_LATE,
|
||||||
|
MISS_LATE,
|
||||||
|
}
|
||||||
1
audio/rhythm_game/globals/enums.gd.uid
Normal file
1
audio/rhythm_game/globals/enums.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://xcrq8x2xiprj
|
||||||
17
audio/rhythm_game/globals/global_settings.gd
Normal file
17
audio/rhythm_game/globals/global_settings.gd
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
signal scroll_speed_changed(speed: float)
|
||||||
|
|
||||||
|
@export var use_filtered_playback: bool = true
|
||||||
|
|
||||||
|
@export var enable_metronome: bool = false
|
||||||
|
@export var input_latency_ms: int = 20
|
||||||
|
|
||||||
|
@export var scroll_speed: float = 400:
|
||||||
|
set(value):
|
||||||
|
if scroll_speed != value:
|
||||||
|
scroll_speed = value
|
||||||
|
scroll_speed_changed.emit(value)
|
||||||
|
@export var show_offsets: bool = false
|
||||||
|
|
||||||
|
@export var selected_chart: ChartData.Chart = ChartData.Chart.THE_COMEBACK
|
||||||
1
audio/rhythm_game/globals/global_settings.gd.uid
Normal file
1
audio/rhythm_game/globals/global_settings.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dxaqpkhijmwxf
|
||||||
45
audio/rhythm_game/globals/one_euro_filter.gd
Normal file
45
audio/rhythm_game/globals/one_euro_filter.gd
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Copyright (c) 2023 Patryk Kalinowski (patrykkalinowski.com)
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
## Implementation of the 1€ filter (https://gery.casiez.net/1euro/).[br]
|
||||||
|
## Modification of https://github.com/patrykkalinowski/godot-xr-kit/blob/master/addons/xr-kit/smooth-input-filter/scripts/one_euro_filter.gd
|
||||||
|
class_name OneEuroFilter
|
||||||
|
|
||||||
|
var min_cutoff: float
|
||||||
|
var beta: float
|
||||||
|
var d_cutoff: float
|
||||||
|
var x_filter: LowPassFilter
|
||||||
|
var dx_filter: LowPassFilter
|
||||||
|
|
||||||
|
func _init(args: Variant) -> void:
|
||||||
|
min_cutoff = args.cutoff
|
||||||
|
beta = args.beta
|
||||||
|
d_cutoff = args.cutoff
|
||||||
|
x_filter = LowPassFilter.new()
|
||||||
|
dx_filter = LowPassFilter.new()
|
||||||
|
|
||||||
|
func alpha(rate: float, cutoff: float) -> float:
|
||||||
|
var tau: float = 1.0 / (2 * PI * cutoff)
|
||||||
|
var te: float = 1.0 / rate
|
||||||
|
|
||||||
|
return 1.0 / (1.0 + tau/te)
|
||||||
|
|
||||||
|
func filter(value: float, delta: float) -> float:
|
||||||
|
var rate: float = 1.0 / delta
|
||||||
|
var dx: float = (value - x_filter.last_value) * rate
|
||||||
|
|
||||||
|
var edx: float = dx_filter.filter(dx, alpha(rate, d_cutoff))
|
||||||
|
var cutoff: float = min_cutoff + beta * abs(edx)
|
||||||
|
return x_filter.filter(value, alpha(rate, cutoff))
|
||||||
|
|
||||||
|
class LowPassFilter:
|
||||||
|
var last_value: float
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
last_value = 0
|
||||||
|
|
||||||
|
func filter(value: float, alpha: float) -> float:
|
||||||
|
var result := alpha * value + (1 - alpha) * last_value
|
||||||
|
last_value = result
|
||||||
|
|
||||||
|
return result
|
||||||
1
audio/rhythm_game/globals/one_euro_filter.gd.uid
Normal file
1
audio/rhythm_game/globals/one_euro_filter.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cig8iydqhxf1i
|
||||||
BIN
audio/rhythm_game/icon.webp
Normal file
BIN
audio/rhythm_game/icon.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 208 B |
34
audio/rhythm_game/icon.webp.import
Normal file
34
audio/rhythm_game/icon.webp.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bdd4ws8b7jdqh"
|
||||||
|
path="res://.godot/imported/icon.webp-e94f9a68b0f625a567a797079e4d325f.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://icon.webp"
|
||||||
|
dest_files=["res://.godot/imported/icon.webp-e94f9a68b0f625a567a797079e4d325f.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
audio/rhythm_game/music/Perc_MetronomeQuartz_hi.wav
Normal file
BIN
audio/rhythm_game/music/Perc_MetronomeQuartz_hi.wav
Normal file
Binary file not shown.
24
audio/rhythm_game/music/Perc_MetronomeQuartz_hi.wav.import
Normal file
24
audio/rhythm_game/music/Perc_MetronomeQuartz_hi.wav.import
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://dbs7gpp3wnsrd"
|
||||||
|
path="res://.godot/imported/Perc_MetronomeQuartz_hi.wav-3da327bdefbd27ee612e1cef533f46ee.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://music/Perc_MetronomeQuartz_hi.wav"
|
||||||
|
dest_files=["res://.godot/imported/Perc_MetronomeQuartz_hi.wav-3da327bdefbd27ee612e1cef533f46ee.sample"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
force/8_bit=false
|
||||||
|
force/mono=false
|
||||||
|
force/max_rate=false
|
||||||
|
force/max_rate_hz=44100
|
||||||
|
edit/trim=false
|
||||||
|
edit/normalize=false
|
||||||
|
edit/loop_mode=0
|
||||||
|
edit/loop_begin=0
|
||||||
|
edit/loop_end=-1
|
||||||
|
compress/mode=2
|
||||||
BIN
audio/rhythm_game/music/the_comeback2.ogg
Normal file
BIN
audio/rhythm_game/music/the_comeback2.ogg
Normal file
Binary file not shown.
19
audio/rhythm_game/music/the_comeback2.ogg.import
Normal file
19
audio/rhythm_game/music/the_comeback2.ogg.import
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="oggvorbisstr"
|
||||||
|
type="AudioStreamOggVorbis"
|
||||||
|
uid="uid://cdrr8wk42fkfg"
|
||||||
|
path="res://.godot/imported/the_comeback2.ogg-c6401f04c274bd0ff2fb70a543ae6ac6.oggvorbisstr"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://music/the_comeback2.ogg"
|
||||||
|
dest_files=["res://.godot/imported/the_comeback2.ogg-c6401f04c274bd0ff2fb70a543ae6ac6.oggvorbisstr"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
loop=false
|
||||||
|
loop_offset=0.0
|
||||||
|
bpm=116.0
|
||||||
|
beat_count=0
|
||||||
|
bar_beats=4
|
||||||
12
audio/rhythm_game/objects/guide/guide.gd
Normal file
12
audio/rhythm_game/objects/guide/guide.gd
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
extends Sprite2D
|
||||||
|
|
||||||
|
var _guide_tween: Tween
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
if Input.is_action_just_pressed("main_key"):
|
||||||
|
scale = 1.2 * Vector2.ONE
|
||||||
|
if _guide_tween:
|
||||||
|
_guide_tween.kill()
|
||||||
|
_guide_tween = create_tween().set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC)
|
||||||
|
_guide_tween.tween_property(self, "scale", Vector2.ONE, 0.2)
|
||||||
1
audio/rhythm_game/objects/guide/guide.gd.uid
Normal file
1
audio/rhythm_game/objects/guide/guide.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://b6thxwiktedgx
|
||||||
BIN
audio/rhythm_game/objects/guide/guide.png
Normal file
BIN
audio/rhythm_game/objects/guide/guide.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 214 B |
34
audio/rhythm_game/objects/guide/guide.png.import
Normal file
34
audio/rhythm_game/objects/guide/guide.png.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dmatn1tgn5pn6"
|
||||||
|
path="res://.godot/imported/guide.png-36e780c483986c2cd13ea8423ff7de13.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://objects/guide/guide.png"
|
||||||
|
dest_files=["res://.godot/imported/guide.png-36e780c483986c2cd13ea8423ff7de13.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
8
audio/rhythm_game/objects/guide/guide.tscn
Normal file
8
audio/rhythm_game/objects/guide/guide.tscn
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[gd_scene load_steps=3 format=3 uid="uid://rrnv37orejuv"]
|
||||||
|
|
||||||
|
[ext_resource type="Texture2D" uid="uid://dmatn1tgn5pn6" path="res://objects/guide/guide.png" id="1_fpaen"]
|
||||||
|
[ext_resource type="Script" uid="uid://b6thxwiktedgx" path="res://objects/guide/guide.gd" id="2_euo2a"]
|
||||||
|
|
||||||
|
[node name="Guide" type="Sprite2D"]
|
||||||
|
texture = ExtResource("1_fpaen")
|
||||||
|
script = ExtResource("2_euo2a")
|
||||||
83
audio/rhythm_game/objects/note/note.gd
Normal file
83
audio/rhythm_game/objects/note/note.gd
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
class_name Note
|
||||||
|
extends Node2D
|
||||||
|
|
||||||
|
@export_category("Nodes")
|
||||||
|
@export var conductor: Conductor
|
||||||
|
|
||||||
|
@export_category("Settings")
|
||||||
|
@export var x_offset: float = 0
|
||||||
|
@export var beat: float = 0
|
||||||
|
|
||||||
|
var _speed: float
|
||||||
|
var _movement_paused: bool = false
|
||||||
|
var _song_time_delta: float = 0
|
||||||
|
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
_speed = GlobalSettings.scroll_speed
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
GlobalSettings.scroll_speed_changed.connect(_on_scroll_speed_changed)
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
if _movement_paused:
|
||||||
|
return
|
||||||
|
|
||||||
|
_update_position()
|
||||||
|
|
||||||
|
|
||||||
|
func update_beat(curr_beat: float) -> void:
|
||||||
|
_song_time_delta = (curr_beat - beat) * conductor.get_beat_duration()
|
||||||
|
|
||||||
|
_update_position()
|
||||||
|
|
||||||
|
|
||||||
|
func hit_perfect() -> void:
|
||||||
|
_movement_paused = true
|
||||||
|
|
||||||
|
modulate = Color.YELLOW
|
||||||
|
|
||||||
|
var tween := create_tween()
|
||||||
|
tween.set_ease(Tween.EASE_OUT)
|
||||||
|
tween.set_trans(Tween.TRANS_QUAD)
|
||||||
|
tween.parallel().tween_property(self, "modulate:a", 0, 0.2)
|
||||||
|
tween.parallel().tween_property($Sprite2D, "scale", 1.5 * Vector2.ONE, 0.2)
|
||||||
|
tween.tween_callback(queue_free)
|
||||||
|
|
||||||
|
|
||||||
|
func hit_good() -> void:
|
||||||
|
_movement_paused = true
|
||||||
|
|
||||||
|
modulate = Color.DEEP_SKY_BLUE
|
||||||
|
|
||||||
|
var tween := create_tween()
|
||||||
|
tween.set_ease(Tween.EASE_OUT)
|
||||||
|
tween.set_trans(Tween.TRANS_QUAD)
|
||||||
|
tween.parallel().tween_property(self, "modulate:a", 0, 0.2)
|
||||||
|
tween.parallel().tween_property($Sprite2D, "scale", 1.2 * Vector2.ONE, 0.2)
|
||||||
|
tween.tween_callback(queue_free)
|
||||||
|
|
||||||
|
|
||||||
|
func miss(stop_movement: bool = true) -> void:
|
||||||
|
_movement_paused = stop_movement
|
||||||
|
|
||||||
|
modulate = Color.DARK_RED
|
||||||
|
|
||||||
|
var tween := create_tween()
|
||||||
|
tween.parallel().tween_property(self, "modulate:a", 0, 0.5)
|
||||||
|
tween.tween_callback(queue_free)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_position() -> void:
|
||||||
|
if _song_time_delta > 0:
|
||||||
|
# Slow the note down past the judgment line.
|
||||||
|
position.y = _speed * _song_time_delta - _speed * pow(_song_time_delta, 2)
|
||||||
|
else:
|
||||||
|
position.y = _speed * _song_time_delta
|
||||||
|
position.x = x_offset
|
||||||
|
|
||||||
|
|
||||||
|
func _on_scroll_speed_changed(speed: float) -> void:
|
||||||
|
_speed = speed
|
||||||
1
audio/rhythm_game/objects/note/note.gd.uid
Normal file
1
audio/rhythm_game/objects/note/note.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dtp0l467x8dhf
|
||||||
BIN
audio/rhythm_game/objects/note/note.png
Normal file
BIN
audio/rhythm_game/objects/note/note.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 143 B |
34
audio/rhythm_game/objects/note/note.png.import
Normal file
34
audio/rhythm_game/objects/note/note.png.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://b7gri822pdstu"
|
||||||
|
path="res://.godot/imported/note.png-5d2572a024e219e8fd10eaed58eb7c40.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://objects/note/note.png"
|
||||||
|
dest_files=["res://.godot/imported/note.png-5d2572a024e219e8fd10eaed58eb7c40.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
10
audio/rhythm_game/objects/note/note.tscn
Normal file
10
audio/rhythm_game/objects/note/note.tscn
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[gd_scene load_steps=3 format=3 uid="uid://bkefp51bal7ci"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://dtp0l467x8dhf" path="res://objects/note/note.gd" id="1_doocs"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://b7gri822pdstu" path="res://objects/note/note.png" id="2_wn01w"]
|
||||||
|
|
||||||
|
[node name="Note" type="Node2D"]
|
||||||
|
script = ExtResource("1_doocs")
|
||||||
|
|
||||||
|
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||||
|
texture = ExtResource("2_wn01w")
|
||||||
65
audio/rhythm_game/project.godot
Normal file
65
audio/rhythm_game/project.godot
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
; Engine configuration file.
|
||||||
|
; It's best edited using the editor UI and not directly,
|
||||||
|
; since the parameters that go here are not all obvious.
|
||||||
|
;
|
||||||
|
; Format:
|
||||||
|
; [section] ; section goes between []
|
||||||
|
; param=value ; assign values to parameters
|
||||||
|
|
||||||
|
config_version=5
|
||||||
|
|
||||||
|
[application]
|
||||||
|
|
||||||
|
config/name="Rhythm Game"
|
||||||
|
config/description="Simple rhythm game utilizing precise playback position."
|
||||||
|
run/main_scene="uid://cv53erwosrk7o"
|
||||||
|
config/features=PackedStringArray("4.4", "GL Compatibility")
|
||||||
|
run/max_fps=1000
|
||||||
|
config/icon="uid://bdd4ws8b7jdqh"
|
||||||
|
|
||||||
|
[audio]
|
||||||
|
|
||||||
|
general/default_playback_type.web=0
|
||||||
|
|
||||||
|
[autoload]
|
||||||
|
|
||||||
|
GlobalSettings="*res://globals/global_settings.gd"
|
||||||
|
|
||||||
|
[debug]
|
||||||
|
|
||||||
|
gdscript/warnings/untyped_declaration=1
|
||||||
|
|
||||||
|
[display]
|
||||||
|
|
||||||
|
window/size/viewport_width=1280
|
||||||
|
window/size/viewport_height=720
|
||||||
|
window/stretch/mode="canvas_items"
|
||||||
|
window/stretch/aspect="expand"
|
||||||
|
window/vsync/vsync_mode=0
|
||||||
|
|
||||||
|
[input]
|
||||||
|
|
||||||
|
main_key={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":true,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
pause={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":80,"key_label":0,"unicode":112,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":6,"pressure":0.0,"pressed":true,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
restart={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":82,"key_label":0,"unicode":114,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":3,"pressure":0.0,"pressed":true,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
[rendering]
|
||||||
|
|
||||||
|
renderer/rendering_method="gl_compatibility"
|
||||||
|
renderer/rendering_method.mobile="gl_compatibility"
|
||||||
219
audio/rhythm_game/scenes/main/main.gd
Normal file
219
audio/rhythm_game/scenes/main/main.gd
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
extends Node2D
|
||||||
|
|
||||||
|
class NoteHitData:
|
||||||
|
var beat_time: float
|
||||||
|
var type: Enums.HitType
|
||||||
|
var error: float
|
||||||
|
|
||||||
|
@warning_ignore("shadowed_variable")
|
||||||
|
func _init(beat_time: float, type: Enums.HitType, error: float) -> void:
|
||||||
|
self.beat_time = beat_time
|
||||||
|
self.type = type
|
||||||
|
self.error = error
|
||||||
|
|
||||||
|
var _judgment_tween: Tween
|
||||||
|
var _hit_data: Array[NoteHitData] = []
|
||||||
|
|
||||||
|
|
||||||
|
func _enter_tree() -> void:
|
||||||
|
$Notes.chart = GlobalSettings.selected_chart
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
$Control/SettingsVBox/UseFilteredCheckBox.button_pressed = GlobalSettings.use_filtered_playback
|
||||||
|
$Control/SettingsVBox/ShowOffsetCheckBox.button_pressed = GlobalSettings.show_offsets
|
||||||
|
$Control/SettingsVBox/MetronomeCheckBox.button_pressed = GlobalSettings.enable_metronome
|
||||||
|
$Control/SettingsVBox/InputLatencyHBox/SpinBox.value = GlobalSettings.input_latency_ms
|
||||||
|
$Control/SettingsVBox/ScrollSpeedHBox/CenterContainer/HSlider.value = GlobalSettings.scroll_speed
|
||||||
|
$Control/ChartVBox/OptionButton.selected = GlobalSettings.selected_chart
|
||||||
|
$Control/JudgmentHBox/LJudgmentLabel.modulate.a = 0
|
||||||
|
$Control/JudgmentHBox/RJudgmentLabel.modulate.a = 0
|
||||||
|
|
||||||
|
var latency_line_edit: LineEdit = $Control/SettingsVBox/InputLatencyHBox/SpinBox.get_line_edit()
|
||||||
|
latency_line_edit.text_submitted.connect(
|
||||||
|
func(_text: String) -> void:
|
||||||
|
latency_line_edit.release_focus())
|
||||||
|
|
||||||
|
await get_tree().create_timer(0.5).timeout
|
||||||
|
|
||||||
|
$Conductor.play()
|
||||||
|
$Metronome.start()
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
if Input.is_action_just_pressed("restart"):
|
||||||
|
get_tree().reload_current_scene()
|
||||||
|
$Control/ErrorGraphVBox/CenterContainer/TimeGraph.queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _update_stats(play_stats: PlayStats) -> void:
|
||||||
|
$Control/StatsVBox/PerfectLabel.text = "Perfect: %d" % play_stats.perfect_count
|
||||||
|
$Control/StatsVBox/GoodLabel.text = "Good: %d" % play_stats.good_count
|
||||||
|
$Control/StatsVBox/MissLabel.text = "Miss: %d" % play_stats.miss_count
|
||||||
|
var hit_error_ms := play_stats.mean_hit_error * 1000
|
||||||
|
if hit_error_ms < 0:
|
||||||
|
$Control/StatsVBox/HitErrorLabel.text = "Avg Error: %+.1f ms (Early)" % hit_error_ms
|
||||||
|
else:
|
||||||
|
$Control/StatsVBox/HitErrorLabel.text = "Avg Error: %+.1f ms (Late)" % hit_error_ms
|
||||||
|
|
||||||
|
|
||||||
|
func _update_filter_state(use_filter: bool) -> void:
|
||||||
|
GlobalSettings.use_filtered_playback = use_filter
|
||||||
|
if use_filter:
|
||||||
|
$Notes.time_type = Enums.TimeType.FILTERED
|
||||||
|
else:
|
||||||
|
$Notes.time_type = Enums.TimeType.RAW
|
||||||
|
|
||||||
|
|
||||||
|
func _hit_type_to_string(hit_type: Enums.HitType) -> String:
|
||||||
|
match hit_type:
|
||||||
|
Enums.HitType.MISS_EARLY:
|
||||||
|
return "Too Early..."
|
||||||
|
Enums.HitType.GOOD_EARLY:
|
||||||
|
return "Good"
|
||||||
|
Enums.HitType.PERFECT:
|
||||||
|
return "Perfect!"
|
||||||
|
Enums.HitType.GOOD_LATE:
|
||||||
|
return "Good"
|
||||||
|
Enums.HitType.MISS_LATE:
|
||||||
|
return "Miss..."
|
||||||
|
_:
|
||||||
|
assert(false, "Unknown HitType: %s" % hit_type)
|
||||||
|
return "Unknown"
|
||||||
|
|
||||||
|
|
||||||
|
func _on_use_filtered_check_box_toggled(toggled_on: bool) -> void:
|
||||||
|
_update_filter_state(toggled_on)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_show_offset_check_box_toggled(toggled_on: bool) -> void:
|
||||||
|
GlobalSettings.show_offsets = toggled_on
|
||||||
|
|
||||||
|
|
||||||
|
func _on_metronome_check_box_toggled(toggled_on: bool) -> void:
|
||||||
|
GlobalSettings.enable_metronome = toggled_on
|
||||||
|
|
||||||
|
|
||||||
|
func _on_note_hit(beat: float, hit_type: Enums.HitType, hit_error: float) -> void:
|
||||||
|
var hit_type_str := _hit_type_to_string(hit_type)
|
||||||
|
if GlobalSettings.show_offsets:
|
||||||
|
var hit_error_ms := hit_error * 1000
|
||||||
|
if hit_error_ms < 0:
|
||||||
|
hit_type_str += "\n(Early %+d ms)" % hit_error_ms
|
||||||
|
else:
|
||||||
|
hit_type_str += "\n(Late %+d ms)" % hit_error_ms
|
||||||
|
$Control/JudgmentHBox/LJudgmentLabel.text = hit_type_str
|
||||||
|
$Control/JudgmentHBox/RJudgmentLabel.text = hit_type_str
|
||||||
|
|
||||||
|
$Control/JudgmentHBox/LJudgmentLabel.modulate.a = 1
|
||||||
|
$Control/JudgmentHBox/RJudgmentLabel.modulate.a = 1
|
||||||
|
|
||||||
|
if _judgment_tween:
|
||||||
|
_judgment_tween.kill()
|
||||||
|
_judgment_tween = create_tween()
|
||||||
|
_judgment_tween.tween_interval(0.2)
|
||||||
|
_judgment_tween.tween_property($Control/JudgmentHBox/LJudgmentLabel, "modulate:a", 0, 0.5)
|
||||||
|
_judgment_tween.parallel().tween_property($Control/JudgmentHBox/RJudgmentLabel, "modulate:a", 0, 0.5)
|
||||||
|
|
||||||
|
_hit_data.append(NoteHitData.new(beat, hit_type, hit_error))
|
||||||
|
$Control/ErrorGraphVBox/CenterContainer/JudgmentsGraph.queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_play_stats_updated(play_stats: PlayStats) -> void:
|
||||||
|
_update_stats(play_stats)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_song_finished(play_stats: PlayStats) -> void:
|
||||||
|
$Control/SongCompleteLabel.show()
|
||||||
|
_update_stats(play_stats)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_input_latency_spin_box_value_changed(value: float) -> void:
|
||||||
|
var latency_ms := roundi(value)
|
||||||
|
GlobalSettings.input_latency_ms = latency_ms
|
||||||
|
$Control/SettingsVBox/InputLatencyHBox/SpinBox.get_line_edit().release_focus()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_scroll_speed_h_slider_value_changed(value: float) -> void:
|
||||||
|
GlobalSettings.scroll_speed = value
|
||||||
|
$Control/SettingsVBox/ScrollSpeedHBox/Label.text = str(roundi(value))
|
||||||
|
|
||||||
|
|
||||||
|
func _on_chart_option_button_item_selected(index: int) -> void:
|
||||||
|
if GlobalSettings.selected_chart != index:
|
||||||
|
GlobalSettings.selected_chart = index as ChartData.Chart
|
||||||
|
get_tree().reload_current_scene()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_judgments_graph_draw() -> void:
|
||||||
|
var graph: Control = $Control/ErrorGraphVBox/CenterContainer/JudgmentsGraph
|
||||||
|
var song_beats := ChartData.get_chart_data(GlobalSettings.selected_chart).size() * 4
|
||||||
|
|
||||||
|
# Draw horizontal lines for judgment edges
|
||||||
|
var abs_error_bound := NoteManager.HIT_MARGIN_GOOD + 0.01
|
||||||
|
var early_edge_good_y: float = remap(
|
||||||
|
-NoteManager.HIT_MARGIN_GOOD,
|
||||||
|
-abs_error_bound, abs_error_bound,
|
||||||
|
0, graph.size.y)
|
||||||
|
var early_edge_perfect_y: float = remap(
|
||||||
|
-NoteManager.HIT_MARGIN_PERFECT,
|
||||||
|
-abs_error_bound, abs_error_bound,
|
||||||
|
0, graph.size.y)
|
||||||
|
var late_edge_perfect_y: float = remap(
|
||||||
|
NoteManager.HIT_MARGIN_PERFECT,
|
||||||
|
-abs_error_bound, abs_error_bound,
|
||||||
|
0, graph.size.y)
|
||||||
|
var late_edge_good_y: float = remap(
|
||||||
|
NoteManager.HIT_MARGIN_GOOD,
|
||||||
|
-abs_error_bound, abs_error_bound,
|
||||||
|
0, graph.size.y)
|
||||||
|
graph.draw_line(
|
||||||
|
Vector2(0, early_edge_good_y),
|
||||||
|
Vector2(graph.size.x, early_edge_good_y),
|
||||||
|
Color.DIM_GRAY)
|
||||||
|
graph.draw_line(
|
||||||
|
Vector2(0, early_edge_perfect_y),
|
||||||
|
Vector2(graph.size.x, early_edge_perfect_y),
|
||||||
|
Color.DIM_GRAY)
|
||||||
|
graph.draw_line(
|
||||||
|
Vector2(0, graph.size.y / 2),
|
||||||
|
Vector2(graph.size.x, graph.size.y / 2),
|
||||||
|
Color.WHITE)
|
||||||
|
graph.draw_line(
|
||||||
|
Vector2(0, late_edge_perfect_y),
|
||||||
|
Vector2(graph.size.x, late_edge_perfect_y),
|
||||||
|
Color.DIM_GRAY)
|
||||||
|
graph.draw_line(
|
||||||
|
Vector2(0, late_edge_good_y),
|
||||||
|
Vector2(graph.size.x, late_edge_good_y),
|
||||||
|
Color.DIM_GRAY)
|
||||||
|
|
||||||
|
# Draw the judgments on the graph
|
||||||
|
for data in _hit_data:
|
||||||
|
var error := data.error
|
||||||
|
var color: Color
|
||||||
|
match data.type:
|
||||||
|
Enums.HitType.MISS_EARLY:
|
||||||
|
error = -NoteManager.HIT_MARGIN_GOOD - 0.005
|
||||||
|
color = Color.DARK_RED
|
||||||
|
Enums.HitType.MISS_LATE:
|
||||||
|
error = NoteManager.HIT_MARGIN_GOOD + 0.005
|
||||||
|
color = Color.DARK_RED
|
||||||
|
Enums.HitType.GOOD_EARLY, Enums.HitType.GOOD_LATE:
|
||||||
|
color = Color.DEEP_SKY_BLUE
|
||||||
|
Enums.HitType.PERFECT:
|
||||||
|
color = Color.YELLOW
|
||||||
|
_:
|
||||||
|
assert(false, "Unknown hit type: %d" % data.type)
|
||||||
|
color = Color.WHITE
|
||||||
|
var px: float = round(remap(data.beat_time, 0, song_beats, 0, graph.size.x))
|
||||||
|
var py: float = round(remap(error, -abs_error_bound, abs_error_bound, 0, graph.size.y))
|
||||||
|
graph.draw_rect(Rect2(px-1, py-1, 3, 3), Color(color, 0.8))
|
||||||
|
|
||||||
|
|
||||||
|
func _on_time_graph_draw() -> void:
|
||||||
|
var graph: Control = $Control/ErrorGraphVBox/CenterContainer/TimeGraph
|
||||||
|
var song_beats := ChartData.get_chart_data(GlobalSettings.selected_chart).size() * 4
|
||||||
|
var curr_beat := clampf($Conductor.get_current_beat(), 0, song_beats)
|
||||||
|
var time_x: float = remap(curr_beat, 0, song_beats, 0, graph.size.x)
|
||||||
|
graph.draw_line(Vector2(time_x, 0), Vector2(time_x, graph.size.y), Color.WHITE, 2)
|
||||||
1
audio/rhythm_game/scenes/main/main.gd.uid
Normal file
1
audio/rhythm_game/scenes/main/main.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bw05ipplwsl4v
|
||||||
282
audio/rhythm_game/scenes/main/main.tscn
Normal file
282
audio/rhythm_game/scenes/main/main.tscn
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
[gd_scene load_steps=9 format=3 uid="uid://cv53erwosrk7o"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://bw05ipplwsl4v" path="res://scenes/main/main.gd" id="1_0xm2m"]
|
||||||
|
[ext_resource type="Script" uid="uid://dxdm5hivq6xkf" path="res://game_state/conductor.gd" id="2_1bvp3"]
|
||||||
|
[ext_resource type="Script" uid="uid://c7jdh7pv088ja" path="res://scenes/main/pause_handler.gd" id="2_7mycd"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://cdrr8wk42fkfg" path="res://music/the_comeback2.ogg" id="2_h2yge"]
|
||||||
|
[ext_resource type="Script" uid="uid://d0qi52a8nkb6o" path="res://game_state/note_manager.gd" id="4_lquwl"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://rrnv37orejuv" path="res://objects/guide/guide.tscn" id="6_ow5a4"]
|
||||||
|
[ext_resource type="Script" uid="uid://gd4p06mb2biq" path="res://game_state/metronome.gd" id="7_hujxm"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://dbs7gpp3wnsrd" path="res://music/Perc_MetronomeQuartz_hi.wav" id="8_yyfjg"]
|
||||||
|
|
||||||
|
[node name="Main" type="Node2D"]
|
||||||
|
script = ExtResource("1_0xm2m")
|
||||||
|
|
||||||
|
[node name="PauseHandler" type="Node" parent="."]
|
||||||
|
process_mode = 3
|
||||||
|
script = ExtResource("2_7mycd")
|
||||||
|
|
||||||
|
[node name="Conductor" type="Node" parent="." node_paths=PackedStringArray("player")]
|
||||||
|
process_mode = 3
|
||||||
|
script = ExtResource("2_1bvp3")
|
||||||
|
player = NodePath("../Player")
|
||||||
|
bpm = 116.052
|
||||||
|
first_beat_offset_ms = 8283
|
||||||
|
|
||||||
|
[node name="Player" type="AudioStreamPlayer" parent="."]
|
||||||
|
stream = ExtResource("2_h2yge")
|
||||||
|
volume_db = -12.0
|
||||||
|
|
||||||
|
[node name="Notes" type="Node2D" parent="." node_paths=PackedStringArray("conductor")]
|
||||||
|
position = Vector2(640, 594)
|
||||||
|
script = ExtResource("4_lquwl")
|
||||||
|
conductor = NodePath("../Conductor")
|
||||||
|
|
||||||
|
[node name="Guide" parent="." instance=ExtResource("6_ow5a4")]
|
||||||
|
position = Vector2(640, 594)
|
||||||
|
|
||||||
|
[node name="Control" type="Control" parent="."]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 0
|
||||||
|
offset_right = 1280.0
|
||||||
|
offset_bottom = 720.0
|
||||||
|
|
||||||
|
[node name="TutorialLabel" type="Label" parent="Control"]
|
||||||
|
layout_mode = 1
|
||||||
|
offset_left = 16.0
|
||||||
|
offset_top = 16.0
|
||||||
|
offset_right = 365.0
|
||||||
|
offset_bottom = 91.0
|
||||||
|
text = "Space to hit notes
|
||||||
|
Esc or P to pause
|
||||||
|
R to restart"
|
||||||
|
|
||||||
|
[node name="SettingsVBox" type="VBoxContainer" parent="Control"]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 16.0
|
||||||
|
offset_top = 157.0
|
||||||
|
offset_right = 367.0
|
||||||
|
offset_bottom = 398.0
|
||||||
|
|
||||||
|
[node name="UseFilteredCheckBox" type="CheckBox" parent="Control/SettingsVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
focus_mode = 0
|
||||||
|
button_pressed = true
|
||||||
|
text = "Enable smoothing filter on playback position"
|
||||||
|
|
||||||
|
[node name="Control" type="Control" parent="Control/SettingsVBox"]
|
||||||
|
custom_minimum_size = Vector2(0, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="MetronomeCheckBox" type="CheckBox" parent="Control/SettingsVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
focus_mode = 0
|
||||||
|
text = "Enable metronome"
|
||||||
|
|
||||||
|
[node name="InputLatencyHBox" type="HBoxContainer" parent="Control/SettingsVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="Control/SettingsVBox/InputLatencyHBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Input Latency (ms):"
|
||||||
|
|
||||||
|
[node name="SpinBox" type="SpinBox" parent="Control/SettingsVBox/InputLatencyHBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
min_value = -100.0
|
||||||
|
max_value = 1000.0
|
||||||
|
value = 20.0
|
||||||
|
rounded = true
|
||||||
|
|
||||||
|
[node name="LatencyInstructionsLabel" type="Label" parent="Control/SettingsVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "(Increase this if you are hitting notes too late)"
|
||||||
|
|
||||||
|
[node name="Control2" type="Control" parent="Control/SettingsVBox"]
|
||||||
|
custom_minimum_size = Vector2(0, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="ScrollSpeedInstructionsLabel" type="Label" parent="Control/SettingsVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Scroll Speed:"
|
||||||
|
|
||||||
|
[node name="ScrollSpeedHBox" type="HBoxContainer" parent="Control/SettingsVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 12
|
||||||
|
|
||||||
|
[node name="CenterContainer" type="CenterContainer" parent="Control/SettingsVBox/ScrollSpeedHBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="HSlider" type="HSlider" parent="Control/SettingsVBox/ScrollSpeedHBox/CenterContainer"]
|
||||||
|
custom_minimum_size = Vector2(300, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
focus_mode = 0
|
||||||
|
min_value = 200.0
|
||||||
|
max_value = 1000.0
|
||||||
|
step = 50.0
|
||||||
|
value = 400.0
|
||||||
|
tick_count = 17
|
||||||
|
ticks_on_borders = true
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="Control/SettingsVBox/ScrollSpeedHBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "400"
|
||||||
|
|
||||||
|
[node name="ShowOffsetCheckBox" type="CheckBox" parent="Control/SettingsVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
focus_mode = 0
|
||||||
|
text = "Show offset on judgment"
|
||||||
|
|
||||||
|
[node name="StatsVBox" type="VBoxContainer" parent="Control"]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 900.0
|
||||||
|
offset_top = 361.0
|
||||||
|
offset_right = 1066.0
|
||||||
|
offset_bottom = 465.0
|
||||||
|
|
||||||
|
[node name="PerfectLabel" type="Label" parent="Control/StatsVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Perfect: 0"
|
||||||
|
|
||||||
|
[node name="GoodLabel" type="Label" parent="Control/StatsVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Good: 0"
|
||||||
|
|
||||||
|
[node name="MissLabel" type="Label" parent="Control/StatsVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Miss: 0"
|
||||||
|
|
||||||
|
[node name="HitErrorLabel" type="Label" parent="Control/StatsVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Avg Hit Offset: 0.0 ms"
|
||||||
|
|
||||||
|
[node name="ChartVBox" type="VBoxContainer" parent="Control"]
|
||||||
|
layout_mode = 2
|
||||||
|
offset_left = 900.0
|
||||||
|
offset_top = 60.0
|
||||||
|
offset_right = 1066.0
|
||||||
|
offset_bottom = 118.0
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="Control/ChartVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Chart:"
|
||||||
|
|
||||||
|
[node name="OptionButton" type="OptionButton" parent="Control/ChartVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
focus_mode = 0
|
||||||
|
selected = 0
|
||||||
|
allow_reselect = true
|
||||||
|
item_count = 2
|
||||||
|
popup/item_0/text = "The Comeback"
|
||||||
|
popup/item_0/id = 1
|
||||||
|
popup/item_1/text = "Sync Test"
|
||||||
|
popup/item_1/id = 0
|
||||||
|
|
||||||
|
[node name="SongCompleteLabel" type="Label" parent="Control"]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 8
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -79.5
|
||||||
|
offset_top = -37.5
|
||||||
|
offset_right = 79.5
|
||||||
|
offset_bottom = 37.5
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
theme_override_font_sizes/font_size = 40
|
||||||
|
text = "Song Complete!
|
||||||
|
|
||||||
|
Press R to play again"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
vertical_alignment = 1
|
||||||
|
|
||||||
|
[node name="PauseLabel" type="Label" parent="Control"]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 8
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -79.5
|
||||||
|
offset_top = -37.5
|
||||||
|
offset_right = 79.5
|
||||||
|
offset_bottom = 37.5
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
theme_override_font_sizes/font_size = 40
|
||||||
|
text = "Paused"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
vertical_alignment = 1
|
||||||
|
|
||||||
|
[node name="JudgmentHBox" type="HBoxContainer" parent="Control"]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 7
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 1.0
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = -20.0
|
||||||
|
offset_top = -184.0
|
||||||
|
offset_right = 20.0
|
||||||
|
offset_bottom = -144.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 0
|
||||||
|
theme_override_constants/separation = 200
|
||||||
|
|
||||||
|
[node name="LJudgmentLabel" type="Label" parent="Control/JudgmentHBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Perfect!!"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="RJudgmentLabel" type="Label" parent="Control/JudgmentHBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Perfect!!"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="ErrorGraphVBox" type="VBoxContainer" parent="Control"]
|
||||||
|
layout_mode = 1
|
||||||
|
offset_left = 900.0
|
||||||
|
offset_top = 518.0
|
||||||
|
offset_right = 1250.0
|
||||||
|
offset_bottom = 665.0
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="Control/ErrorGraphVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Error Graph:"
|
||||||
|
|
||||||
|
[node name="CenterContainer" type="CenterContainer" parent="Control/ErrorGraphVBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="GraphBackground" type="Panel" parent="Control/ErrorGraphVBox/CenterContainer"]
|
||||||
|
custom_minimum_size = Vector2(350, 120)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="TimeGraph" type="Control" parent="Control/ErrorGraphVBox/CenterContainer"]
|
||||||
|
custom_minimum_size = Vector2(350, 120)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="JudgmentsGraph" type="Control" parent="Control/ErrorGraphVBox/CenterContainer"]
|
||||||
|
custom_minimum_size = Vector2(350, 120)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Metronome" type="AudioStreamPlayer" parent="." node_paths=PackedStringArray("conductor")]
|
||||||
|
stream = ExtResource("8_yyfjg")
|
||||||
|
volume_db = 2.0
|
||||||
|
script = ExtResource("7_hujxm")
|
||||||
|
conductor = NodePath("../Conductor")
|
||||||
|
|
||||||
|
[connection signal="note_hit" from="Notes" to="." method="_on_note_hit"]
|
||||||
|
[connection signal="play_stats_updated" from="Notes" to="." method="_on_play_stats_updated"]
|
||||||
|
[connection signal="song_finished" from="Notes" to="." method="_on_song_finished"]
|
||||||
|
[connection signal="toggled" from="Control/SettingsVBox/UseFilteredCheckBox" to="." method="_on_use_filtered_check_box_toggled"]
|
||||||
|
[connection signal="toggled" from="Control/SettingsVBox/MetronomeCheckBox" to="." method="_on_metronome_check_box_toggled"]
|
||||||
|
[connection signal="value_changed" from="Control/SettingsVBox/InputLatencyHBox/SpinBox" to="." method="_on_input_latency_spin_box_value_changed"]
|
||||||
|
[connection signal="value_changed" from="Control/SettingsVBox/ScrollSpeedHBox/CenterContainer/HSlider" to="." method="_on_scroll_speed_h_slider_value_changed"]
|
||||||
|
[connection signal="toggled" from="Control/SettingsVBox/ShowOffsetCheckBox" to="." method="_on_show_offset_check_box_toggled"]
|
||||||
|
[connection signal="item_selected" from="Control/ChartVBox/OptionButton" to="." method="_on_chart_option_button_item_selected"]
|
||||||
|
[connection signal="draw" from="Control/ErrorGraphVBox/CenterContainer/TimeGraph" to="." method="_on_time_graph_draw"]
|
||||||
|
[connection signal="draw" from="Control/ErrorGraphVBox/CenterContainer/JudgmentsGraph" to="." method="_on_judgments_graph_draw"]
|
||||||
8
audio/rhythm_game/scenes/main/pause_handler.gd
Normal file
8
audio/rhythm_game/scenes/main/pause_handler.gd
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Pause logic is separated out since it needs to run with PROCESS_MODE_ALWAYS.
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
if Input.is_action_just_pressed("pause"):
|
||||||
|
get_tree().paused = not get_tree().paused
|
||||||
|
$"../Control/PauseLabel".visible = get_tree().paused
|
||||||
1
audio/rhythm_game/scenes/main/pause_handler.gd.uid
Normal file
1
audio/rhythm_game/scenes/main/pause_handler.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://c7jdh7pv088ja
|
||||||
0
audio/rhythm_game/screenshots/.gdignore
Normal file
0
audio/rhythm_game/screenshots/.gdignore
Normal file
BIN
audio/rhythm_game/screenshots/rhythm_game.webp
Normal file
BIN
audio/rhythm_game/screenshots/rhythm_game.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Reference in New Issue
Block a user