【Godot】Godotの設定画面入門:3つの設定を1つの画面にまとめ、開いただけで値が消えないようにする

作成: 2026-09-23最終更新: 2026-10-10

音量・言語・最大化の3つを1つの設定画面にまとめ、ConfigFileに残して、開いただけでは値が変わらないようにする方法を解説します。

設定画面は、音量や言語のような値を変えて、すぐゲームに効かせ、次の起動まで残すための画面です。Godot では、値を Autoload で覚え、ConfigFile でファイルに残して作ります。

このとき、開くときにファイルの値を画面へ戻す1行が、人が操作したときと同じ合図(シグナル)を出します。その合図で保存が走ると、何も触らずに閉じただけで、前回選んだ言語が初期値に戻ります。

この記事では、音量・言語・最大化の3つを1つの画面にまとめ、開いただけでは値が変わらない設定画面の作り方を解説します。

開いただけで設定ファイルの言語が en から ja に変わる画面と、開いても en のまま変わらない画面の比較

この記事でわかること

  • 設定画面の仕事が「変える」「効かせる」「残す」の3つに分かれること
  • コードからの代入もシグナルを出すことと、出さずに値を入れる書き方
  • ファイルへ書く値を、画面ではなく覚えている側から取る理由
  • 実践:音量・言語・最大化を1画面にまとめ、再生し直しても値がそろうことを確かめる

動作確認環境: Godot 4.7

Sponsored

設定画面の3つの仕事

設定画面の仕事は3つに分かれます。スライダーやドロップダウンで値を「変える」、その値をゲームに「効かせる」、次の起動のためにファイルへ「残す」です。

設定画面の仕事を、画面の「変える」、エンジンへの「効かせる」、ファイルへの「残す」の3つに分けた図
仕事担当中身
変える画面の Controlスライダー・ドロップダウン・チェックが並ぶ
効かせる覚えている側AudioServer(音)・ TranslationServer(言語)・ DisplayServer(ウィンドウ)を呼ぶ
残す覚えている側ConfigFile で user://settings.cfg へ書く

「覚えている側」は、設定の値を1か所で持つノードで、あとの節で作ります。user:// は、ゲームごとに用意される書き込み用のフォルダです。AudioServer などの3つはノードではなく、エンジンに最初から1つずつある窓口で、シーンに置かなくても、どのスクリプトからも名前で呼べます。

画面は値を渡すだけで、エンジンにもファイルにも触りません。この線を引いておくと、あとの節で出てくる問題がほとんど消えます。

効かせる中身は、それぞれ別の記事が持っています。音量のバスは オーディオ入門 、言語の切り替えは 多言語対応入門 、キーの差し替えは InputMap入門 、画面の見た目は UIレイアウト入門 です。ConfigFile の書式そのものは セーブ/ロード入門 にあります。

Sponsored

コードからの代入とシグナル

シグナル は、ボタンを押したりスライダーをつまんだりしたときに出るもの、という感覚で書いてしまいがちです。ところが HSlider の value_changed は、「値が変わった」ことを知らせるシグナルです。変えたのが人かコードかを区別しません。CheckButton の toggled も同じです。

冒頭で書いた事故は、ここから起きます。画面の値を集めてファイルへ書く保存関数を、value_changed につないでいたとします。開くときに slider.value = ... でファイルの値を戻すと、その代入で保存関数が走ります。そのとき言語のドロップダウンはまだ戻しておらず、最初の項目の「日本語」を選んだままです。ファイルの en は、ここで ja に上書きされます。

ファイルの en を読み、スライダーへの代入がシグナルを出し、まだ日本語のままのドロップダウンから ja を書き戻す4段の流れ

値を入れることと、知らせることを切り離す入口が、それぞれのノードに用意されています。

ノードシグナルが出る書き方出ない書き方
HSliderslider.value = 0.3slider.set_value_no_signal(0.3)
CheckButtoncheck.button_pressed = truecheck.set_pressed_no_signal(true)
OptionButton—option.select(1)

