Godotでオプション画面や一時停止メニューを作るとき、ついこういう構成になりがちですよね。

  • AudioSettings.gd という巨大スクリプトが UI 全体を管理している
  • HSlider に個別のコードを書いて、BGM / SE / マスタ音量を切り替え
  • シーンを分けるたびに、同じような「音量調整ロジック」をコピペ

さらに悪化すると、

  • 「このスライダーはマスタ音量」「こっちはBGM」みたいな情報がスクリプトの中にしかなくて、シーンを見ただけでは分からない
  • UI のノード階層が深くなり、どこにロジックを書くべきか迷子になる

こういう「UIロジック全部入りコントローラ」スタイルは、Godotに限らずすぐに肥大化してメンテ地獄になります。そこで、音量調整の責務だけを切り出したコンポーネントとして、VolumeControl を用意しておくとスッキリします。

今回作る VolumeControl は:

  • 親の SliderHSlider / 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 をアタッチしてみます。

手順①: スクリプトをプロジェクトに追加

  1. res://components/ui/volume_control.gd など、好きな場所に上記コードを保存します。
  2. 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 に対して:

  1. MasterSlider を選択し、インスペクタで Min0.0Max1.0 に設定します。
  2. MasterSlider の子に VolumeControl ノードを追加します(+ Node → 検索バーに VolumeControl)。
  3. 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(完全ミュート相当)

同様に、MusicSlider / SfxSlider の子にもそれぞれ VolumeControl を追加し、bus_name"Music" / "SFX" などに変えれば、まったく同じコンポーネントを3回使い回せます

手順④: 実行して確認

  1. プロジェクト設定 > Audio > Buses で、Master / Music / SFX などのバスを用意しておきます。
  2. BGM や SE の AudioStreamPlayer を、それぞれ対応するバスに割り当てておきます。
  3. ゲームを実行し、オプションメニューを開いてスライダーを動かしてみましょう。
  4. スライダーの値に応じて、対応バスの音量が変化していれば成功です。

このとき、オプションメニュー側には一行も音量調整コードを書いていないはずです。
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 に集約されているので、他のシーンでも同じパターンを簡単に再利用できますね。