Godotでオプション画面や一時停止メニューを作るとき、ついこういう構成になりがちですよね。
AudioSettings.gdという巨大スクリプトが UI 全体を管理している- 各
HSliderに個別のコードを書いて、BGM / SE / マスタ音量を切り替え - シーンを分けるたびに、同じような「音量調整ロジック」をコピペ
さらに悪化すると、
- 「このスライダーはマスタ音量」「こっちはBGM」みたいな情報がスクリプトの中にしかなくて、シーンを見ただけでは分からない
- UI のノード階層が深くなり、どこにロジックを書くべきか迷子になる
こういう「UIロジック全部入りコントローラ」スタイルは、Godotに限らずすぐに肥大化してメンテ地獄になります。そこで、音量調整の責務だけを切り出したコンポーネントとして、VolumeControl を用意しておくとスッキリします。
今回作る VolumeControl は:
- 親の
Slider(HSlider/VSlider)の値を監視 - 値が変わるたびに
AudioServerのバス音量(dB)を変更 - どのバスを操作するか、どの値レンジをdBに変換するかを
@exportで柔軟に設定
という「音量調整だけやる小さな部品」です。スライダーの子ノードとしてポンと置くだけで、どのシーンでも同じ挙動を再利用できます。
【Godot 4】UIに差し込むだけで音量調整!「VolumeControl」コンポーネント
フルコード(GDScript)
extends Node
class_name VolumeControl
## VolumeControl
## 親の Slider の値を AudioServer のバス音量(dB)に反映するコンポーネント。
##
## 想定ノード構成:
## - 親: HSlider / VSlider など Slider を継承したノード
## - 自分: VolumeControl (Node)
@export_category("基本設定")
## 操作対象のオーディオバス名。
## 例: "Master", "Music", "SFX" など。
@export var bus_name: StringName = &"Master"
## 親 Slider の値レンジをどのようなdBレンジにマッピングするか。
## 例: 0.0 ~ 1.0 を -40dB ~ 0dB にマッピングする、など。
@export var slider_min_value: float = 0.0
@export var slider_max_value: float = 1.0
## 対応するdBの最小値・最大値。
## 人間がほぼ聞こえないくらいの小ささを -40 ~ -60dB くらいにするのが一般的です。
@export var db_min_value: float = -40.0
@export var db_max_value: float = 0.0
## 親 Slider の初期値を、現在のバス音量から自動で設定するかどうか。
## 「オプション画面を開いたときに、今の音量を反映した値にしたい」場合は true に。
@export var sync_slider_from_bus_on_ready: bool = true
@export_category("動作オプション")
## 親が Slider 以外だった場合に警告を出すかどうか。
@export var warn_if_no_slider_parent: bool = true
## Slider の値をクランプ(範囲内に制限)するかどうか。
@export var clamp_slider_value: bool = true
## 0 に近い値をミュート扱いにするための閾値。
## 例: 0.01 以下なら -80dB にして完全ミュート、など。
@export var use_mute_threshold: bool = true
@export var mute_threshold: float = 0.001
@export var mute_db_value: float = -80.0
var _bus_index: int = -1
var _slider: Slider
func _ready() -> void:
## バスインデックスを取得
_bus_index = AudioServer.get_bus_index(bus_name)
if _bus_index == -1:
push_warning(
"VolumeControl: Bus '%s' が見つかりません。AudioServer でバスを作成してください。" % bus_name
)
## 親が Slider かチェック
_slider = _find_parent_slider()
if _slider == null:
if warn_if_no_slider_parent:
push_warning("VolumeControl: 親に Slider が見つかりません。VolumeControl は Slider の子に置いてください。")
return
## 値変更シグナルを接続
## Godot 4 では、Slider は 'value_changed' シグナルを持っています。
_slider.value_changed.connect(_on_slider_value_changed)
## 必要なら、現在のバス音量から Slider の値を初期化
if sync_slider_from_bus_on_ready and _bus_index != -1:
var current_db := AudioServer.get_bus_volume_db(_bus_index)
_slider.value = _db_to_slider_value(current_db)
func _find_parent_slider() -> Slider:
## 自分の親をたどって、最初に見つかった Slider を返す。
## 通常は直上の親が Slider ですが、柔軟性のために少し上まで見るようにしています。
var current := get_parent()
while current:
if current is Slider:
return current
current = current.get_parent()
return null
func _on_slider_value_changed(value: float) -> void:
## 親 Slider の値が変化したときに呼ばれます。
if _bus_index == -1:
return
var v := value
## 必要ならクランプ
if clamp_slider_value:
v = clampf(v, slider_min_value, slider_max_value)
## ミュート閾値の処理
var db_value: float
if use_mute_threshold and absf(v - slider_min_value) <= mute_threshold:
db_value = mute_db_value
else:
db_value = _slider_value_to_db(v)
AudioServer.set_bus_volume_db(_bus_index, db_value)
func _slider_value_to_db(slider_value: float) -> float:
## slider_min_value ~ slider_max_value を
## db_min_value ~ db_max_value に線形マッピングする。
if is_equal_approx(slider_max_value, slider_min_value):
return db_min_value
var t := (slider_value - slider_min_value) / (slider_max_value - slider_min_value)
return lerpf(db_min_value, db_max_value, t)
func _db_to_slider_value(db_value: float) -> float:
## db_min_value ~ db_max_value を
## slider_min_value ~ slider_max_value に逆マッピングする。
if is_equal_approx(db_max_value, db_min_value):
return slider_min_value
var t := (db_value - db_min_value) / (db_max_value - db_min_value)
return lerpf(slider_min_value, slider_max_value, t)
使い方の手順
ここでは、典型的な「オプションメニュー」の例として、マスタ音量スライダーに VolumeControl をアタッチしてみます。
手順①: スクリプトをプロジェクトに追加
res://components/ui/volume_control.gdなど、好きな場所に上記コードを保存します。- Godotエディタでスクリプトを開き、
class_name VolumeControlが認識されていることを確認します(右クリック → 「Create New Node」 で検索してもOK)。
手順②: スライダーシーンを用意
オプションメニューの一部として、こんなシーンを作るとします:
OptionsMenu (Control)
├── VBoxContainer
│ ├── Label ("Master Volume")
│ └── MasterSlider (HSlider)
│ └── VolumeControl (Node)
├── VBoxContainer
│ ├── Label ("BGM Volume")
│ └── MusicSlider (HSlider)
│ └── VolumeControl (Node)
└── VBoxContainer
├── Label ("SFX Volume")
└── SfxSlider (HSlider)
└── VolumeControl (Node)
ポイントは、各スライダーの子として VolumeControl ノードを 1個置くだけという構成にしていることです。
巨大な OptionsMenu.gd にロジックを全部書かなくて済みます。
手順③: VolumeControl のパラメータを設定
例えば MasterSlider に対して:
MasterSliderを選択し、インスペクタでMinを0.0、Maxを1.0に設定します。MasterSliderの子にVolumeControlノードを追加します(+ Node→ 検索バーにVolumeControl)。VolumeControlを選択し、インスペクタで以下のように設定します:- bus_name:
Master(Audio バスに合わせて変更) - slider_min_value:
0.0 - slider_max_value:
1.0 - db_min_value:
-40.0(ほぼ無音) - db_max_value:
0.0(フルボリューム) - sync_slider_from_bus_on_ready:
On(現在の音量をUIに反映したい場合) - use_mute_threshold:
On - mute_threshold:
0.001 - mute_db_value:
-80.0(完全ミュート相当)
- bus_name:
同様に、MusicSlider / SfxSlider の子にもそれぞれ VolumeControl を追加し、bus_name を "Music" / "SFX" などに変えれば、まったく同じコンポーネントを3回使い回せます。
手順④: 実行して確認
- プロジェクト設定 > Audio > Buses で、
Master/Music/SFXなどのバスを用意しておきます。 - BGM や SE の AudioStreamPlayer を、それぞれ対応するバスに割り当てておきます。
- ゲームを実行し、オプションメニューを開いてスライダーを動かしてみましょう。
- スライダーの値に応じて、対応バスの音量が変化していれば成功です。
このとき、オプションメニュー側には一行も音量調整コードを書いていないはずです。
UI の見た目と「どのバスを操作するか」という設定だけをシーン上で完結させているのがポイントですね。
メリットと応用
この VolumeControl コンポーネントを使うことで、次のようなメリットがあります。
- UIロジックの分離: 「音量調整」という責務を一つの小さなノードに閉じ込められるので、
OptionsMenu.gdなどのスクリプトが痩せていきます。 - シーン構造が読みやすい: シーンツリーを眺めるだけで、「このスライダーはどのバスを操作するのか」が一目瞭然です。
- 再利用性の高さ: タイトル画面の設定、ポーズメニュー、ゲーム内の簡易ミキサーなど、どこでも同じコンポーネントをポン付けできます。
- 継承地獄を避けられる:
MasterVolumeSlider.gd/MusicVolumeSlider.gd/SfxVolumeSlider.gdのようにスクリプトを量産せず、1つの VolumeControl を合成(Composition)して使い回せるのが嬉しいところです。
さらに、「コンポーネントを足すだけで機能が増える」という形にしておくと、デザイナーやレベル担当も自分で UI を組み替えやすくなります。
「音量スライダーを増やしたいなら、HSlider と VolumeControl をコピペしてバス名を変えればOK」というルールにしておけば、エンジニアが毎回コードを書く必要がありません。
改造案:ミュートトグルボタンと連携する
例えば「ミュートボタン」と連動させたい場合、VolumeControl に簡単な API を足しておくと便利です。
## VolumeControl に追加する例: ミュート切り替え用の関数
func set_muted(muted: bool) -> void:
if _bus_index == -1:
return
if muted:
AudioServer.set_bus_volume_db(_bus_index, mute_db_value)
else:
## 現在の Slider 値から dB を再計算して反映
if _slider:
var db_value := _slider_value_to_db(_slider.value)
AudioServer.set_bus_volume_db(_bus_index, db_value)
これで、同じシーン内の CheckBox などから:
func _on_mute_checkbox_toggled(pressed: bool) -> void:
$MasterSlider/VolumeControl.set_muted(pressed)
のように呼び出せば、スライダーとミュートボタンがきれいに連携します。
ロジックはあくまで VolumeControl に集約されているので、他のシーンでも同じパターンを簡単に再利用できますね。
