Refactor rhythm action architecture
This commit is contained in:
167
autoload/rhythm_manager.gd
Normal file
167
autoload/rhythm_manager.gd
Normal file
@@ -0,0 +1,167 @@
|
||||
extends AudioStreamPlayer
|
||||
|
||||
signal beat_ticked(beat_index: int)
|
||||
signal judgement_made(quality: StringName, offset_ms: float)
|
||||
signal action_judged(action_name: StringName, rating: Dictionary)
|
||||
|
||||
@export var bpm: float = 120.0:
|
||||
set(value):
|
||||
bpm = maxf(1.0, value)
|
||||
beat_time = 60.0 / bpm
|
||||
@export var measures := 4
|
||||
@export var beat_offset := 0.0
|
||||
@export var perfect_window := 0.060
|
||||
@export var good_window := 0.120
|
||||
@export var bad_window := 0.200
|
||||
@export var judgement_scale := 1.0
|
||||
@export var starts_on_ready := true
|
||||
|
||||
const DEFAULT_STREAM_PATH := "res://assets/audio/song.ogg"
|
||||
|
||||
var beat_time := 0.5
|
||||
var beat_index := 0
|
||||
var running := false
|
||||
|
||||
var _start_time_usec := 0
|
||||
var _last_reported_beat := -1
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if stream == null and DisplayServer.get_name() != "headless":
|
||||
stream = load(DEFAULT_STREAM_PATH)
|
||||
volume_db = -10.0
|
||||
beat_time = 60.0 / maxf(1.0, bpm)
|
||||
var bus := _event_bus_or_null()
|
||||
if bus != null and not bus.is_connected("rhythm_action_requested", _on_rhythm_action_requested):
|
||||
bus.connect("rhythm_action_requested", _on_rhythm_action_requested)
|
||||
if starts_on_ready:
|
||||
start()
|
||||
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
if not running:
|
||||
return
|
||||
var adjusted_position := _apply_beat_offset(song_position())
|
||||
beat_index = int(floor(adjusted_position / beat_time))
|
||||
if _last_reported_beat < beat_index:
|
||||
_last_reported_beat = beat_index
|
||||
beat_ticked.emit(beat_index)
|
||||
var bus := _event_bus_or_null()
|
||||
if bus != null:
|
||||
bus.emit_signal("beat_ticked", beat_index)
|
||||
|
||||
|
||||
func configure(next_bpm: float, next_measures: int, next_beat_offset: float, next_windows := Vector3(0.060, 0.120, 0.200)) -> void:
|
||||
bpm = next_bpm
|
||||
measures = next_measures
|
||||
beat_offset = next_beat_offset
|
||||
perfect_window = next_windows.x
|
||||
good_window = next_windows.y
|
||||
bad_window = next_windows.z
|
||||
|
||||
|
||||
func start() -> void:
|
||||
running = true
|
||||
_start_time_usec = Time.get_ticks_usec()
|
||||
beat_index = 0
|
||||
_last_reported_beat = -1
|
||||
if stream != null and not playing:
|
||||
play()
|
||||
|
||||
|
||||
func stop_manager() -> void:
|
||||
if playing:
|
||||
stop()
|
||||
running = false
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if playing:
|
||||
stop()
|
||||
stream = null
|
||||
|
||||
|
||||
func song_position() -> float:
|
||||
if running and playing:
|
||||
var current_position := get_playback_position() + AudioServer.get_time_since_last_mix()
|
||||
current_position -= AudioServer.get_output_latency()
|
||||
return maxf(0.0, current_position)
|
||||
if running:
|
||||
return float(Time.get_ticks_usec() - _start_time_usec) / 1000000.0
|
||||
return 0.0
|
||||
|
||||
|
||||
func judge(input_timestamp_ms: float) -> Dictionary:
|
||||
return get_rating_for_time(input_timestamp_ms / 1000.0)
|
||||
|
||||
|
||||
func judge_action(action_name: StringName) -> Dictionary:
|
||||
var rating := get_current_rating()
|
||||
rating["action"] = action_name
|
||||
action_judged.emit(action_name, rating)
|
||||
judgement_made.emit(StringName(str(rating.get("label", "miss"))), float(rating.get("diff", INF)) * 1000.0)
|
||||
var bus := _event_bus_or_null()
|
||||
if bus != null:
|
||||
bus.emit_signal("judgement_made", StringName(str(rating.get("label", "miss"))), float(rating.get("diff", INF)) * 1000.0)
|
||||
bus.emit_signal("action_judged", action_name, rating)
|
||||
return rating
|
||||
|
||||
|
||||
func get_current_rating() -> Dictionary:
|
||||
return get_rating_for_time(song_position())
|
||||
|
||||
|
||||
func get_rating_for_time(time_seconds: float) -> Dictionary:
|
||||
var adjusted_time := _apply_beat_offset(time_seconds)
|
||||
if adjusted_time < 0.0:
|
||||
return _rating_result(&"miss", Color("ff0055"), 0, 0.0, INF, INF)
|
||||
|
||||
var nearest_beat := int(round(adjusted_time / beat_time))
|
||||
var nearest_beat_time := nearest_beat * beat_time
|
||||
var diff := adjusted_time - nearest_beat_time
|
||||
var abs_diff := absf(diff)
|
||||
var scale := maxf(0.01, judgement_scale)
|
||||
|
||||
if abs_diff <= perfect_window * scale:
|
||||
return _rating_result(&"perfect", Color("00f2ff"), nearest_beat, nearest_beat_time, diff, abs_diff)
|
||||
if abs_diff <= good_window * scale:
|
||||
return _rating_result(&"good", Color("ffffff"), nearest_beat, nearest_beat_time, diff, abs_diff)
|
||||
if abs_diff <= bad_window * scale:
|
||||
return _rating_result(&"bad", Color("ffaa00"), nearest_beat, nearest_beat_time, diff, abs_diff)
|
||||
return _rating_result(&"miss", Color("ff0055"), nearest_beat, nearest_beat_time, diff, abs_diff)
|
||||
|
||||
|
||||
func get_current_beat_progress() -> float:
|
||||
return get_beat_progress_for_time(song_position())
|
||||
|
||||
|
||||
func get_beat_progress_for_time(time_seconds: float) -> float:
|
||||
var adjusted_time := _apply_beat_offset(time_seconds)
|
||||
if adjusted_time < 0.0:
|
||||
return 0.0
|
||||
return fposmod(adjusted_time, beat_time) / beat_time
|
||||
|
||||
|
||||
func _on_rhythm_action_requested(action_name: StringName) -> void:
|
||||
judge_action(action_name)
|
||||
|
||||
|
||||
func _apply_beat_offset(time_seconds: float) -> float:
|
||||
return time_seconds + beat_offset
|
||||
|
||||
|
||||
func _rating_result(label: StringName, color: Color, nearest_beat: int, nearest_beat_time: float, diff: float, abs_diff: float) -> Dictionary:
|
||||
return {
|
||||
"label": str(label),
|
||||
"color": color,
|
||||
"nearest_beat": nearest_beat,
|
||||
"nearest_beat_time": nearest_beat_time,
|
||||
"diff": diff,
|
||||
"abs_diff": abs_diff,
|
||||
}
|
||||
|
||||
|
||||
func _event_bus_or_null() -> Node:
|
||||
if not is_inside_tree():
|
||||
return null
|
||||
return get_tree().root.get_node_or_null("EventBus")
|
||||
Reference in New Issue
Block a user