Godotでメニュー画面やUIを作っていると、「ボタンが押されたらシーンを切り替えたい」という場面は必ず出てきますよね。
多くの入門記事では、こんな感じの実装が紹介されがちです。
extends Button
func _on_pressed() -> void:
get_tree().change_scene_to_file("res://scenes/game.tscn")
これでも動きますが、問題はここからです。
- タイトルボタン、リトライボタン、ステージ選択ボタン…ボタンの数だけスクリプトを量産しがち
- 「継承」で Button を拡張してしまうと、別の共通処理を入れたいときに継承ツリーがぐちゃぐちゃになる
- UIデザイナーやレベルデザイナーにとって、シーン遷移の設定がコード依存になってしまう
つまり、「ボタンの見た目」と「シーン遷移のロジック」がベッタリ結合してしまうわけですね。
そこで登場するのが、コンポーネントとしてアタッチするだけでシーン遷移を担当してくれる「SceneChanger」コンポーネントです。
【Godot 4】ボタンはただのボタンに!シーン遷移は「SceneChanger」コンポーネントに丸投げ
今回作る SceneChanger コンポーネントは、
- 自分の親ノードが Button だったら、その pressed シグナルを自動接続
- 押されたら、指定したシーンパスに遷移
- フェード時間などの演出オプションも一応持たせておく(拡張しやすい)
という「ボタンにくっつけるだけ」のシンプルなコンポーネントです。
ボタン側は 一切スクリプトを書かず、SceneChanger をアタッチしてパスを設定するだけでOKにしてしまいましょう。
フルコード:SceneChanger.gd
extends Node
class_name SceneChanger
## 親の Button が押されたときに、指定したシーンへ遷移するコンポーネント。
##
## 想定するノード構成:
## Button
## └── SceneChanger (このスクリプトをアタッチした Node)
##
## Button 側にはスクリプト不要で、SceneChanger 側の設定だけでシーン遷移が完結します。
@export_file("*.tscn") var target_scene_path: String = ""
## 遷移先のシーンパス。
## 例: res://scenes/game.tscn
## 空文字のままだと押されても何もしません(警告を出すだけ)。
@export var auto_disable_button: bool = true
## 遷移開始時に親の Button を自動的に disabled にするかどうか。
## 二重クリックによる多重ロードを防ぐ目的です。
@export var use_transition: bool = false
## true の場合、簡易フェード演出を入れてからシーンを切り替えます。
## 実際のプロジェクトでは、ここを自前のトランジションマネージャー呼び出しに差し替えるとよいです。
@export_range(0.0, 5.0, 0.1) var fade_time: float = 0.5
## use_transition が true のときのフェード時間(秒)。
## 今回は簡易的に、非同期で待つだけのダミー実装です。
var _button: Button
func _ready() -> void:
## 親ノードが Button かどうかをチェックし、シグナルを自動接続します。
_button = _find_parent_button()
if _button == null:
push_warning("SceneChanger: 親に Button が見つかりません。このコンポーネントは Button の子として配置してください。")
return
# すでに接続済みでなければ、pressed シグナルを接続
if not _button.pressed.is_connected(_on_button_pressed):
_button.pressed.connect(_on_button_pressed)
func _find_parent_button() -> Button:
## 直近の親が Button であることを想定しつつ、
## 念のため上方向にたどって Button を探します。
var current: Node = get_parent()
while current:
if current is Button:
return current
current = current.get_parent()
return null
func _on_button_pressed() -> void:
## Button が押されたときに呼ばれるコールバック。
if target_scene_path.is_empty():
push_warning("SceneChanger: target_scene_path が設定されていません。シーン遷移をスキップします。")
return
if auto_disable_button and _button:
_button.disabled = true
if use_transition:
# 簡易的なトランジション(フェード)処理。
# 実プロジェクトでは、ここを TransitionManager 的なシングルトンに置き換えると良いです。
_change_scene_with_fade()
else:
_change_scene_immediately()
func _change_scene_immediately() -> void:
## 即座にシーンを切り替えるシンプルな実装。
var error := get_tree().change_scene_to_file(target_scene_path)
if error != OK:
push_error("SceneChanger: シーンの読み込みに失敗しました: %s" % target_scene_path)
if auto_disable_button and _button:
_button.disabled = false
func _change_scene_with_fade() -> void:
## 疑似的なフェード演出付きシーン遷移。
## ここでは単に時間待ちしてからシーンを切り替えるだけのダミー実装です。
## 実際にはフェード用 CanvasLayer を制御したり、アニメーションを再生したりします。
_run_fade_transition()
@func
async func _run_fade_transition() -> void:
# ここでは "フェード中" という体で、指定時間待ってからシーン遷移します。
# await を使うので、Godot 4 以降が前提です。
if fade_time > 0.0:
await get_tree().create_timer(fade_time).timeout
var error := get_tree().change_scene_to_file(target_scene_path)
if error != OK:
push_error("SceneChanger: シーンの読み込みに失敗しました: %s" % target_scene_path)
if auto_disable_button and _button:
_button.disabled = false
使い方の手順
前提として、以下のようなシーンがあるとします。
- TitleScene(タイトル画面)
- GameScene(ゲーム本編)
TitleScene の「スタートボタン」を押したら GameScene に遷移したい、という例で解説します。
手順①:コンポーネントスクリプトを作成
- 任意のフォルダ(例:
res://addons/components/)にSceneChanger.gdを作成 - 上記のフルコードをコピペ保存
これで、インスペクタから SceneChanger というスクリプトが選べるようになります。
手順②:ボタンの子として SceneChanger ノードを追加
TitleScene のシーン構成を、例えばこんな感じにします。
TitleScene (Control) ├── MarginContainer │ └── VBoxContainer │ ├── StartButton (Button) │ │ └── SceneChanger (Node) ← このノードにコンポーネントをアタッチ │ └── QuitButton (Button) └── Background (TextureRect)
StartButton の子として Node を1つ追加し、そのノードに SceneChanger.gd をアタッチします。
(ノード名は SceneChanger としておくと分かりやすいです)
手順③:インスペクタでシーンパスを設定
SceneChanger ノードを選択すると、インスペクタに以下のようなプロパティが見えます。
- target_scene_path: 遷移先のシーンパス(例:
res://scenes/GameScene.tscn) - auto_disable_button: 押された直後にボタンを無効化するか(多重押し防止)
- use_transition: フェード演出を挟むか
- fade_time: フェード時間(秒)
ここでは target_scene_path に GameScene のパスを設定しておきましょう。
これだけで、StartButton にシーン遷移機能が追加されます。
手順④:他のボタンにもコピペで使い回す
たとえば、ゲームオーバー画面からタイトルに戻るボタンにも同じコンポーネントを使えます。
GameOverScene (Control)
├── Label
├── RetryButton (Button)
│ └── SceneChanger (Node)
└── BackToTitleButton (Button)
└── SceneChanger (Node)
- RetryButton の SceneChanger:
target_scene_path = res://scenes/GameScene.tscn - BackToTitleButton の SceneChanger:
target_scene_path = res://scenes/TitleScene.tscn
ボタン側には一切スクリプトを書かず、どのボタンがどこに飛ぶかは SceneChanger の設定だけで完結します。
まさに「継承より合成」で、ボタンはただのボタンにしておけるのがポイントですね。
メリットと応用
SceneChanger コンポーネントを導入すると、こんなメリットがあります。
- UIごとにスクリプトを量産しなくてよい
→ タイトル、ポーズメニュー、オプション画面など、どこでも同じコンポーネントを再利用できます。 - シーン構造がシンプルで読みやすい
→ 「このボタンはどこに飛ぶの?」が、インスペクタのtarget_scene_pathを見るだけで一目瞭然。 - ボタンの見た目とロジックが分離
→ アート担当やレベルデザイナーは見た目だけをいじり、エンジニアはコンポーネントをメンテナンスする、という役割分担がしやすくなります。 - 演出の共通化が簡単
→ フェード演出や SE 再生などを SceneChanger に集約すれば、全ボタンの挙動を一括で変えられます。
特に、Godot の「ノードに直接スクリプトを継承でベタ付けする」スタイルを続けていると、UI周りがスクリプトだらけでカオスになりがちです。
SceneChanger のように 小さくて再利用可能なコンポーネントを積み上げていくと、プロジェクトの見通しがかなり良くなりますね。
改造案:BGM フェードアウトも一緒にやる
例えば、シーン遷移前に BGM をフェードアウトしたい場合は、
Autoload(シングルトン)で AudioManager を用意しておき、SceneChanger から呼ぶようにするときれいにまとまります。
SceneChanger に次のような関数を追加してみましょう。
func _fadeout_bgm_before_change() -> void:
# AudioManager は Autoload(Project Settings > Autoload)で登録されている想定
if Engine.has_singleton("AudioManager"):
var audio_manager = Engine.get_singleton("AudioManager")
if "fadeout_bgm" in audio_manager:
audio_manager.fadeout_bgm(fade_time)
そして _run_fade_transition() の先頭で呼び出せば、シーン遷移する全ボタンから自動的に BGM フェードアウトがかかるようになります。
こんな感じで、「ボタンを押したら起きてほしいこと」をどんどん SceneChanger に集約していけば、
UIロジックがバラバラに散らばらず、コンポーネント指向らしい気持ちいい設計になっていきますね。