他のエンジンでいうと: Unity の Slider.SetValueWithoutNotify() と Toggle.SetIsOnWithoutNotify() にあたります。Unity でも slider.value への代入は onValueChanged を呼びます。

素の代入はスライダーから関数へ知らせが届き、専用の入口では知らせる矢印が途中で止まる上下2段の図

OptionButton だけ、シグナルが出る書き方の欄が空です。select() はもともと item_selected を出さないので、値を戻すときもそのまま呼べます。

ファイルへ書く値の出どころ

シグナルを止めても、危うさはもう1つ残っています。さきほどの保存関数は、画面に並んだコントロールを見て回って、そのときの値をファイルへ書いていました。

画面のコントロールから値を集める保存関数と、音量・言語・最大化を覚えた1枚の変数から書く保存関数の対比

この書き方だと、保存が走る時刻に画面が完成していることが前提になります。開いた直後や、コントロールを1つ足した直後に、その前提が崩れます。

そこで、値を覚えている側を1つ作り、そこを唯一の出どころにします。画面は値を渡すだけで、ファイルへは覚えている側の変数からだけ書きます。画面がどこまで組み上がっていても、保存の中身は変わりません。

覚えている側は Autoload に置きます。Autoload はシーンを切り替えても残り続けるノードで、最初のシーンが出る前に読み込まれます。起動した瞬間にファイルを読んで、音量や言語を効かせておけます。

Sponsored

ファイルへ書くタイミング

value_changed のたびにファイルへ書くと、スライダーを1回ドラッグするだけで何十回も書き込みが走ります。

つまんでから離すまでは音にだけ効かせ、離した瞬間に1回だけファイルへ書く時間軸の図

スライダーなら、つまんでいるあいだは音量だけを更新し、指を離したときに出る drag_ended で1回だけ書きます。ドロップダウンやチェックは連続値ではないので、操作した瞬間に効かせて、その場で書いて問題ありません。

drag_ended は、キーボードやゲームパッドの左右、ホイールで値を変えたときには出ません。その取りこぼしは、閉じるときにもう一度書いて拾います。ゲームのウィンドウを閉じようとすると、NOTIFICATION_WM_CLOSE_REQUEST という通知が届きます。エディタの停止ボタンではこの通知が届かないので、確かめるときはウィンドウの × で閉じてください。

既定値の持ち方

設定ファイルは、手で開いて書き換えられます。前の版から項目が増えていることもあります。読み込みでは、次の3つをどれも同じコードの定数へ戻します。

ファイルが無い・キーが無い・値がおかしい、の3つの失敗が、同じ1組のコードの定数へ戻る図

ファイルが無いか読めないときは、ConfigFile.load() が OK を返さないので、定数のまま起動します。ConfigFile のファイルは、[audio] のような見出し(セクション)の下に、master=0.8 のような「キー=値」の行が並んだものです。キーが無いときは、get_value() の第3引数に渡した定数が、そのキーにだけ入ります。型が違う・範囲の外、のように値がおかしいときも、読み込みのコードで定数に戻します。

定数を1組だけ持っておけば、「既定に戻す」ボタンにも同じものを使えます。ただし、ボタンでファイルを消すだけでは足りません。いま鳴っている音量と、いまの言語が古いままになります。

変数を定数に戻し、エンジンへ効かせ、画面へシグナルを出さずに反映し、ファイルへ書きます。この4つがそろって、押した瞬間の見た目と次の起動が一致します。

実践:音量・言語・最大化を1つの画面にまとめる

RPGのオプション、対戦ゲームの設定タブ、モバイルの歯車アイコン——設定画面の中身はジャンルが変わっても似た顔ぶれになります。ここでは種類の違う3つを1画面に置いて、開いても値が消えないことを確かめます。

① 置くもの

