Godotで「設定画面」を作るとき、ついこんな構成になりがちですよね。
- 各画面ごとに
OptionsMenu.gdを作って、音量スライダーや明るさスライダーを全部そこで管理 - スライダーごとに
value_changedシグナルをメニュー側で拾って、ConfigFileを開いて、値を書き込んで、保存して… - 別シーンに同じような設定UIを作ると、同じようなコードをコピペ or 継承地獄
結果として、「設定UIの見た目」と「保存ロジック」がベッタリ結合してしまいがちです。
しかも Godot では HSlider や VSlider に直接ロジックを書き込むことも多く、「このスライダーだけ別のConfigキーにしたい」みたいなときに継承や条件分岐で泥沼になりがちですね。
そこで今回は、「どのスライダーにもポン付けできて、値変更と同時にConfigFileへ保存してくれる」コンポーネントを作ってみましょう。
スライダー本体は純粋に「見た目と値」だけを持ち、保存ロジックは独立したコンポーネントとして合成するスタイルです。
【Godot 4】差し替え自由な設定UIに!「SettingSlider」コンポーネント
SettingSlider は、任意のスライダー(Control系ノード) にアタッチして使うコンポーネントです。
- 音量(BGM / SE / マスターボリューム)
- 画面の明るさ(ガンマ / ポストプロセスの強度)
- マウス感度
など、「0〜100」「0〜1」系の設定値を扱うのに向いています。
特徴:
- @export で設定項目名・Configファイルパス・セクション名などを指定
- 起動時にConfigFileから値を読み込み → スライダーに反映
- スライダーの値が変わるたびに即座にConfigFileへ保存
- オプションで AudioServer や任意のコールバック にも即反映できるフック付き
フルコード:SettingSlider.gd
extends Node
class_name SettingSlider
## 任意のスライダー(Control)にアタッチして使う「設定スライダー」コンポーネント。
## - ConfigFile との読み書き
## - スライダーと内部値の同期
## をまとめて担当します。
## 設定ファイルのパス(プロジェクト内相対パス)
## 例: "user://settings.cfg"(ユーザーごとの保存領域)
@export_file("*.cfg") var config_path: String = "user://settings.cfg"
## ConfigFile 内のセクション名
## 例: "audio", "video", "gameplay" など
@export var config_section: String = "audio"
## ConfigFile 内のキー名
## 例: "master_volume", "bgm_volume", "brightness"
@export var config_key: String = "master_volume"
## デフォルト値(Configにまだ値がない場合に使う)
## スライダーの min/max に合わせて設定しておくと良いです。
@export var default_value: float = 1.0
## スライダーの値を 0.0〜1.0 に正規化して保存するかどうか
## true の場合:
## - Config には 0.0〜1.0 の値を保存
## - スライダーの min/max に応じて変換
## false の場合:
## - スライダーの値をそのまま保存
@export var normalize_to_01: bool = true
## スライダーの親ノードを自動で探すかどうか
## true の場合:
## - 自分の親ノードが HSlider / VSlider / Range を継承しているかチェックして自動で紐づけ
## false の場合:
## - inspector から手動で slider_node を指定
@export var auto_find_slider: bool = true
## 手動でスライダーを指定したい場合はこちらにアサイン
## Range を継承していれば OK(HSlider, VSlider, SpinBox など)
@export var slider_node: Range
## 値が変わったときに AudioServer にも反映するオプション
## 例: master バスの音量を連動させる
@export var apply_to_audio_bus: bool = false
## AudioServer に適用するバス名(例: "Master", "BGM")
@export var audio_bus_name: String = "Master"
## AudioServer に適用する際の dB 範囲
## normalize_to_01 = true のとき:
## 0.0 -> min_db, 1.0 -> max_db に線形マッピング
@export_range(-80.0, 24.0, 0.1) var min_db: float = -40.0
@export_range(-80.0, 24.0, 0.1) var max_db: float = 0.0
## 設定値が変わったときに通知するシグナル
## 外部でこれを受けて、明るさのシェーダーパラメータを変えるなどの用途に。
signal setting_changed(value: float)
var _config := ConfigFile.new()
var _is_ready: bool = false
var _slider: Range
func _ready() -> void:
_is_ready = false
# スライダーを取得
_init_slider_reference()
if not _slider:
push_warning("[SettingSlider] スライダーが見つかりません。auto_find_slider=false の場合は slider_node を設定してください。")
return
# ConfigFile から値を読み込む
var loaded_value := _load_value_from_config()
# スライダーに反映
_apply_value_to_slider(loaded_value)
# 必要なら AudioServer にも反映
_apply_value_to_audio_bus(loaded_value)
# 外部向けシグナル
emit_signal("setting_changed", loaded_value)
# スライダーのシグナルに接続
# Godot 4: "value_changed" シグナル
if not _slider.value_changed.is_connected(_on_slider_value_changed):
_slider.value_changed.connect(_on_slider_value_changed)
_is_ready = true
func _init_slider_reference() -> void:
## スライダー参照を初期化する。
if slider_node:
_slider = slider_node
return
if auto_find_slider:
var parent := get_parent()
if parent and parent is Range:
_slider = parent
else:
_slider = null
else:
_slider = null
func _load_value_from_config() -> float:
## ConfigFile から値を読み込む。存在しない場合は default_value を返す。
var err := _config.load(config_path)
if err != OK:
# ファイルがまだない場合など。ここでは特にエラーにせず default を使う。
return default_value
if not _config.has_section_key(config_section, config_key):
return default_value
var raw_value = _config.get_value(config_section, config_key, default_value)
if typeof(raw_value) == TYPE_FLOAT or typeof(raw_value) == TYPE_INT:
return float(raw_value)
# 型が違う場合も default にフォールバック
return default_value
func _save_value_to_config(value: float) -> void:
## 値を ConfigFile に保存する。
## - normalize_to_01 が true の場合は 0〜1 に正規化して保存
## - false の場合はそのまま保存
var stored_value := value
if normalize_to_01 and _slider:
stored_value = _to_normalized_01(value)
var err := _config.load(config_path)
if err != OK:
# ファイルがまだない場合は新規作成扱い
_config = ConfigFile.new()
_config.set_value(config_section, config_key, stored_value)
err = _config.save(config_path)
if err != OK:
push_warning("[SettingSlider] Config の保存に失敗しました: %s" % config_path)
func _apply_value_to_slider(value: float) -> void:
## 読み込んだ値をスライダーに反映する。
if not _slider:
return
var slider_value := value
if normalize_to_01:
# Config に 0〜1 で入っている前提なので、スライダーの範囲に拡大
slider_value = _from_normalized_01(value)
# 範囲外にならないよう clamp
slider_value = clampf(slider_value, _slider.min_value, _slider.max_value)
_slider.value = slider_value
func _apply_value_to_audio_bus(value: float) -> void:
## AudioServer に値を反映する(音量系設定向け)。
if not apply_to_audio_bus:
return
var bus_index := AudioServer.get_bus_index(audio_bus_name)
if bus_index < 0:
push_warning("[SettingSlider] Audio bus '%s' が見つかりません。" % audio_bus_name)
return
# value は「論理値」。normalize_to_01 が true なら 0〜1, false ならスライダーの値。
var normalized := value
if not normalize_to_01 and _slider:
normalized = _to_normalized_01(value)
# 0〜1 を dB 範囲にマッピング
var db_value := lerpf(min_db, max_db, clampf(normalized, 0.0, 1.0))
AudioServer.set_bus_volume_db(bus_index, db_value)
func _to_normalized_01(v: float) -> float:
## スライダーの min〜max を 0〜1 に正規化
if not _slider:
return v
if is_equal_approx(_slider.max_value, _slider.min_value):
return 0.0
return (v - _slider.min_value) / (_slider.max_value - _slider.min_value)
func _from_normalized_01(n: float) -> float:
## 0〜1 の値をスライダーの min〜max に拡大
if not _slider:
return n
return lerpf(_slider.min_value, _slider.max_value, n)
func _on_slider_value_changed(new_value: float) -> void:
## スライダーの値が変化したときに呼ばれるコールバック。
if not _is_ready:
# ready 前のノイズは無視
return
var logical_value := new_value
if normalize_to_01 and _slider:
# Config には 0〜1 で保存
logical_value = _to_normalized_01(new_value)
# ConfigFile に保存
_save_value_to_config(logical_value)
# AudioServer などに反映
_apply_value_to_audio_bus(logical_value)
# 外部に通知
emit_signal("setting_changed", logical_value)
## 外部から値をセットしたい場合用のヘルパー。
## スライダーと ConfigFile を両方更新します。
func set_setting_value(value: float, already_normalized: bool = false) -> void:
if not _slider:
return
var slider_value := value
var logical_value := value
if normalize_to_01:
if already_normalized:
# 0〜1 の値が来たとみなしてスライダーに拡大
slider_value = _from_normalized_01(value)
logical_value = value
else:
# スライダー値が来たとみなして正規化
slider_value = value
logical_value = _to_normalized_01(value)
else:
# 正規化しない場合はそのまま
slider_value = value
logical_value = value
_slider.value = slider_value
_save_value_to_config(logical_value)
_apply_value_to_audio_bus(logical_value)
emit_signal("setting_changed", logical_value)
使い方の手順
ここでは、典型的な「オプション画面」の例として、マスターボリューム と 画面の明るさ をそれぞれスライダーで調整するケースを見ていきます。
① コンポーネントスクリプトを用意する
res://addons/components/SettingSlider.gdなど、好きな場所に上記コードを保存します。- Godot エディタで一度プロジェクトを再読み込みすると、
SettingSliderがクラスとして認識されます。
② シーン構成:オプションメニューにアタッチ
例として、こんなシーンを想定します。
OptionsMenu (Control)
├── VBoxContainer
│ ├── HBoxContainer
│ │ ├── Label ("Master Volume")
│ │ └── MasterVolumeSlider (HSlider)
│ ├── HBoxContainer
│ │ ├── Label ("Brightness")
│ │ └── BrightnessSlider (HSlider)
│ └── Button ("Back")
├── SettingSlider_Master (Node) # マスターボリューム用コンポーネント
└── SettingSlider_Brightness (Node) # 明るさ用コンポーネント
ポイントは、各スライダーに対して1つの SettingSlider ノードを用意することです。
Godot 的な「深いツリー」ではなく、プレーンなノード + コンポーネントノードの組み合わせで管理するイメージですね。
③ インスペクタでパラメータを設定する
MasterVolume 用の SettingSlider_Master ノードを選択し、インスペクタで以下を設定します。
config_path:user://settings.cfgconfig_section:"audio"config_key:"master_volume"default_value:1.0(100%)normalize_to_01:trueauto_find_slider:false(今回は明示的にスライダーを指定してみます)slider_node:MasterVolumeSliderをドラッグ&ドロップapply_to_audio_bus:trueaudio_bus_name:"Master"(プロジェクトのバス構成に合わせて)min_db:-40.0,max_db:0.0など好みで
Brightness 用の SettingSlider_Brightness では、例えば以下のようにします。
config_path:user://settings.cfgconfig_section:"video"config_key:"brightness"default_value:0.5normalize_to_01:trueauto_find_slider:falseslider_node:BrightnessSliderapply_to_audio_bus:false(音量ではないので)
明るさの値は setting_changed シグナルを使って、ポストプロセスやシェーダーに反映できます。
④ 明るさをシェーダーに反映する例
例えば、OptionsMenu にこんなスクリプトを付けて、明るさスライダーの変更を画面全体のシェーダーに反映してみます。
extends Control
@onready var brightness_slider_comp: SettingSlider = $SettingSlider_Brightness
@onready var brightness_rect: ColorRect = $"../BrightnessOverlay" # 画面全体を覆う ColorRect など
func _ready() -> void:
# SettingSlider のシグナルに接続
if not brightness_slider_comp.setting_changed.is_connected(_on_brightness_changed):
brightness_slider_comp.setting_changed.connect(_on_brightness_changed)
func _on_brightness_changed(value: float) -> void:
# value は 0〜1 の想定(normalize_to_01 = true のため)
# ここでは単純に ColorRect の不透明度を変える例
brightness_rect.modulate.a = 1.0 - clampf(value, 0.0, 1.0)
こうしておけば、スライダーの値変更 → Config 保存 → シェーダー反映までがコンポーネント経由で自動的につながります。
メリットと応用
SettingSlider を使うことで、設定周りのコードがかなりスッキリします。
- 各スライダーはただの UI として存在し、保存ロジックはコンポーネントに集約される
- 新しい設定項目を追加したいときは、スライダー + SettingSlider ノードをコピペして、キー名だけ変えればOK
- ConfigFile のパスやセクション名も @export で明示されるので、後から見返しても分かりやすい
- 「音量は AudioServer にも反映したい」「明るさはシェーダーに反映したい」といった違いも、コンポーネントの設定&シグナル接続だけで対応できる
継承ベースで MasterVolumeSlider.gd, BrightnessSlider.gd … と増やしていくより、「スライダーは素のまま」「ロジックは SettingSlider に合成」という構成の方が、長期的にメンテしやすいですね。
改造案:一括で「ミュート」する機能を追加する
例えば、マスターボリュームを一時的にミュートしたい場合、SettingSlider にこんなヘルパーを追加しておくと便利です。
func toggle_mute(is_mute: bool) -> void:
## マスターボリューム用の簡易ミュート機能。
## - ミュート中は AudioServer のボリュームだけ下げる
## - ConfigFile の値は変更しない(ユーザーの設定値は保持)
if not apply_to_audio_bus:
return
var bus_index := AudioServer.get_bus_index(audio_bus_name)
if bus_index < 0:
return
if is_mute:
AudioServer.set_bus_volume_db(bus_index, min_db)
else:
# 現在の設定値を再適用
var current_value := _load_value_from_config()
_apply_value_to_audio_bus(current_value)
こうしておけば、「ポーズメニューを開いたら一時的にミュート」「ポーズ解除で元の音量に戻す」といった挙動も簡単に実装できます。
このように、1つのコンポーネントを育てていくスタイルにすると、プロジェクトが大きくなっても「設定周りは全部 SettingSlider で見ればOK」という状態を維持しやすくなります。
継承より合成、どんどん試していきましょう。
