commit f16cf0fb964d777d7e66a40b5cf18ce8a6796be4 Author: Turtike Date: Mon Jan 19 22:48:17 2026 +0800 Initial commit. Implement music sync features. diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f28239b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +root = true + +[*] +charset = utf-8 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..99bf096 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Godot 4+ specific ignores +.godot/ +/android/ + +# Ignore folders starting with . +.*/ \ No newline at end of file diff --git a/chart/test_nibelungen/audio.mp3 b/chart/test_nibelungen/audio.mp3 new file mode 100644 index 0000000..65429d8 Binary files /dev/null and b/chart/test_nibelungen/audio.mp3 differ diff --git a/chart/test_nibelungen/audio.mp3.import b/chart/test_nibelungen/audio.mp3.import new file mode 100644 index 0000000..e261ee7 --- /dev/null +++ b/chart/test_nibelungen/audio.mp3.import @@ -0,0 +1,19 @@ +[remap] + +importer="mp3" +type="AudioStreamMP3" +uid="uid://btmy8ffph5gn3" +path="res://.godot/imported/audio.mp3-a6ce572dbede3712a85c3e444692049c.mp3str" + +[deps] + +source_file="res://chart/test_nibelungen/audio.mp3" +dest_files=["res://.godot/imported/audio.mp3-a6ce572dbede3712a85c3e444692049c.mp3str"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/chart/test_nibelungen/tempo.tres b/chart/test_nibelungen/tempo.tres new file mode 100644 index 0000000..26753a2 --- /dev/null +++ b/chart/test_nibelungen/tempo.tres @@ -0,0 +1,9 @@ +[gd_resource type="Resource" script_class="Tempo" load_steps=2 format=3 uid="uid://b34uhnkvwfyc1"] + +[ext_resource type="Script" uid="uid://c3vwmf1f1woo3" path="res://resource_type/tempo.gd" id="1_yhdoo"] + +[resource] +script = ExtResource("1_yhdoo") +bpms = Array[float]([160.0, 222.0, 200.0, 190.0, 170.0, 150.0, 135.0]) +lengths = Array[float]([51.0, 638.0, 640.0, 641.0, 642.0, 644.0]) +metadata/_custom_type_script = "uid://c3vwmf1f1woo3" diff --git a/image/godot_icon.svg b/image/godot_icon.svg new file mode 100644 index 0000000..c6bbb7d --- /dev/null +++ b/image/godot_icon.svg @@ -0,0 +1 @@ + diff --git a/image/godot_icon.svg.import b/image/godot_icon.svg.import new file mode 100644 index 0000000..a9f34a3 --- /dev/null +++ b/image/godot_icon.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://m4o76cw7njx2" +path="res://.godot/imported/godot_icon.svg-701d499c081c2a6850df1a5de9e7d612.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://image/godot_icon.svg" +dest_files=["res://.godot/imported/godot_icon.svg-701d499c081c2a6850df1a5de9e7d612.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..9094a3a --- /dev/null +++ b/project.godot @@ -0,0 +1,21 @@ +; 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="SolarBeat" +run/main_scene="uid://dwro2d0482v0p" +config/features=PackedStringArray("4.5", "GL Compatibility") +config/icon="res://image/godot_icon.svg" + +[rendering] + +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility" diff --git a/resource_type/chart.gd b/resource_type/chart.gd new file mode 100644 index 0000000..59564d7 --- /dev/null +++ b/resource_type/chart.gd @@ -0,0 +1,4 @@ +## Contains all the notes and events that make up a chart. +class_name Chart extends Resource + +var notes: Array[Note] diff --git a/resource_type/chart.gd.uid b/resource_type/chart.gd.uid new file mode 100644 index 0000000..2965f5b --- /dev/null +++ b/resource_type/chart.gd.uid @@ -0,0 +1 @@ +uid://oih6cupm4xnu diff --git a/resource_type/music.gd b/resource_type/music.gd new file mode 100644 index 0000000..a9d8ce8 --- /dev/null +++ b/resource_type/music.gd @@ -0,0 +1,10 @@ +class_name Music extends Resource + +## Music AudioStream. +@export var stream: AudioStream = null + +## Music offset in seconds. +@export var offset: float = 0.0 + +## BPM data to sync to the music. +@export var tempo: Tempo = Tempo.new() diff --git a/resource_type/music.gd.uid b/resource_type/music.gd.uid new file mode 100644 index 0000000..e095a36 --- /dev/null +++ b/resource_type/music.gd.uid @@ -0,0 +1 @@ +uid://rg6orh6kutai diff --git a/resource_type/song.gd b/resource_type/song.gd new file mode 100644 index 0000000..943bb20 --- /dev/null +++ b/resource_type/song.gd @@ -0,0 +1,13 @@ +## Represents a level for a music audio that may contain multiple charts. +class_name Song extends Resource + +var music: Music + +var charts: Dictionary[StringName, Chart] = {} + + +func has_music() -> bool: + return music != null and music.stream != null + +func has_chart() -> bool: + return not charts.is_empty() diff --git a/resource_type/song.gd.uid b/resource_type/song.gd.uid new file mode 100644 index 0000000..727ab61 --- /dev/null +++ b/resource_type/song.gd.uid @@ -0,0 +1 @@ +uid://jcm4r2avbciq diff --git a/resource_type/tempo.gd b/resource_type/tempo.gd new file mode 100644 index 0000000..6aa408f --- /dev/null +++ b/resource_type/tempo.gd @@ -0,0 +1,75 @@ +## Resource for storing BPM data and loading into a SyncTrack. +class_name Tempo extends Resource + +## Defines how the BPM changes throughout its length. +enum BPM_TYPE { + HOLD = 0, ## BPM stays constant through the whole length. + LINEAR = 1 ## BPM linearly interpolates to the next BPM. +} + +# The data for each BPM is stored in the same index +# across the different arrays. +@export var bpms: Array[float] = [] +@export var lengths: Array[float] = [] +@export var types: Array[BPM_TYPE] = [] + + +## Get the number of valid BPM changes. +func size() -> int: + return min(bpms.size(), lengths.size() + 1) + + +## Ensure the arrays obey the following properties: +## all arrays are the same size (every BPM has a length and type), +## last element of BPM Length Beats is -1 (since the +## corresponding BPM is assumed to last the rest of the chart). +## Extra BPM or BPM Length values are truncated. +## If BPM Types must be resized greater, assume every BPM after is +## BPM_TYPE.HOLD. +func normalize() -> void: + var target_size := size() + bpms.resize(target_size) + lengths.resize(target_size) + types.resize(target_size) + + if target_size > 0: + _make_bpm_positive() + _make_lengths_positive() + lengths[-1] = -1 + types[-1] = BPM_TYPE.HOLD + + +#----- Static Constructors -----# + +static func create_from_arrays( + p_bpms: Array[float], + p_lengths: Array[float], + p_types: Array[BPM_TYPE] + ) -> Tempo: + var result: Tempo = Tempo.new() + result.bpms = p_bpms + result.lengths = p_lengths + result.types = p_types + result.normalize() + return result + +static func create_from_nodes(_p_nodes: Array[Variant]) -> Tempo: + var result: Tempo = Tempo.new() + # Parse array and add data to result... + # Might not be used? + return result +#----- ------------- -----# + + +## Makes sure every BPM entry is positive. +## Negative BPMs are inverted. +func _make_bpm_positive() -> void: + for i: int in range(bpms.size()): + bpms[i] = abs(bpms[i]) + + +## Makes sure every length entry is positive. +## Negative lengths are inverted. +func _make_lengths_positive() -> void: + for i: int in range(lengths.size()): + lengths[i] = abs(lengths[i]) diff --git a/resource_type/tempo.gd.uid b/resource_type/tempo.gd.uid new file mode 100644 index 0000000..0b064dd --- /dev/null +++ b/resource_type/tempo.gd.uid @@ -0,0 +1 @@ +uid://c3vwmf1f1woo3 diff --git a/rhythm_game/lane/lane.gd b/rhythm_game/lane/lane.gd new file mode 100644 index 0000000..d5605dc --- /dev/null +++ b/rhythm_game/lane/lane.gd @@ -0,0 +1,9 @@ +class_name Lane extends Node2D + + +func get_hit_pos() -> Vector2: + return position + + +func update(_beat: BeatUpdate) -> void: + pass diff --git a/rhythm_game/lane/lane.gd.uid b/rhythm_game/lane/lane.gd.uid new file mode 100644 index 0000000..42db10f --- /dev/null +++ b/rhythm_game/lane/lane.gd.uid @@ -0,0 +1 @@ +uid://u42y08gn6bi7 diff --git a/rhythm_game/lane/lane.tscn b/rhythm_game/lane/lane.tscn new file mode 100644 index 0000000..429e5b9 --- /dev/null +++ b/rhythm_game/lane/lane.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://ch06ds4gr82nl"] + +[ext_resource type="Script" uid="uid://u42y08gn6bi7" path="res://rhythm_game/lane/lane.gd" id="1_p48sd"] + +[node name="Lane" type="Node2D"] +script = ExtResource("1_p48sd") diff --git a/rhythm_game/metronome.gd b/rhythm_game/metronome.gd new file mode 100644 index 0000000..090d0f1 --- /dev/null +++ b/rhythm_game/metronome.gd @@ -0,0 +1,5 @@ +class_name Metronome extends AudioStreamPlayer + +func on_conductor_ticked(update: BeatUpdate) -> void: + if update.new_beat(): + play() diff --git a/rhythm_game/metronome.gd.uid b/rhythm_game/metronome.gd.uid new file mode 100644 index 0000000..9ce78b1 --- /dev/null +++ b/rhythm_game/metronome.gd.uid @@ -0,0 +1 @@ +uid://bbpyym0kgujev diff --git a/rhythm_game/music_sync/beat_update.gd b/rhythm_game/music_sync/beat_update.gd new file mode 100644 index 0000000..7ec764f --- /dev/null +++ b/rhythm_game/music_sync/beat_update.gd @@ -0,0 +1,26 @@ +class_name BeatUpdate + +## Beat this frame. +var current: float + +## Beat last frame. +var previous: float + + +## Beat updated forward in time. +func forward() -> bool: + return current > previous + + +## Beat updated backward in time. +func backward() -> bool: + return current < previous + + +func new_beat() -> bool: + return floori(previous) != floori(current) + + +func _init(current_beat: float = 0.0, previous_beat: float = 0.0) -> void: + current = current_beat + previous = previous_beat diff --git a/rhythm_game/music_sync/beat_update.gd.uid b/rhythm_game/music_sync/beat_update.gd.uid new file mode 100644 index 0000000..e7462e2 --- /dev/null +++ b/rhythm_game/music_sync/beat_update.gd.uid @@ -0,0 +1 @@ +uid://dyjoi5i3iha0h diff --git a/rhythm_game/music_sync/conductor.gd b/rhythm_game/music_sync/conductor.gd new file mode 100644 index 0000000..dd94ceb --- /dev/null +++ b/rhythm_game/music_sync/conductor.gd @@ -0,0 +1,142 @@ +## Class for playing music and providing an interface to keep events in sync. +class_name Conductor extends Node + +## If true, start immediately when ready. +@export var autostart: bool = false + +## Music to play. +@export var music: Music + +signal started +signal ticked(beat_update: BeatUpdate) +signal ticked_forward(new_beat: float) +#signal ticked_backward(new_beat: float) +signal finished + + +func start() -> void: + assert(music != null) + assert(_audio_player != null) + + _stop_playback_and_initialize() + _active = true + started.emit() + + +func is_active() -> bool: + return _active + + +func current_time() -> float: + return(_audio_player.get_playback_position() + + AudioServer.get_time_since_last_mix() + + _music_offset - _remaining_silence) + + +func previous_time() -> float: + return _previous_time + + +func current_beat() -> float: + return _sync_track.to_beat(current_time()) + + +func previous_beat() -> float: + return _previous_beat + + +func current_bpm() -> float: + return _sync_track.bpm_at_time(current_time()) + + +func add_time(seconds: float) -> void: + if _audio_player == null: + return + _audio_player.seek(_audio_player.get_playback_position() + seconds) + _previous_time = current_time() + _previous_beat = current_beat() + + + +# ====== IMPLEMENTATION ====== # + +var _active: bool = false + +var _previous_time: float = -1.0 +var _previous_beat: float = -1.0 + +var _audio_player: AudioStreamPlayer = null +var _sync_track: SyncTrack = SyncTrack.new() + +var _music_offset: float +var _music_playing: bool = false +var _remaining_silence: float = 0.0 + + +func _ready() -> void: + _audio_player = AudioStreamPlayer.new() + add_child(_audio_player) + _audio_player.finished.connect(_on_music_finished) + + if autostart: + start() + + +func _on_music_finished() -> void: + _music_playing = false + _active = false + finished.emit() + + +func _process(delta: float) -> void: + if not _active: + return + + if _music_playing: + _beat_update() + else: + _update_silence(delta) + + +func _update_silence(elapsed_time: float) -> void: + _remaining_silence -= elapsed_time + + if _remaining_silence < 0: + _play_music_from_offset(-_remaining_silence) + _remaining_silence = 0.0 + + +func _beat_update() -> void: + var _current_time = current_time() + + # _audio_player.get_playback_position() used in current_time() + # is not perfect and may return a lesser value than last frame. + # This makes sure the beat never updates backward. + if _current_time <= _previous_time: + return + + var _current_beat = _sync_track.to_beat(_current_time) + ticked.emit(BeatUpdate.new(_current_beat, _previous_beat)) + ticked_forward.emit(_current_beat) + + _previous_beat = _current_beat + _previous_time = _current_time + + +func _stop_playback_and_initialize() -> void: + if _audio_player.playing: + _audio_player.stop() + + _active = false + _music_playing = false + _previous_time = -1.0 + _previous_beat = -1.0 + _music_offset = music.offset + _remaining_silence = _music_offset + _audio_player.stream = music.stream + _sync_track.initialize(music.tempo) + + +func _play_music_from_offset(offset: float) -> void: + _audio_player.play(offset) + _music_playing = true diff --git a/rhythm_game/music_sync/conductor.gd.uid b/rhythm_game/music_sync/conductor.gd.uid new file mode 100644 index 0000000..936e4fb --- /dev/null +++ b/rhythm_game/music_sync/conductor.gd.uid @@ -0,0 +1 @@ +uid://s16dt0bu0jrg diff --git a/rhythm_game/music_sync/event_layout.gd b/rhythm_game/music_sync/event_layout.gd new file mode 100644 index 0000000..c867205 --- /dev/null +++ b/rhythm_game/music_sync/event_layout.gd @@ -0,0 +1,25 @@ +class_name EventLayout extends Node + +#var _beats: Array[float] +#var _callables: Array[Callable] +#var _index: int = 0 + +### Schedule the callable to be called on the specified beat. +#func schedule(beat: float, callable: Callable) -> void: + #var i := schedule_beats.find_custom(func(s_beat: float): return s_beat > beat ) + #if i == -1: + #i = schedule_beats.size() + #schedule_beats.insert(i, beat) + #schedule_callables.insert(i, callable) +# +### Clears all scheduled callables. +#func reset_schedule() -> void: + #schedule_beats.clear() + #schedule_callables.clear() + #schedule_index = 0 + +# +#while(schedule_index < schedule_beats.size() + #and get_beat() >= schedule_beats.get(schedule_index)): + #schedule_callables.get(schedule_index).call() + #schedule_index += 1 diff --git a/rhythm_game/music_sync/event_layout.gd.uid b/rhythm_game/music_sync/event_layout.gd.uid new file mode 100644 index 0000000..8b681e4 --- /dev/null +++ b/rhythm_game/music_sync/event_layout.gd.uid @@ -0,0 +1 @@ +uid://102cl75cfpgw diff --git a/rhythm_game/music_sync/sync_track.gd b/rhythm_game/music_sync/sync_track.gd new file mode 100644 index 0000000..d4553cf --- /dev/null +++ b/rhythm_game/music_sync/sync_track.gd @@ -0,0 +1,167 @@ +## Class for converting beats to time (since start of music) and vice versa. +class_name SyncTrack extends Node + + +func initialize(tempo: Tempo) -> void: + _set_bpm_data(tempo) + _calculate_cache() + + +## Get the number of valid BPM changes. +func size() -> int: + return min(_bpms.size(), _lengths.size() + 1) + + +## Get the BPM at the given time (since music start). +func bpm_at_time(time: float) -> float: + var i: int = _get_bpm_index_at_time(time) + if _types[i] == Tempo.BPM_TYPE.LINEAR: + var delta = inverse_lerp(_elapsed_seconds[i], _elapsed_seconds[i+1], time) + return lerp(_bpms[i], _bpms[i+1], delta) + else: + return _bpms.get(i) + + +## Get the beat at the given time (since music start). +func to_beat(time: float) -> float: + var i: int = _get_bpm_index_at_time(time) + + # If i is not the last BPM: + if i < _bpms.size() - 1: + var delta = inverse_lerp(_elapsed_seconds[i], _elapsed_seconds[i+1], time) + return lerp(_elapsed_beats[i], _elapsed_beats[i+1], delta) + + # Else i is the last BPM: + else: + var bps: float = _bpms[i] / 60.0 + var remaining_seconds: float = time - _elapsed_seconds[i] + var remaining_beats: float = remaining_seconds * bps + return _elapsed_beats[i] + remaining_beats + + +## Get the time (since music start) at the given beat. +func to_time(beat: float) -> float: + var i: int = _get_bpm_index_at_beat(beat) + + if i == -1: + return -1.0 + + # Else if i is not the last BPM: + elif i < _bpms.size() - 1: + var delta = inverse_lerp(_elapsed_beats[i], _elapsed_beats[i+1], beat) + return lerp(_elapsed_seconds[i], _elapsed_seconds[i+1], delta) + + # Else i is the last BPM: + else: + var bps: float = _bpms[i] / 60.0 + var remaining_beats: float = beat - _elapsed_beats[i] + var remaining_seconds: float = remaining_beats / bps + return _elapsed_seconds[i] + remaining_seconds + + +func _init(tempo: Tempo = Tempo.new()) -> void: + initialize(tempo) + + +# IMPLEMENTATION + +# === Note about BPM data === +# The data for each BPM is stored in the same index +# across the different arrays. +# =========================== + +## BPM array (Beats per Minute). +var _bpms: Array[float] +## The duration of each BPM in beats. +var _lengths: Array[float] +## The BpmData.BPM_TYPE of each BPM. +var _types: Array[Tempo.BPM_TYPE] + +# Cache variables that make calculations easier. +## The duration of each BPM in seconds. +var _length_seconds: Array[float] +## Seconds passed before the corresponding BPM. +var _elapsed_seconds: Array[float] = [0.0] +## Beats passed before the corresponding BPM. +var _elapsed_beats: Array[float] = [0.0] + + +## Set the BPM data arrays. +func _set_bpm_data(tempo: Tempo) -> void: + + _clear_bpm_data() + tempo.normalize() + _bpms = tempo.bpms + _lengths = tempo.lengths + _types = tempo.types + + +func _calculate_cache() -> void: + _clear_cache() + for i: int in range(0, _bpms.size() - 1): + var bps: float + var length_in_beats: float = _lengths[i] + if _types[i] == Tempo.BPM_TYPE.LINEAR: + # Average BPS. + bps = (_bpms[i] + _bpms[i+1]) / 120.0 + else: + bps = _bpms[i] / 60.0 + var length_in_seconds: float = length_in_beats / bps + + _length_seconds.append(length_in_seconds) + _elapsed_seconds.append(_elapsed_seconds.back() + length_in_seconds) + _elapsed_beats.append(_elapsed_beats.back() + length_in_beats) + + if _length_seconds.size() > 0: + _length_seconds[-1] = -1.0 + + +func _clear_bpm_data() -> void: + _bpms = [] + _lengths = [] + _types = [] + + +func _clear_cache() -> void: + _length_seconds = [] + _elapsed_seconds = [0.0] + _elapsed_beats = [0.0] + + +func _get_bpm_index_at_time(time: float) -> int: + assert(_elapsed_seconds.size() > 0) + assert(_bpms.size() > 0) + + var _find_index: Callable = func (p_elapsed_seconds: float) -> bool: + return time < p_elapsed_seconds + + var index: int = _elapsed_seconds.find_custom(_find_index) + if index == -1: + index = _bpms.size() - 1 + else: + index -= 1 + return index + + +func _get_bpm_index_at_beat(beat: float) -> int: + var _find_index: Callable = func (p_elapsed_beats: float) -> bool: + return beat < p_elapsed_beats + + if _elapsed_beats.size() == 0: + return -1 + + var index: int = _elapsed_beats.find_custom(_find_index) + if index > 0: + index -= 1 + elif index == -1: + index = max(0, _bpms.size()-1) + return index + + +func _print_data() -> void: + print("=== SyncTrack ===") + print("BPMs: ", _bpms) + print("BPM Lengths (Beats): ", _lengths) + print("BPM Types: ", _types) + print("Elapsed Seconds: ", _elapsed_seconds) + print("Elapsed Beats: ", _elapsed_beats) diff --git a/rhythm_game/music_sync/sync_track.gd.uid b/rhythm_game/music_sync/sync_track.gd.uid new file mode 100644 index 0000000..5cb9b99 --- /dev/null +++ b/rhythm_game/music_sync/sync_track.gd.uid @@ -0,0 +1 @@ +uid://wbqg55ld6py5 diff --git a/rhythm_game/note.gd b/rhythm_game/note.gd new file mode 100644 index 0000000..76b0ee4 --- /dev/null +++ b/rhythm_game/note.gd @@ -0,0 +1,18 @@ +class_name Note extends Object + +enum TYPE { + TAP = 0, + HOLD_START = 1, + HOLD_END = 2 +} + +var hit_beat: float + +var type: TYPE + +var lane: int + +func _init(p_hit_beat: float = -99.0, p_lane: int = 0, p_type: TYPE = TYPE.TAP) -> void: + hit_beat = p_hit_beat + lane = p_lane + type = p_type diff --git a/rhythm_game/note.gd.uid b/rhythm_game/note.gd.uid new file mode 100644 index 0000000..173d9da --- /dev/null +++ b/rhythm_game/note.gd.uid @@ -0,0 +1 @@ +uid://cgxrnivh6rjje diff --git a/rhythm_game/note_layout.gd b/rhythm_game/note_layout.gd new file mode 100644 index 0000000..a33f0a0 --- /dev/null +++ b/rhythm_game/note_layout.gd @@ -0,0 +1,84 @@ +#TODO Split class into two. +# One should be responsible for storing arrays of data, like a NoteData class? +# Another represents a slice of the data, NoteView. +# Before that, figure out how the part of a hold note between the hold start +# and hold end will be implemented. +class_name NoteLayout extends Node + +## Controls at what beat notes are visible. +@export_group("Note View Offset") +## At what beat offset from the current beat will notes will be visible. +## Example, spawn = 4.0 means notes are spawned 4 beats before +## they are meant to be hit. +@export var spawn: float = 4.0 + +## At what beat offset from the current beat will notes be invisible. +## Example, despawn = 4.0 means notes are despawned 4 beats after +## they are meant to be hit. +@export var despawn: float = 4.0 + + +## The index in notes of the first active note. +func start() -> int: + return _start + + +## The index in notes after the last active note. +func end() -> int: + return _end + + +func size() -> int: + return _beat.size() + + +func beat(index: int) -> float: + return _beat[index] + + +func lane(index: int) -> int: + return _lane[index] + + +func type(index: int) -> Note.TYPE: + return _type[index] + + +# ======= IMPLEMENTATION ======== # + +# All notes in the chart, arranged by ascending beat. +var _beat: Array[float] = [] +var _lane: Array[int] = [] +var _type: Array[Note.TYPE] = [] + +var _start: int = 0 +var _end: int = 0 + + +func _push_note(_note: Note) -> void: + pass + + +# TODO FIX THESE +func _update_forward(new_beat: float) -> void: + var spawn_beat = new_beat + spawn + + while _end < size() and _beat[_end] <= spawn_beat: + _end += 1 + + var despawn_beat = new_beat - despawn + + while _start < size() and _beat[_start] < despawn_beat: + _start += 1 + +# TODO FIX THESE +#func _update_backward(new_beat: float) -> void: + #var spawn_beat = new_beat + note_spawn_offset + # + #while _end >= 0 and _beat[_end] > spawn_beat: + #_end -= 1 + # + #var despawn_beat = new_beat - note_despawn_offset + # + #while _start >= 0 and _beat[_start] < despawn_beat: + #_start -= 1 diff --git a/rhythm_game/note_layout.gd.uid b/rhythm_game/note_layout.gd.uid new file mode 100644 index 0000000..9fa00b2 --- /dev/null +++ b/rhythm_game/note_layout.gd.uid @@ -0,0 +1 @@ +uid://dlnnbx2wvn66t diff --git a/rhythm_game/rhythm_game.tscn b/rhythm_game/rhythm_game.tscn new file mode 100644 index 0000000..9d485e8 --- /dev/null +++ b/rhythm_game/rhythm_game.tscn @@ -0,0 +1,37 @@ +[gd_scene load_steps=10 format=3 uid="uid://dwro2d0482v0p"] + +[ext_resource type="Script" uid="uid://dlnnbx2wvn66t" path="res://rhythm_game/note_layout.gd" id="1_cr5rn"] +[ext_resource type="Script" uid="uid://102cl75cfpgw" path="res://rhythm_game/music_sync/event_layout.gd" id="1_jnfl3"] +[ext_resource type="Script" uid="uid://s16dt0bu0jrg" path="res://rhythm_game/music_sync/conductor.gd" id="2_62aw1"] +[ext_resource type="AudioStream" uid="uid://btmy8ffph5gn3" path="res://chart/test_nibelungen/audio.mp3" id="3_txi6k"] +[ext_resource type="Resource" uid="uid://b34uhnkvwfyc1" path="res://chart/test_nibelungen/tempo.tres" id="5_10cpq"] +[ext_resource type="Script" uid="uid://rg6orh6kutai" path="res://resource_type/music.gd" id="5_nsyv8"] +[ext_resource type="AudioStream" uid="uid://be8dyt7nfpffw" path="res://sfx/sfx_cowbell.ogg" id="6_ecbku"] +[ext_resource type="Script" uid="uid://bbpyym0kgujev" path="res://rhythm_game/metronome.gd" id="7_nsyv8"] + +[sub_resource type="Resource" id="Resource_10cpq"] +script = ExtResource("5_nsyv8") +stream = ExtResource("3_txi6k") +offset = -0.17 +tempo = ExtResource("5_10cpq") +metadata/_custom_type_script = "uid://rg6orh6kutai" + +[node name="RhythmGame" type="Node2D"] +script = ExtResource("1_jnfl3") + +[node name="Conductor" type="Node" parent="."] +script = ExtResource("2_62aw1") +autostart = true +music = SubResource("Resource_10cpq") +metadata/_custom_type_script = "uid://s16dt0bu0jrg" + +[node name="NoteLayout" type="Node" parent="."] +script = ExtResource("1_cr5rn") +metadata/_custom_type_script = "uid://dlnnbx2wvn66t" + +[node name="Metronome" type="AudioStreamPlayer" parent="."] +stream = ExtResource("6_ecbku") +script = ExtResource("7_nsyv8") +metadata/_custom_type_script = "uid://bbpyym0kgujev" + +[connection signal="ticked" from="Conductor" to="Metronome" method="on_conductor_ticked"] diff --git a/sfx/sfx_cowbell.ogg b/sfx/sfx_cowbell.ogg new file mode 100644 index 0000000..79c1561 Binary files /dev/null and b/sfx/sfx_cowbell.ogg differ diff --git a/sfx/sfx_cowbell.ogg.import b/sfx/sfx_cowbell.ogg.import new file mode 100644 index 0000000..7ec149f --- /dev/null +++ b/sfx/sfx_cowbell.ogg.import @@ -0,0 +1,19 @@ +[remap] + +importer="oggvorbisstr" +type="AudioStreamOggVorbis" +uid="uid://be8dyt7nfpffw" +path="res://.godot/imported/sfx_cowbell.ogg-db8c1909cb010205f13e34959d743eb1.oggvorbisstr" + +[deps] + +source_file="res://sfx/sfx_cowbell.ogg" +dest_files=["res://.godot/imported/sfx_cowbell.ogg-db8c1909cb010205f13e34959d743eb1.oggvorbisstr"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4