新しいシーンを1つ作ります。

  • ルートに Control を置いて名前を Main にし、Main Scene に指定する。Anchors Preset は Full Rect。あとで書く main.gd を付ける
  • Main の子に PanelContainer を置いて Box 。Anchors Preset を Center にし、Custom Minimum Size を (460, 0) にする
  • Box の子に VBoxContainer を置いて Rows
  • Rows の子に HBoxContainer を2つ置いて VolumeRow と LocaleRow 。それぞれの子に Label (Text は「音量」「言語」)と、HSlider の VolumeSlider ・ OptionButton の LocaleOption を置く
  • VolumeSlider の Min 0 ・Max 1 ・Step 0.01 。Layout の Container Sizing で Horizontal に Expand と Fill を付ける(行の残りの幅いっぱいまでつまみを伸ばす)
  • Rows の子に CheckButton の MaximizeCheck (Text は「最大化」)、Button の ResetButton (Text は「既定に戻す」)、Label の StateLabel を置く
  • VolumeSlider ・ LocaleOption ・ MaximizeCheck ・ ResetButton ・ StateLabel の5つに、右クリックメニューの「Access as Unique Name」を付ける。コードから %VolumeSlider のように名前で取れるようになる
  • settings.gd を作り、Project Settings の Globals → Autoload に Settings という名前で登録する

ウィンドウの最大化は、エディタにゲームを埋め込んだまま再生すると効きません。Game タブの右上のメニューで Embed Game on Next Play のチェックを外してから再生します。

② 組み立て

覚えている側です。ファイルの読み書きと、エンジンへの適用が、ここに閉じています。

# settings.gd(Autoload に Settings の名前で登録)
extends Node

const PATH := "user://settings.cfg"
const LOCALES := ["ja", "en"]
const DEFAULT_VOLUME := 0.8
const DEFAULT_LOCALE := "ja"
const DEFAULT_MAXIMIZED := false

var volume := DEFAULT_VOLUME
var locale := DEFAULT_LOCALE
var maximized := DEFAULT_MAXIMIZED

func _ready() -> void:
	var config := ConfigFile.new()
	if config.load(PATH) == OK:                       # 読めなければ定数のまま
		var v = config.get_value("audio", "master", DEFAULT_VOLUME)
		volume = v if v is float and v >= 0.0 and v <= 1.0 else DEFAULT_VOLUME
		locale = _known(str(config.get_value("display", "locale", DEFAULT_LOCALE)))
		var m = config.get_value("display", "maximized", DEFAULT_MAXIMIZED)
		maximized = m if m is bool else DEFAULT_MAXIMIZED   # 型が違えば定数へ
	_apply()

func set_volume(value: float) -> void:
	volume = clampf(value, 0.0, 1.0)
	AudioServer.set_bus_volume_db(0, linear_to_db(volume))   # 0〜1 をデシベルに直して Master(0番)へ

func set_locale_code(code: String) -> void:
	locale = _known(code)
	TranslationServer.set_locale(locale)
	save()

func set_maximized(on: bool) -> void:
	maximized = on
	DisplayServer.window_set_mode(
		DisplayServer.WINDOW_MODE_MAXIMIZED if on else DisplayServer.WINDOW_MODE_WINDOWED
	)
	save()

func reset() -> void:
	volume = DEFAULT_VOLUME
	locale = DEFAULT_LOCALE
	maximized = DEFAULT_MAXIMIZED
	_apply()
	save()

func save() -> void:
	var config := ConfigFile.new()
	config.set_value("audio", "master", volume)       # 画面ではなく、この変数から書く
	config.set_value("display", "locale", locale)
	config.set_value("display", "maximized", maximized)
	config.save(PATH)

func _notification(what: int) -> void:
	if what == NOTIFICATION_WM_CLOSE_REQUEST:          # ウィンドウを閉じようとしたという知らせ
		save()                                        # ホイールで変えた音量を取りこぼさない

func _known(code: String) -> String:
	return code if code in LOCALES else DEFAULT_LOCALE

