From 36ec30dd0ab80de0fe1e2f3fa121e47dd338b36a Mon Sep 17 00:00:00 2001 From: Ben McVety Date: Wed, 22 Jan 2025 14:43:11 -0500 Subject: [PATCH 1/2] Add Settings page with volume controls Add two audio buses, Music and Sfx, and assign all audio nodes in the project to the appropriate bus. Add a Settings page with one slider per bus. Persist the values to a config file, and restore them when the Settings scene is instantiated (which happens when the game is launched). Co-authored-by: Will Thompson --- CandyWrapper/World.tscn | 3 + Scene/Explosion.tscn | 1 + Scene/Player.tscn | 1 + Scene/WorldSelector.tscn | 9 ++- Scene/WorldSelector/MainWorldSelector.tscn | 5 ++ Scene/WorldSelector/SettingsPage.tscn | 76 +++++++++++++++++++ Script/WorldSelector.gd | 12 ++- Script/WorldSelector/MainWorldSelector.gd | 5 ++ Script/WorldSelector/SettingsPage.gd | 56 ++++++++++++++ .../Scene/Explosion.tscn | 1 + .../Cassidy's Purple World/Scene/Player.tscn | 1 + Worlds/Cassidy's Purple World/World.tscn | 3 + Worlds/Sample/World.tscn | 3 + default_bus_layout.tres | 15 ++++ 14 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 Scene/WorldSelector/SettingsPage.tscn create mode 100644 Script/WorldSelector/SettingsPage.gd create mode 100644 default_bus_layout.tres diff --git a/CandyWrapper/World.tscn b/CandyWrapper/World.tscn index 235c9d8..1569fe1 100644 --- a/CandyWrapper/World.tscn +++ b/CandyWrapper/World.tscn @@ -28,11 +28,14 @@ frame = 1 [node name="Music" type="AudioStreamPlayer" parent="."] stream = ExtResource("2_swba7") autoplay = true +bus = &"Music" [node name="Audio" type="Node" parent="."] [node name="Win" type="AudioStreamPlayer" parent="Audio"] stream = ExtResource("5_qdrvr") +bus = &"Sfx" [node name="Lose" type="AudioStreamPlayer" parent="Audio"] stream = ExtResource("6_rs0e0") +bus = &"Sfx" diff --git a/Scene/Explosion.tscn b/Scene/Explosion.tscn index 2efc54e..062db13 100644 --- a/Scene/Explosion.tscn +++ b/Scene/Explosion.tscn @@ -48,6 +48,7 @@ frame = 1 [node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="."] stream = ExtResource("3") autoplay = true +bus = &"Sfx" [node name="AnimationPlayer" type="AnimationPlayer" parent="."] libraries = { diff --git a/Scene/Player.tscn b/Scene/Player.tscn index 826e45e..75e106a 100644 --- a/Scene/Player.tscn +++ b/Scene/Player.tscn @@ -91,6 +91,7 @@ shape = SubResource("2") [node name="Audio" type="AudioStreamPlayer" parent="."] stream = ExtResource("3") +bus = &"Sfx" [node name="AnimationPlayer" type="AnimationPlayer" parent="."] libraries = { diff --git a/Scene/WorldSelector.tscn b/Scene/WorldSelector.tscn index a214df3..a20e958 100644 --- a/Scene/WorldSelector.tscn +++ b/Scene/WorldSelector.tscn @@ -1,9 +1,10 @@ -[gd_scene load_steps=5 format=3 uid="uid://h6eyk0hg1y1f"] +[gd_scene load_steps=6 format=3 uid="uid://h6eyk0hg1y1f"] [ext_resource type="Theme" uid="uid://dqt6eyc0l8kms" path="res://Theme/WorldSelector.tres" id="1_h1u7b"] [ext_resource type="Script" path="res://Script/WorldSelector.gd" id="2_c1jjg"] [ext_resource type="PackedScene" uid="uid://b1qffdabwqnxf" path="res://Scene/WorldSelector/ExtraWorldSelector.tscn" id="3_d8a1w"] [ext_resource type="PackedScene" uid="uid://pnnwgl5j385l" path="res://Scene/WorldSelector/MainWorldSelector.tscn" id="3_dc6jw"] +[ext_resource type="PackedScene" uid="uid://d2ay7olsehadk" path="res://Scene/WorldSelector/SettingsPage.tscn" id="5_opwul"] [node name="TitleScreen" type="Control"] layout_mode = 3 @@ -22,7 +23,13 @@ layout_mode = 1 visible = false layout_mode = 1 +[node name="SettingsPage" parent="." instance=ExtResource("5_opwul")] +visible = false +layout_mode = 1 + [connection signal="main_world_selected" from="MainWorldSelector" to="." method="_on_main_world_selected"] +[connection signal="settings_selected" from="MainWorldSelector" to="." method="_on_main_world_selector_settings_selected"] [connection signal="show_extra_worlds" from="MainWorldSelector" to="." method="_on_extra_worlds_button_pressed"] [connection signal="back" from="ExtraWorldSelector" to="." method="_on_back_button_pressed"] [connection signal="world_selected" from="ExtraWorldSelector" to="." method="_enter_world"] +[connection signal="back" from="SettingsPage" to="." method="_on_settings_page_back"] diff --git a/Scene/WorldSelector/MainWorldSelector.tscn b/Scene/WorldSelector/MainWorldSelector.tscn index bb78f60..d2a8143 100644 --- a/Scene/WorldSelector/MainWorldSelector.tscn +++ b/Scene/WorldSelector/MainWorldSelector.tscn @@ -46,6 +46,11 @@ text = "Play Candy Wrapper" layout_mode = 2 text = "Extra Worlds" +[node name="Settings" type="Button" parent="VBox/MarginContainer2/MainWorldBox"] +layout_mode = 2 +text = "Settings" + [connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"] [connection signal="pressed" from="VBox/MarginContainer2/MainWorldBox/CandyWrapperButton" to="." method="_on_candy_wrapper_button_pressed"] [connection signal="pressed" from="VBox/MarginContainer2/MainWorldBox/ExtraWorldsButton" to="." method="_on_extra_worlds_button_pressed"] +[connection signal="pressed" from="VBox/MarginContainer2/MainWorldBox/Settings" to="." method="_on_settings_button_pressed"] diff --git a/Scene/WorldSelector/SettingsPage.tscn b/Scene/WorldSelector/SettingsPage.tscn new file mode 100644 index 0000000..0dc8c5e --- /dev/null +++ b/Scene/WorldSelector/SettingsPage.tscn @@ -0,0 +1,76 @@ +[gd_scene load_steps=3 format=3 uid="uid://d2ay7olsehadk"] + +[ext_resource type="Theme" uid="uid://dqt6eyc0l8kms" path="res://Theme/WorldSelector.tres" id="1_gll88"] +[ext_resource type="Script" path="res://Script/WorldSelector/SettingsPage.gd" id="2_6mi7e"] + +[node name="SettingsPage" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme = ExtResource("1_gll88") +script = ExtResource("2_6mi7e") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 2 +offset_right = 140.0 +offset_bottom = 144.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Title" type="Label" parent="VBoxContainer"] +layout_mode = 2 +theme_type_variation = &"HeaderLarge" +text = "Settings" +horizontal_alignment = 1 + +[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_right = 1 +theme_override_constants/margin_bottom = 5 + +[node name="ExtraWorldsBox" type="VBoxContainer" parent="VBoxContainer/MarginContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 2 + +[node name="MusicLabel" type="Label" parent="VBoxContainer/MarginContainer/ExtraWorldsBox"] +layout_mode = 2 +size_flags_horizontal = 0 +theme_type_variation = &"HeaderLarge" +theme_override_font_sizes/font_size = 9 +text = "music" +horizontal_alignment = 1 + +[node name="MusicSlider" type="HSlider" parent="VBoxContainer/MarginContainer/ExtraWorldsBox"] +unique_name_in_owner = true +layout_mode = 2 +min_value = -30.0 +max_value = 0.0 + +[node name="SfxLabel" type="Label" parent="VBoxContainer/MarginContainer/ExtraWorldsBox"] +layout_mode = 2 +size_flags_horizontal = 0 +theme_type_variation = &"HeaderLarge" +theme_override_font_sizes/font_size = 9 +text = "sfx" +horizontal_alignment = 1 + +[node name="SfxSlider" type="HSlider" parent="VBoxContainer/MarginContainer/ExtraWorldsBox"] +unique_name_in_owner = true +layout_mode = 2 +min_value = -30.0 +max_value = 0.0 + +[node name="BackButton" type="Button" parent="VBoxContainer/MarginContainer/ExtraWorldsBox"] +unique_name_in_owner = true +layout_mode = 2 +text = "Go Back" + +[connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"] +[connection signal="pressed" from="VBoxContainer/MarginContainer/ExtraWorldsBox/BackButton" to="." method="_on_back_button_pressed"] diff --git a/Script/WorldSelector.gd b/Script/WorldSelector.gd index ed24577..37d4b10 100644 --- a/Script/WorldSelector.gd +++ b/Script/WorldSelector.gd @@ -2,7 +2,7 @@ extends Control @onready var MainWorldSelector = $MainWorldSelector @onready var ExtraWorldSelector = $ExtraWorldSelector - +@onready var SettingsPage = $SettingsPage func _enter_world(world: String) -> void: get_tree().change_scene_to_file(world) @@ -20,3 +20,13 @@ func _on_extra_worlds_button_pressed() -> void: func _on_back_button_pressed() -> void: ExtraWorldSelector.visible = false MainWorldSelector.visible = true + + +func _on_main_world_selector_settings_selected() -> void: + MainWorldSelector.visible = false + SettingsPage.visible = true + + +func _on_settings_page_back() -> void: + SettingsPage.visible = false + MainWorldSelector.visible = true diff --git a/Script/WorldSelector/MainWorldSelector.gd b/Script/WorldSelector/MainWorldSelector.gd index af6df38..b52302b 100644 --- a/Script/WorldSelector/MainWorldSelector.gd +++ b/Script/WorldSelector/MainWorldSelector.gd @@ -2,6 +2,7 @@ extends Control signal main_world_selected signal show_extra_worlds +signal settings_selected @onready var CandyWrapperButton = %CandyWrapperButton @@ -20,3 +21,7 @@ func _on_extra_worlds_button_pressed() -> void: func _on_visibility_changed() -> void: if self.visible and CandyWrapperButton: CandyWrapperButton.grab_focus() + + +func _on_settings_button_pressed() -> void: + settings_selected.emit() diff --git a/Script/WorldSelector/SettingsPage.gd b/Script/WorldSelector/SettingsPage.gd new file mode 100644 index 0000000..f5447cc --- /dev/null +++ b/Script/WorldSelector/SettingsPage.gd @@ -0,0 +1,56 @@ +extends Control + +signal back + +const SETTINGS_PATH := "user://settings.cfg" +const VOLUME_SECTION := "Volume" +var _settings := ConfigFile.new() +var _loading = true + +@onready var music_slider : HSlider = %MusicSlider +@onready var sfx_slider : HSlider = %SfxSlider + +func _ready() -> void: + var err := _settings.load(SETTINGS_PATH) + if err != OK and err != ERR_FILE_NOT_FOUND: + print("Failed to load %s: %s" % [SETTINGS_PATH, err]) + + _initialise_slider(music_slider, "Music") + _initialise_slider(sfx_slider, "Sfx") + + _loading = false + + +func _initialise_slider(slider: Slider, bus: String): + var level = _settings.get_value(VOLUME_SECTION, bus, 0) + + slider.value_changed.connect(_on_slider_value_changed.bind(slider, bus)) + slider.value = level + + +func _on_slider_value_changed(value: float, slider: Slider, bus: String) -> void: + var bus_idx = AudioServer.get_bus_index(bus) + + AudioServer.set_bus_volume_db(bus_idx, value) + var mute := value == slider.min_value + AudioServer.set_bus_mute(bus_idx, mute) + + if not _loading: + _settings.set_value(VOLUME_SECTION, bus, value) + var err := _settings.save(SETTINGS_PATH) + if err != OK: + print("Failed to save settings to %s: %s" % [SETTINGS_PATH, err]) + + +func _on_visibility_changed() -> void: + if self.visible and music_slider: + music_slider.grab_focus() + + +func _input(event: InputEvent) -> void: + if self.visible and event.is_action_pressed("ui_cancel"): + back.emit() + + +func _on_back_button_pressed() -> void: + back.emit() diff --git a/Worlds/Cassidy's Purple World/Scene/Explosion.tscn b/Worlds/Cassidy's Purple World/Scene/Explosion.tscn index 5f68543..4fee9c6 100644 --- a/Worlds/Cassidy's Purple World/Scene/Explosion.tscn +++ b/Worlds/Cassidy's Purple World/Scene/Explosion.tscn @@ -48,6 +48,7 @@ frame = 1 [node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="."] stream = ExtResource("2_u3wti") autoplay = true +bus = &"Sfx" [node name="AnimationPlayer" type="AnimationPlayer" parent="."] libraries = { diff --git a/Worlds/Cassidy's Purple World/Scene/Player.tscn b/Worlds/Cassidy's Purple World/Scene/Player.tscn index e5aa376..3e6c8b3 100644 --- a/Worlds/Cassidy's Purple World/Scene/Player.tscn +++ b/Worlds/Cassidy's Purple World/Scene/Player.tscn @@ -91,6 +91,7 @@ shape = SubResource("2") [node name="Audio" type="AudioStreamPlayer" parent="."] stream = ExtResource("3_ea3of") +bus = &"Sfx" [node name="AnimationPlayer" type="AnimationPlayer" parent="."] libraries = { diff --git a/Worlds/Cassidy's Purple World/World.tscn b/Worlds/Cassidy's Purple World/World.tscn index 31cfbaa..1bdc20b 100644 --- a/Worlds/Cassidy's Purple World/World.tscn +++ b/Worlds/Cassidy's Purple World/World.tscn @@ -30,11 +30,14 @@ frame = 1 [node name="Music" type="AudioStreamPlayer" parent="."] stream = ExtResource("4_21637") autoplay = true +bus = &"Music" [node name="Audio" type="Node" parent="."] [node name="Win" type="AudioStreamPlayer" parent="Audio"] stream = ExtResource("5_q65sc") +bus = &"Sfx" [node name="Lose" type="AudioStreamPlayer" parent="Audio"] stream = ExtResource("6_upqr8") +bus = &"Sfx" diff --git a/Worlds/Sample/World.tscn b/Worlds/Sample/World.tscn index f5d456b..165983c 100644 --- a/Worlds/Sample/World.tscn +++ b/Worlds/Sample/World.tscn @@ -28,11 +28,14 @@ frame = 1 [node name="Music" type="AudioStreamPlayer" parent="."] stream = ExtResource("2_pgqpj") autoplay = true +bus = &"Music" [node name="Audio" type="Node" parent="."] [node name="Win" type="AudioStreamPlayer" parent="Audio"] stream = ExtResource("5_aa7p7") +bus = &"Sfx" [node name="Lose" type="AudioStreamPlayer" parent="Audio"] stream = ExtResource("6_0fq84") +bus = &"Sfx" diff --git a/default_bus_layout.tres b/default_bus_layout.tres new file mode 100644 index 0000000..d0b7951 --- /dev/null +++ b/default_bus_layout.tres @@ -0,0 +1,15 @@ +[gd_resource type="AudioBusLayout" format=3 uid="uid://dedcyh6mnrxw5"] + +[resource] +bus/1/name = &"Sfx" +bus/1/solo = false +bus/1/mute = false +bus/1/bypass_fx = false +bus/1/volume_db = 0.0 +bus/1/send = &"Master" +bus/2/name = &"Music" +bus/2/solo = false +bus/2/mute = false +bus/2/bypass_fx = false +bus/2/volume_db = 0.0 +bus/2/send = &"Master" From 0dd8dbd1fa09f1a96d2b2697304a7162203f2c5b Mon Sep 17 00:00:00 2001 From: Will Thompson Date: Mon, 10 Feb 2025 16:02:30 +0000 Subject: [PATCH 2/2] Add Settings global to handle volumes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the volume settings were restored whenever SettingsPage.tscn was instantiated. When running the whole game, this is on startup, and then redundantly whenever the player returns to the title. However, when running an individual level or world from the editor, the volume settings were not restored, making the developer sad if they are tired of the background music. Add a new global script, Settings, which restores the channel volumes on _ready(), and exposes API to fetch & adjust the volume for a channel. Adapt SettingsPage.gd accordingly. As a result, the volumes are restored when the application starts – regardless of whether that's the whole game or any specific scene – and only then. --- Script/Settings.gd | 50 ++++++++++++++++++++++++++++ Script/WorldSelector/SettingsPage.gd | 31 +++-------------- project.godot | 1 + 3 files changed, 55 insertions(+), 27 deletions(-) create mode 100644 Script/Settings.gd diff --git a/Script/Settings.gd b/Script/Settings.gd new file mode 100644 index 0000000..fcc04d3 --- /dev/null +++ b/Script/Settings.gd @@ -0,0 +1,50 @@ +extends Node + +const SETTINGS_PATH := "user://settings.cfg" + +const VOLUME_SECTION := "Volume" +const MIN_VOLUME := -30.0 + +var _settings := ConfigFile.new() + + +func _ready() -> void: + var err := _settings.load(SETTINGS_PATH) + if err != OK and err != ERR_FILE_NOT_FOUND: + print("Failed to load %s: %s" % [SETTINGS_PATH, err]) + + _restore_volumes() + + +func _restore_volumes() -> void: + for bus_idx in AudioServer.bus_count: + var bus := AudioServer.get_bus_name(bus_idx) + var volume_db : float = _settings.get_value(VOLUME_SECTION, bus, 0.0) + print("Restored", [bus_idx, bus, volume_db]) + _set_volume(bus_idx, volume_db) + + +func get_volume(bus: String) -> float: + var bus_idx = AudioServer.get_bus_index(bus) + + return AudioServer.get_bus_volume_db(bus_idx) + + +func set_volume(bus: String, volume_db: float) -> void: + var bus_idx = AudioServer.get_bus_index(bus) + _set_volume(bus_idx, volume_db) + + _settings.set_value(VOLUME_SECTION, bus, volume_db) + _save() + + +func _set_volume(bus_idx: int, volume_db: float) -> void: + AudioServer.set_bus_volume_db(bus_idx, volume_db) + var mute := volume_db <= MIN_VOLUME + AudioServer.set_bus_mute(bus_idx, mute) + + +func _save(): + var err := _settings.save(SETTINGS_PATH) + if err != OK: + print("Failed to save settings to %s: %s" % [SETTINGS_PATH, err]) diff --git a/Script/WorldSelector/SettingsPage.gd b/Script/WorldSelector/SettingsPage.gd index f5447cc..28dd3b4 100644 --- a/Script/WorldSelector/SettingsPage.gd +++ b/Script/WorldSelector/SettingsPage.gd @@ -2,44 +2,21 @@ extends Control signal back -const SETTINGS_PATH := "user://settings.cfg" -const VOLUME_SECTION := "Volume" -var _settings := ConfigFile.new() -var _loading = true - @onready var music_slider : HSlider = %MusicSlider @onready var sfx_slider : HSlider = %SfxSlider func _ready() -> void: - var err := _settings.load(SETTINGS_PATH) - if err != OK and err != ERR_FILE_NOT_FOUND: - print("Failed to load %s: %s" % [SETTINGS_PATH, err]) - _initialise_slider(music_slider, "Music") _initialise_slider(sfx_slider, "Sfx") - _loading = false - func _initialise_slider(slider: Slider, bus: String): - var level = _settings.get_value(VOLUME_SECTION, bus, 0) - - slider.value_changed.connect(_on_slider_value_changed.bind(slider, bus)) - slider.value = level - - -func _on_slider_value_changed(value: float, slider: Slider, bus: String) -> void: - var bus_idx = AudioServer.get_bus_index(bus) + slider.value = Settings.get_volume(bus) + slider.value_changed.connect(_on_slider_value_changed.bind(bus)) - AudioServer.set_bus_volume_db(bus_idx, value) - var mute := value == slider.min_value - AudioServer.set_bus_mute(bus_idx, mute) - if not _loading: - _settings.set_value(VOLUME_SECTION, bus, value) - var err := _settings.save(SETTINGS_PATH) - if err != OK: - print("Failed to save settings to %s: %s" % [SETTINGS_PATH, err]) +func _on_slider_value_changed(value: float, bus: String) -> void: + Settings.set_volume(bus, value) func _on_visibility_changed() -> void: diff --git a/project.godot b/project.godot index 56c0c50..59db72f 100644 --- a/project.godot +++ b/project.godot @@ -20,6 +20,7 @@ config/icon="res://Image/picon.svg" [autoload] global="*res://Script/Global.gd" +Settings="*res://Script/Settings.gd" [display]