|
| 1 | +#################################################################################### |
| 2 | +## This file is part of Button Feedback. ## |
| 3 | +## https://github.com/ProgrammerOnCoffee/Button-Feedback ## |
| 4 | +#################################################################################### |
| 5 | +## Copyright (c) 2025 ProgrammerOnCoffee. ## |
| 6 | +## ## |
| 7 | +## Permission is hereby granted, free of charge, to any person obtaining a copy ## |
| 8 | +## of this software and associated documentation files (the "Software"), to deal ## |
| 9 | +## in the Software without restriction, including without limitation the rights ## |
| 10 | +## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell ## |
| 11 | +## copies of the Software, and to permit persons to whom the Software is ## |
| 12 | +## furnished to do so, subject to the following conditions: ## |
| 13 | +## ## |
| 14 | +## The above copyright notice and this permission notice shall be included in all ## |
| 15 | +## copies or substantial portions of the Software. ## |
| 16 | +## ## |
| 17 | +## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ## |
| 18 | +## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ## |
| 19 | +## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ## |
| 20 | +## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ## |
| 21 | +## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ## |
| 22 | +## OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE ## |
| 23 | +## SOFTWARE. ## |
| 24 | +#################################################################################### |
| 25 | + |
| 26 | +@icon("res://addons/button_feedback/icon.svg") |
| 27 | +extends Node |
| 28 | +## Provides audiovisual feedback when players interact with buttons. |
| 29 | +## |
| 30 | +## Provides audiovisual feedback when players interact with buttons. |
| 31 | + |
| 32 | +## The volume of button sounds in decibels. |
| 33 | +## Note that the hover sound will have [code]6.0[/code] dB subtracted from this number. |
| 34 | +const VOLUME_DB: float = -6.0 |
| 35 | +## The target audio bus of button sounds. |
| 36 | +const AUDIO_BUS: StringName = &"" |
| 37 | +## The pitch scale of button sounds. |
| 38 | +const PITCH_SCALE: float = 2.0 |
| 39 | +## The amount that buttons will be scaled by when hovered. |
| 40 | +const HOVER_SCALE: float = 1.1 |
| 41 | +## The duration of the tween that scales buttons when they are hovered. |
| 42 | +const HOVER_SCALE_DURATION: float = 0.05 |
| 43 | + |
| 44 | +# [AudioStreamPlayer2D]s are used because of a bug where pitch_scale |
| 45 | +# doesn't work on web exports for regular [AudioStreamPlayer]s. |
| 46 | +## The [AudioStreamPlayer2D] that plays the button hover sound. |
| 47 | +var button_hover_player := AudioStreamPlayer2D.new() |
| 48 | +## The [AudioStreamPlayer2D] that plays the button down sound. |
| 49 | +var button_down_player := AudioStreamPlayer2D.new() |
| 50 | +## The [AudioStreamPlayer2D] that plays the button pressed sound. |
| 51 | +var button_pressed_player := AudioStreamPlayer2D.new() |
| 52 | + |
| 53 | +## The [StyleBoxEmpty] that will be set as the focus theme override for clicked |
| 54 | +## buttons.[br] |
| 55 | +## The default stylebox will be shown if the focus is not from a click |
| 56 | +## (e.g. when navigating with a keyboard.) |
| 57 | +var _stylebox_empty := StyleBoxEmpty.new() |
| 58 | + |
| 59 | + |
| 60 | +func _init() -> void: |
| 61 | + button_hover_player.stream = load("res://addons/button_feedback/button_hover.wav") as AudioStreamWAV |
| 62 | + button_hover_player.volume_db = VOLUME_DB - 6.0 |
| 63 | + button_down_player.stream = load("res://addons/button_feedback/button_down.wav") as AudioStreamWAV |
| 64 | + button_down_player.volume_db = VOLUME_DB |
| 65 | + button_pressed_player.stream = load("res://addons/button_feedback/button_pressed.wav") as AudioStreamWAV |
| 66 | + button_pressed_player.volume_db = VOLUME_DB |
| 67 | + |
| 68 | + |
| 69 | +func _ready() -> void: |
| 70 | + for player in [button_hover_player, button_down_player, button_pressed_player] as Array[AudioStreamPlayer2D]: |
| 71 | + player.bus = AUDIO_BUS |
| 72 | + player.pitch_scale = PITCH_SCALE |
| 73 | + player.attenuation = 0.0 |
| 74 | + player.max_distance = INF |
| 75 | + player.panning_strength = 0.0 |
| 76 | + |
| 77 | + setup_recursive(get_parent()) |
| 78 | + |
| 79 | + add_child(button_hover_player) |
| 80 | + add_child(button_down_player) |
| 81 | + add_child(button_pressed_player) |
| 82 | + |
| 83 | + |
| 84 | +## Calls [member setup_button] for all [BaseButton]s |
| 85 | +## that [param node] is an ancestor of. |
| 86 | +func setup_recursive(ancestor: Node) -> void: |
| 87 | + var queue := ancestor.get_children() |
| 88 | + while queue: |
| 89 | + var new_queue: Array[Node] = [] |
| 90 | + for node in queue: |
| 91 | + var button := node as BaseButton |
| 92 | + if button: |
| 93 | + setup_button(button) |
| 94 | + new_queue.append_array(node.get_children()) |
| 95 | + queue = new_queue |
| 96 | + |
| 97 | + |
| 98 | +## Sets up audiovisual feedback for [param button]. |
| 99 | +func setup_button(button: BaseButton) -> void: |
| 100 | + # Return if feedback for button has already been set up |
| 101 | + if button.button_down.is_connected(button_down_player.play): |
| 102 | + return |
| 103 | + |
| 104 | + button.focus_entered.connect(_on_button_focus_entered.bind(button), CONNECT_DEFERRED) |
| 105 | + button.focus_exited.connect(button.remove_theme_stylebox_override.bind(&"focus")) |
| 106 | + button.mouse_entered.connect(_on_button_mouse_entered.bind(button)) |
| 107 | + button.mouse_exited.connect(_on_button_mouse_exited.bind(button)) |
| 108 | + button.mouse_entered.connect(button_hover_player.play) |
| 109 | + button.button_down.connect(button_down_player.play) |
| 110 | + # is_class() won't error if the advanced gui module is disabled |
| 111 | + if not button.is_class("OptionButton"): |
| 112 | + button.pressed.connect(button_pressed_player.play) |
| 113 | + |
| 114 | + |
| 115 | +func _on_button_focus_entered(button: BaseButton) -> void: |
| 116 | + # Remove focus stylebox if focus is from a click |
| 117 | + if button.button_pressed: |
| 118 | + button.add_theme_stylebox_override(&"focus", _stylebox_empty) |
| 119 | + |
| 120 | + |
| 121 | +func _on_button_mouse_entered(button: BaseButton) -> void: |
| 122 | + # Set pivot offset so that button scales from center |
| 123 | + button.pivot_offset = button.size / 2 |
| 124 | + # Kill tween if one is already running |
| 125 | + if button.has_meta(&"_button_feedback_scale_tween"): |
| 126 | + button.get_meta(&"_button_feedback_scale_tween").kill() |
| 127 | + var tween := button.create_tween().set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC) |
| 128 | + tween.tween_property(button, ^":scale", Vector2.ONE * HOVER_SCALE, |
| 129 | + # Tween for less time if button is already partially scaled |
| 130 | + remap(button.scale.x, 1.0, HOVER_SCALE, HOVER_SCALE_DURATION, 0.0)) |
| 131 | + button.set_meta(&"_button_feedback_scale_tween", tween) |
| 132 | + |
| 133 | + |
| 134 | +func _on_button_mouse_exited(button: BaseButton) -> void: |
| 135 | + # Ensure mouse is outside of button, not blocked by another Control |
| 136 | + if not Rect2i(Vector2.ZERO, button.size).has_point(button.get_local_mouse_position()): |
| 137 | + # Kill tween if one is already running |
| 138 | + if button.has_meta(&"_button_feedback_scale_tween"): |
| 139 | + button.get_meta(&"_button_feedback_scale_tween").kill() |
| 140 | + var tween := button.create_tween().set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC) |
| 141 | + tween.tween_property(button, ^":scale", Vector2.ONE, |
| 142 | + # Tween for less time if button isn't fully scaled |
| 143 | + remap(button.scale.x, HOVER_SCALE, 1.0, HOVER_SCALE_DURATION, 0.0)) |
| 144 | + button.set_meta(&"_button_feedback_scale_tween", tween) |
0 commit comments