func _apply() -> void:
	AudioServer.set_bus_volume_db(0, linear_to_db(volume))
	TranslationServer.set_locale(locale)
	DisplayServer.window_set_mode(
		DisplayServer.WINDOW_MODE_MAXIMIZED if maximized else DisplayServer.WINDOW_MODE_WINDOWED
	)

画面側は、値を渡すことと、状態を2行で見せることだけをします。

# main.gd(Main の Control)
extends Control

@onready var slider: HSlider = %VolumeSlider
@onready var option: OptionButton = %LocaleOption
@onready var check: CheckButton = %MaximizeCheck
@onready var state: Label = %StateLabel

func _ready() -> void:
	option.add_item("日本語")
	option.add_item("English")
	_pull()

	slider.value_changed.connect(_on_volume_changed)
	slider.drag_ended.connect(_on_drag_ended)
	option.item_selected.connect(_on_locale_selected)
	check.toggled.connect(_on_maximize_toggled)
	%ResetButton.pressed.connect(_on_reset_pressed)

func _on_volume_changed(value: float) -> void:
	Settings.set_volume(value)                        # エンジンにだけ効かせる(ファイルには書かない)
	_refresh()

func _on_drag_ended(changed: bool) -> void:   # changed は音量ではなく、つまむ前と値が変わったかの真偽
	if changed:
		Settings.save()                               # 離したときに1回だけ書く
		_refresh()

func _on_locale_selected(index: int) -> void:
	Settings.set_locale_code(Settings.LOCALES[index])
	_refresh()

func _on_maximize_toggled(on: bool) -> void:
	Settings.set_maximized(on)
	_refresh()

func _on_reset_pressed() -> void:
	Settings.reset()
	_pull()

func _pull() -> void:
	slider.set_value_no_signal(Settings.volume)       # シグナルを出さずに戻す
	check.set_pressed_no_signal(Settings.maximized)
	option.select(Settings.LOCALES.find(Settings.locale))
	_refresh()

func _refresh() -> void:                              # 確かめるための表示
	var config := ConfigFile.new()
	config.load(Settings.PATH)                        # 読めなければ、下の2つは -1 と「—」になる
	var saved_volume: float = config.get_value("audio", "master", -1.0)
	var saved_locale := str(config.get_value("display", "locale", "—"))
	state.text = "音量 画面 %.2f / エンジン %.2f / ファイル %.2f\n言語 画面 %s / エンジン %s / ファイル %s" % [
		slider.value, db_to_linear(AudioServer.get_bus_volume_db(0)), saved_volume,
		Settings.LOCALES[maxi(option.selected, 0)], TranslationServer.get_locale(), saved_locale
	]

_refresh() はこの記事のための表示で、実際のゲームには要りません。3つの場所がいま何を持っているかを、そのまま並べています。値が変わるたびにファイルを読み直すので、確かめ終わったら外してください。

③ 動かして確かめる

1回目の再生で3つの値がそろい、再生し直してもそろったまま、ドラッグ中はファイルだけが古く、離すとそろう4コマ

再生して、言語を「English」にし、音量のつまみを左へ寄せて 0.20 あたりにし、「最大化」をオンにします。ウィンドウが画面いっぱいに広がり、下の2行は音量が3つとも同じ数、言語が3つとも en になります。広がらないときは、まず Embed Game on Next Play のチェックが外れているかを見てください。

エディタの停止ボタンで止めて、もう一度再生します。ウィンドウは最大化された状態で立ち上がり、つまみは 0.20 の位置、ドロップダウンは「English」です。何も触らずに停止ボタンで止めて settings.cfg を開いても、locale="en" のままです。ファイルの更新日時も前回のままで、開いただけでは1回も書いていないことが分かります。保存先のフォルダは、エディタの Project メニューの Open User Data Folder で開けます。

次に、つまみを右の 0.70 あたりまでドラッグします。動かしているあいだは 画面 と エンジン だけが動き、ファイル は 0.20 のままです。指を離した瞬間に ファイル も 0.70 になります。「既定に戻す」を押すと、ウィンドウが元の大きさに戻ります。つまみは 0.80 、ドロップダウンは「日本語」になり、2行とも3つの値がそろいます。

