Godot で UI ボタンを量産していると、だいたいこういう悩みが出てきますよね。
- ボタンごとに
AudioStreamPlayerを置いて、pressed/mouse_enteredに毎回スクリプトをつなぐのが面倒 - 共通の SE(決定音・カーソル音)を変えたくなったとき、全部のボタンを修正するハメになる
Buttonを継承した「専用クラス」を増やすと、UI シーンが継承ツリー地獄になる
Godot 標準のやり方だと、
MySoundButton.gdみたいなスクリプトをButtonに継承して書く- もしくは親の「UI 管理ノード」が全部のボタンのシグナルを受け取って SE を鳴らす
…というパターンになりがちですが、どちらも「ボタンの数」が増えるほど管理がつらくなります。
そこで今回は、「継承じゃなくてコンポーネントを足すだけ」で SE を鳴らせるようにする SoundButton コンポーネント を作っていきましょう。
Button の子ノードとしてアタッチするだけで、pressed / mouse_entered に反応して自動で SE 再生してくれる、小さなヘルパーです。
【Godot 4】UI ボタンの SE をコンポーネント化!「SoundButton」コンポーネント
このコンポーネントの思想はシンプルです。
Buttonは「見た目と押されたこと」を担当SoundButtonは「押されたとき/ホバーしたときに音を鳴らす」だけを担当
つまり、ボタンの機能を「合成(Composition)」で拡張します。
UI ボタンが 10 個あっても、SoundButton をペタペタ貼っていくだけで SE 対応できますし、ボタンの種類(TextureButton など)を変えてもコンポーネント側のコードはほぼそのまま使い回せます。
フルコード:SoundButton.gd
extends Node
class_name SoundButton
## 親の Button / BaseButton の pressed / mouse_entered に反応して SE を鳴らすコンポーネント
##
## 使い方:
## - Button(または BaseButton 派生ノード)の子としてこのノードを置く
## - inspector から SE を設定する
## - 必要に応じてボリューム・ピッチ・ランダム化などを調整する
@export_category("Target")
## 対象となるボタン。未設定なら「親ノード」が BaseButton なら自動で拾います。
@export var target_button: BaseButton
@export_category("Pressed Sound")
## ボタンが pressed されたときに鳴らす SE。null の場合は鳴らさない。
@export var pressed_stream: AudioStream
## pressed SE の音量(dB)。0 が等倍、-6 で半分くらいの音量。
@export_range(-40.0, 6.0, 0.1)
@export var pressed_volume_db: float = 0.0
## pressed SE のピッチスケール。1.0 が等倍、0.9~1.1 くらいで少し変化をつけると気持ちいいです。
@export_range(0.5, 2.0, 0.01)
@export var pressed_pitch_scale: float = 1.0
## pressed SE のピッチをランダムで揺らす幅。0.0 ならランダムなし。
## 例: 0.05 なら 0.95~1.05 の範囲でランダム再生。
@export_range(0.0, 0.5, 0.01)
@export var pressed_pitch_random: float = 0.0
@export_category("Hover Sound")
## マウスがボタンに乗ったときに鳴らす SE。null の場合は鳴らさない。
@export var hover_stream: AudioStream
## hover SE の音量(dB)。
@export_range(-40.0, 6.0, 0.1)
@export var hover_volume_db: float = -4.0
## hover SE のピッチスケール。
@export_range(0.5, 2.0, 0.01)
@export var hover_pitch_scale: float = 1.0
## hover SE のピッチをランダムで揺らす幅。
@export_range(0.0, 0.5, 0.01)
@export var hover_pitch_random: float = 0.0
@export_category("Playback")
## 同じ SE を連打したときに、前の再生を止めてから再生し直すかどうか。
## UI の「カチカチ」音は上書きしてしまう方が聞きやすいので true 推奨。
@export var stop_previous_on_replay: bool = true
## ゲーム全体が一時停止中でも SE を鳴らすかどうか。
## UI メニューなどはポーズ中でも鳴らしたいので PROCESS_MODE_ALWAYS がデフォルトです。
@export var ignore_pause: bool = true
## 内部で使う AudioStreamPlayer。pressed 用と hover 用で 2 つ持っておきます。
var _pressed_player: AudioStreamPlayer
var _hover_player: AudioStreamPlayer
func _ready() -> void:
# 対象ボタンが未設定なら、親ノードから自動検出します。
if target_button == null:
if get_parent() is BaseButton:
target_button = get_parent() as BaseButton
else:
push_warning("SoundButton: parent is not a BaseButton and target_button is not set. Component will do nothing.")
# ボタンが見つからなかったら、ここで終了。
if target_button == null:
return
# 再生用 AudioStreamPlayer を生成して、このノードの子としてぶら下げます。
_pressed_player = AudioStreamPlayer.new()
_hover_player = AudioStreamPlayer.new()
# ポーズ中の挙動を設定。
if ignore_pause:
_pressed_player.process_mode = Node.PROCESS_MODE_ALWAYS
_hover_player.process_mode = Node.PROCESS_MODE_ALWAYS
add_child(_pressed_player)
add_child(_hover_player)
# ボタンのシグナルに接続。
# pressed: 左クリックや決定キーでボタンが「実行」されたとき
target_button.pressed.connect(_on_button_pressed)
# mouse_entered: マウスカーソルがボタンの上に乗ったとき
# キーボードフォーカスでの移動には反応しないことに注意。
target_button.mouse_entered.connect(_on_button_mouse_entered)
func _on_button_pressed() -> void:
if pressed_stream == null:
return
# ストリームと音量・ピッチを設定。
_pressed_player.stream = pressed_stream
_pressed_player.volume_db = pressed_volume_db
_pressed_player.pitch_scale = _get_randomized_pitch(pressed_pitch_scale, pressed_pitch_random)
if stop_previous_on_replay and _pressed_player.playing:
_pressed_player.stop()
_pressed_player.play()
func _on_button_mouse_entered() -> void:
if hover_stream == null:
return
_hover_player.stream = hover_stream
_hover_player.volume_db = hover_volume_db
_hover_player.pitch_scale = _get_randomized_pitch(hover_pitch_scale, hover_pitch_random)
if stop_previous_on_replay and _hover_player.playing:
_hover_player.stop()
_hover_player.play()
## ピッチにランダム幅を加えるヘルパー。
## base: 基本のピッチ
## random_width: 0.05 なら base ± 0.05 の範囲でランダム
func _get_randomized_pitch(base: float, random_width: float) -> float:
if random_width <= 0.0:
return base
var offset := randf_range(-random_width, random_width)
return base + offset
使い方の手順
ここでは、典型的な「タイトル画面のスタートボタン」に SE をつける例で説明します。
手順①:スクリプトをプロジェクトに追加
res://components/ui/SoundButton.gdなど、分かりやすい場所に上記コードを保存します。- Godot エディタを開くと、
SoundButtonがスクリプトクラスとして認識されます。
手順②:ボタンの子としてコンポーネントを追加
タイトル画面のシーン構成例はこんな感じです:
TitleScreen (Control)
├── StartButton (Button)
│ └── SoundButton (Node)
├── OptionsButton (Button)
│ └── SoundButton (Node)
└── QuitButton (Button)
└── SoundButton (Node)
StartButtonを選択して、右クリック → 「子ノードを追加」。Nodeを追加し、そのノードにSoundButton.gdをアタッチします。
(または、「ノードを追加」で直接SoundButtonクラスを選んでも OK)SoundButtonはButtonの子になっていれば、自動で親をターゲットにしてくれます。
同様に OptionsButton, QuitButton にも SoundButton をペタペタ貼っていくだけで、全ボタン SE 対応になります。
手順③:SE を Inspector から設定
SoundButton ノードを選択すると、Inspector に以下の項目が出てきます。
- Target
target_button: 通常は空のままで OK(親がButtonなら自動設定)。
- Pressed Sound
pressed_stream: 決定音のAudioStreamを指定(例:res://audio/ui_decide.ogg)。pressed_volume_db: ボリュームを調整。全体がうるさいなら -6dB くらいに。pressed_pitch_scale: 1.0 のままで OK。ちょっと軽くしたいなら 1.05 など。pressed_pitch_random: 0.02~0.05 くらい入れると、連打時の耳障り感が減ります。
- Hover Sound
hover_stream: カーソル移動音のAudioStreamを指定(例:res://audio/ui_move.ogg)。hover_volume_db: 決定音より少し小さめ(例: -4dB)にしておくとバランスが良いです。hover_pitch_scale,hover_pitch_random: pressed と同様に調整。
- Playback
stop_previous_on_replay: UI SE は true 推奨。ignore_pause: ポーズメニューのボタンにも使うなら true のままで OK。
手順④:他のシーン・他のボタンにも再利用
このコンポーネントは「ボタンの子ノードとして付けるだけ」なので、プレイヤーのインゲーム UI やポーズメニューでも同じように使えます。
例えば、ゲーム中のポーズメニュー:
PauseMenu (CanvasLayer)
└── Panel (Control)
├── ResumeButton (Button)
│ └── SoundButton (Node)
├── RetryButton (Button)
│ └── SoundButton (Node)
└── BackToTitleButton (Button)
└── SoundButton (Node)
共通の SE を使いたければ、SoundButton をプリセット化(シーン化)しておき、ドラッグ&ドロップで量産していくとさらに楽になります。
メリットと応用
SoundButton コンポーネントを導入すると、UI 設計がかなりスッキリします。
- 継承クラスが増えない
「決定音付きボタン」「ホバー音付きボタン」みたいな派生クラスを作らずに済みます。
どんな種類のボタンでも、「SE が欲しければ SoundButton を付ける」だけです。 - シーン構造が浅くて済む
SE 再生のためだけにAudioStreamPlayerをボタンごとに置く必要がなく、SoundButtonが内部で管理してくれます。
UI シーンを開いたときも、「Button + SoundButton」という分かりやすい構造になります。 - SE の差し替えが局所的
特定のボタンだけ別の SE にしたいときでも、そのボタンのSoundButtonだけ設定を変えれば完了。
共通 SE を一括管理したければ、「SE 設定用のリソース」を export するように拡張するのもアリです。 - ゲームロジックと UI SE を分離できる
「ボタンを押したときに何をするか」と「音を鳴らすかどうか」が完全に別ノードになるので、
ロジック側のスクリプトはクリーンなまま保てます。
コンポーネント指向で UI を組むと、「ボタンの見た目」「ボタンの効果」「ボタンの SE」「ボタンのアニメーション」などを、それぞれ別ノードとして合成できるようになります。
深い継承ツリーを作るよりも、こうした小さなコンポーネントを組み合わせていく方が、後からの変更に強いですね。
改造案:フォーカス移動(キーボード/ゲームパッド)でも Hover SE を鳴らす
標準の mouse_entered だけだと、キーボードやゲームパッドでフォーカスを移動したときに SE が鳴りません。
そこで、focus_entered シグナルにも対応させる改造例を載せておきます。
以下を _ready() に追記し、対応するコールバックを追加するだけです。
func _ready() -> void:
# ... 既存の処理 ...
# キーボード/ゲームパッドでフォーカスが移動したときにも Hover SE を鳴らす
target_button.focus_entered.connect(_on_button_focus_entered)
func _on_button_focus_entered() -> void:
# mouse_entered と同じ処理を流用しても良いし、
# 別の音を鳴らしたければここで分岐しても OK。
_on_button_mouse_entered()
こうして少しずつ「欲しい振る舞い」をコンポーネント側に足していくと、
UI ボタンはいつでもただの Button のまま、振る舞いだけを合成で積み増していけるようになります。