_on_volume_changed に save を1行足すと、ドラッグ中もファイルの数字がついてくる

仕上げに1行足して、書く時刻の違いを見ます。main.gd の _on_volume_changed() で、Settings.set_volume(value) の次の行に Settings.save() を足します。再生して、つまみをドラッグしてください。今度はドラッグ中も ファイル の数字がついてきます。1回のドラッグのあいだに、ファイルを何十回も書いているということです。足した1行を消せば、書くのは指を離したときの1回に戻ります。

ポイントは2つです。

  • 代入と操作は同じ入口を通る: 画面へ値を戻すときは set_value_no_signal() と set_pressed_no_signal() で入れます。OptionButton だけは select() がもともとシグナルを出さないので、そのまま呼べます
  • 書く値の出どころを1つにする: save() が見ているのは Settings の変数だけです。画面がどこまで組み上がっていても、ファイルの中身は変わりません

項目を増やすときは、定数と変数を1組足します。そのうえで、_ready() の読み込み・ _apply() ・ reset() ・ save() ・画面側の _pull() に1行ずつ足します。触る場所が決まっているので、増えても散らかりません。キーの割り当てまで入れるなら、差し替えの作法は InputMap入門 にあります。

うまくいかないときは

症状原因直し方
効果音用のスライダーを足したら、動かしても音量がほとんど変わらない0〜1 の値をそのまま set_bus_volume_db() に渡している。受け取るのはデシベルで、0 は元の音量のままlinear_to_db() で変換してから渡す
設定画面をポーズメニューから開くようにしたら、スライダーもボタンも反応しないget_tree().paused = true のあいだは、Process Mode が既定の Inherit の画面も止まり、入力を受け取らない設定画面のルート(Main)の Process Mode を Always にする(ポーズ入門)
ゲームに「終了」ボタンを足して閉じると、ホイールで変えた音量が残らないget_tree().quit() で閉じると、NOTIFICATION_WM_CLOSE_REQUEST が届かない終了ボタンでは、quit() の前に Settings.save() を呼ぶ
言語を「English」にしても、画面の文字が英語にならないset_locale() は言語を切り替えるだけで、訳文は翻訳ファイルから来る翻訳ファイルを Project Settings の Localization → Translations に登録する(多言語対応入門)

おまけ:先に知っておくと良いこと

  • セーブデータとは別のファイルに分ける: 設定と進行は寿命が違います。セーブを消したら音量まで消えた、という事故は入れ物を分けるだけで防げます。項目の形式を後から変えるときは、版の番号を入れて変換します(セーブデータのバージョン管理)
  • 「キャンセル」は後回しでよい: 変えた瞬間に効かせる作りと両立させるには、開いた時点の状態を一式覚えておく必要があります。まずは「既定に戻す」だけで足ります
Sponsored

まとめ

  • 3つの場所に分ける: 変えるのは画面、効かせるのと残すのは Autoload の Settings
  • 代入もシグナルを出す: 画面へ戻すときは set_value_no_signal() ・ set_pressed_no_signal() 。OptionButton.select() はもともと出さない
  • 書く値は変数から: 画面のコントロールを見て回らないので、組み上がり具合に左右されない
  • 書くのは操作の終わり: ドラッグ中はエンジンだけ。drag_ended とウィンドウを閉じるときの通知で残す
  • 既定値はコードに1組: ファイルが無い・キーが無い・値がおかしい、の3つとも同じ定数へ戻す

まずは実践の3項目を動かして、画面 と エンジン と ファイル の3つがそろうところを見てください。言語の切り替えを実際の訳文までつなぐなら、多言語対応入門 に続きがあります。あなたのゲームの設定画面には、何を並べますか。

次に読む次のカテゴリ「3D」へ
3D

Node3DとMeshInstance3Dで学ぶGodotの3D空間基本概念

この記事を読む
Godot このセクションのノート104