Godotでシーン遷移を作るとき、つい「各シーンごとにアニメーションを仕込む」「CanvasLayerにColorRectを置いてAnimationPlayerで…」みたいな実装をしがちですよね。
でもこのやり方、
- シーンごとに同じようなアニメーションを量産することになる
- フェード時間を変えたいだけで、複数シーンを修正する羽目になる
- 「このシーンだけフェードしないで!」みたいな例外対応が面倒
と、地味に管理コストが高いです。
さらに、Godotの典型的なやり方だと「フェード専用シーン」を継承して増やしていったり、ルートノードにべったり依存したスクリプトになりがちです。
そこでこの記事では、「シーン遷移時の暗転フェード」を完全にコンポーネント化した ScreenFader を紹介します。
どのシーンにもポン付けできて、継承なし・深いノード階層なし・依存度低め で運用できるようにしていきましょう。
【Godot 4】どのシーンにもポン付けOK!「ScreenFader」コンポーネント
今回作る ScreenFader は、
- 画面全体を覆う黒いRect(ColorRect)を自動で生成
- フェードイン / フェードアウトをメソッド呼び出しだけで制御
- シーンをまたいで生き残らせる(AutoLoad)運用も可能
- シグナルで「フェード完了」を通知
という、完全に「画面の暗転」だけに責務を絞ったコンポーネントです。
どのシーンにも共通で使えるように、Node ベースのシンプルなコンポーネントとして実装していきます。
フルコード(GDScript / Godot 4)
extends Node
class_name ScreenFader
##
## ScreenFader コンポーネント
## - 黒いColorRectを自動生成して、画面全体のフェードイン・フェードアウトを行う
## - どのシーンにもアタッチできる汎用コンポーネント
##
## フェード完了時に発火するシグナル
signal fade_in_finished
signal fade_out_finished
## === 設定パラメータ ===
## 画面全体を覆うかどうか
@export var cover_entire_viewport: bool = true
## フェードに使う色(デフォルトは黒)
@export var fade_color: Color = Color.BLACK
## デフォルトのフェード時間(秒)
@export_range(0.01, 10.0, 0.05)
@export var default_duration: float = 0.6
## フェード処理中に入力をブロックするかどうか
## true の場合、透明度が 0 でない間は ColorRect が入力をキャッチして背後のUIを触れなくする
@export var block_input_while_fading: bool = true
## 現在のアルファ値(0.0 ~ 1.0)
var _alpha: float = 0.0
## 内部で使う ColorRect
var _rect: ColorRect
## フェード用のTween
var _tween: Tween
func _ready() -> void:
## ここで ColorRect を自動生成してセットアップする
_create_rect()
_apply_alpha()
## 最初は完全に透明にしておく
_set_visible_if_needed()
func _create_rect() -> void:
_rect = ColorRect.new()
_rect.color = fade_color
_rect.name = "ScreenFaderRect"
_rect.mouse_filter = block_input_while_fading \
if block_input_while_fading else Control.MOUSE_FILTER_IGNORE
## Viewport全体を覆うように設定
_rect.anchor_left = 0.0
_rect.anchor_top = 0.0
_rect.anchor_right = 1.0
_rect.anchor_bottom = 1.0
_rect.offset_left = 0.0
_rect.offset_top = 0.0
_rect.offset_right = 0.0
_rect.offset_bottom = 0.0
## CanvasLayerを挟んで、常に最前面に出す設計もあり
## ここでは自分の親がCanvasItem系でなくても動くように、
## 必要なら自前の CanvasLayer を用意する
var layer := CanvasLayer.new()
layer.name = "ScreenFaderLayer"
add_child(layer)
layer.add_child(_rect)
func _apply_alpha() -> void:
var c := _rect.color
c.a = clamp(_alpha, 0.0, 1.0)
_rect.color = c
func _set_visible_if_needed() -> void:
## 完全に透明なら非表示にしておく(描画コスト&入力ブロックを避ける)
_rect.visible = _alpha > 0.0
func _kill_tween() -> void:
if _tween and _tween.is_valid():
_tween.kill()
_tween = null
## === パブリックAPI ===
## 即座に指定アルファに設定する(0.0 ~ 1.0)
func set_alpha(value: float) -> void:
_kill_tween()
_alpha = clamp(value, 0.0, 1.0)
_apply_alpha()
_set_visible_if_needed()
## 即座に真っ黒にする(シーン切り替え前などに)
func snap_to_black() -> void:
set_alpha(1.0)
## 即座に透明にする
func snap_to_clear() -> void:
set_alpha(0.0)
## フェードイン(黒→透明)
## duration を省略した場合、default_duration を使用
func fade_in(duration: float = -1.0) -> Tween:
if duration <= 0.0:
duration = default_duration
_kill_tween()
_rect.visible = true
if block_input_while_fading:
_rect.mouse_filter = Control.MOUSE_FILTER_STOP
else:
_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
_tween = create_tween()
_tween.set_trans(Tween.TRANS_SINE)
_tween.set_ease(Tween.EASE_IN_OUT)
_tween.tween_property(self, "_alpha", 0.0, duration).from(_alpha)
_tween.tween_callback(Callable(self, "_on_fade_in_finished"))
return _tween
## フェードアウト(透明→黒)
func fade_out(duration: float = -1.0) -> Tween:
if duration <= 0.0:
duration = default_duration
_kill_tween()
_rect.visible = true
if block_input_while_fading:
_rect.mouse_filter = Control.MOUSE_FILTER_STOP
else:
_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
_tween = create_tween()
_tween.set_trans(Tween.TRANS_SINE)
_tween.set_ease(Tween.EASE_IN_OUT)
_tween.tween_property(self, "_alpha", 1.0, duration).from(_alpha)
_tween.tween_callback(Callable(self, "_on_fade_out_finished"))
return _tween
## シーン遷移のためのユーティリティ:
## 1. フェードアウト
## 2. コールバック(シーン切り替えなど)
## 3. フェードイン
func fade_out_in(callback: Callable, fade_out_time: float = -1.0, fade_in_time: float = -1.0) -> void:
if fade_out_time <= 0.0:
fade_out_time = default_duration
if fade_in_time <= 0.0:
fade_in_time = default_duration
_kill_tween()
_rect.visible = true
if block_input_while_fading:
_rect.mouse_filter = Control.MOUSE_FILTER_STOP
else:
_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
_tween = create_tween()
_tween.set_trans(Tween.TRANS_SINE)
_tween.set_ease(Tween.EASE_IN_OUT)
## 透明 → 黒
_tween.tween_property(self, "_alpha", 1.0, fade_out_time).from(_alpha)
## コールバック(シーン切り替えなど)
_tween.tween_callback(callback)
## 黒 → 透明
_tween.tween_property(self, "_alpha", 0.0, fade_in_time)
_tween.tween_callback(Callable(self, "_on_fade_in_finished"))
## === 内部コールバック ===
func _on_fade_in_finished() -> void:
_apply_alpha()
_set_visible_if_needed()
if block_input_while_fading:
_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
fade_in_finished.emit()
func _on_fade_out_finished() -> void:
_apply_alpha()
_set_visible_if_needed()
fade_out_finished.emit()
## プロパティのsetterで常にColorRectに反映する
func _set_alpha(value: float) -> void:
_alpha = clamp(value, 0.0, 1.0)
_apply_alpha()
_set_visible_if_needed()
## Tween から操作しやすいようにプロパティ化
@export var alpha: float:
get:
return _alpha
set(value):
_set_alpha(value)
使い方の手順
ここからは、実際に ScreenFader をシーンに組み込む手順を見ていきましょう。
手順①: スクリプトを作成してリソース化
- 上記コードを
res://components/screen_fader.gdなどに保存します。 class_name ScreenFaderを定義しているので、エディタから直接「ScreenFader」をノードとして追加できます。
手順②: 任意のシーンにコンポーネントとして追加
例として、「タイトル画面」から「ゲーム本編」へフェード付きで遷移するケースを考えます。
TitleScreen (Control) ├── VBoxContainer │ ├── Label │ └── StartButton (Button) └── ScreenFader (ScreenFader) <-- このコンポーネントを追加
- TitleScreenシーンを開き、「+」ボタンから ScreenFader ノードを追加します。
- インスペクタで
default_durationやfade_colorを好みに調整します。
手順③: フェード付きでシーンを切り替えるコードを書く
タイトル画面のスクリプト例:
extends Control
@onready var start_button: Button = %StartButton
@onready var fader: ScreenFader = %ScreenFader
func _ready() -> void:
## タイトル表示時に「黒→透明」のフェードイン
fader.snap_to_black()
fader.fade_in()
start_button.pressed.connect(_on_start_button_pressed)
func _on_start_button_pressed() -> void:
## フェードアウト → シーン切り替え → フェードイン をまとめて実行
fader.fade_out_in(
Callable(self, "_change_to_game_scene"),
0.5, # フェードアウト時間
0.5 # フェードイン時間
)
func _change_to_game_scene() -> void:
get_tree().change_scene_to_file("res://scenes/Game.tscn")
これで、「スタートボタンを押したら暗転してゲームシーンへ遷移、その後フェードイン」という流れが簡単に実現できます。
手順④: プレイヤー死亡時など、他のシーンでも再利用する
同じコンポーネントを、例えばゲームシーン側でも使い回せます。
Game (Node2D) ├── Player (CharacterBody2D) ├── UI (CanvasLayer) └── ScreenFader (ScreenFader)
ゲームシーンのスクリプト例:
extends Node2D
@onready var player := %Player
@onready var fader: ScreenFader = %ScreenFader
func _ready() -> void:
## ステージ開始時もフェードインしたい場合
fader.snap_to_black()
fader.fade_in(0.8)
func on_player_dead() -> void:
## プレイヤー死亡時に暗転してタイトルへ戻る
fader.fade_out_in(
Callable(self, "_back_to_title"),
0.8,
0.8
)
func _back_to_title() -> void:
get_tree().change_scene_to_file("res://scenes/TitleScreen.tscn")
ポイントは、どのシーンでも「ScreenFaderコンポーネントを1つ追加するだけ」で同じAPIが使えることです。
継承ベースで「TitleBaseScene」「GameBaseScene」みたいな共通親を作らなくても、フェードだけをコンポーネントとして合成できます。
メリットと応用
ScreenFader をコンポーネントとして切り出すことで、以下のようなメリットがあります。
- シーン構造がシンプル
「フェード専用シーン」をわざわざ作らず、必要なシーンにだけコンポーネントを生やせます。 - 再利用性が高い
タイトル、ゲーム本編、リザルト画面など、全部同じScreenFaderを使い回せます。 - 設定の一元管理
デフォルトのフェード時間や色を、このコンポーネントのエクスポート変数だけで調整できます。 - テストがしやすい
単体のシーンにScreenFaderだけを置いて、フェード挙動を確認することも簡単です。
さらに一歩進めると、AutoLoad(シングルトン) にして「ゲーム全体で1つだけのフェーダー」を運用することもできます。
その場合は、ScreenFader をシーンに直接置くのではなく、AutoLoadとして登録し、各シーンから ScreenFader.fade_out_in(...) のように呼び出す形になります。
改造案:フェード中に任意のテキストを表示する
例えば、「Now Loading…」のようなテキストをフェード中だけ表示したい場合は、Label を追加して制御するのが手っ取り早いです。
以下は、簡易的に「メッセージを表示する」機能を足す改造案です。
## ScreenFader に追記する例
var _message_label: Label
func set_message(text: String) -> void:
if not _message_label:
_message_label = Label.new()
_message_label.name = "ScreenFaderMessage"
_message_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_message_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_message_label.anchor_left = 0.0
_message_label.anchor_top = 0.0
_message_label.anchor_right = 1.0
_message_label.anchor_bottom = 1.0
_message_label.offset_left = 0.0
_message_label.offset_top = 0.0
_message_label.offset_right = 0.0
_message_label.offset_bottom = 0.0
_message_label.add_theme_color_override("font_color", Color.WHITE)
_rect.add_child(_message_label)
_message_label.text = text
_message_label.visible = text != ""
これを使って、シーン遷移時に
fader.set_message("Now Loading...")
fader.fade_out_in(Callable(self, "_change_scene"))
のようにすれば、フェードの責務を保ちつつ、ちょっとしたローディング演出も同じコンポーネントで扱えます。
継承で「ロード付きフェードシーン」「タイトル専用フェードシーン」…と増やしていくより、
こうやって 小さなコンポーネントを合成していく方が、長期的にははるかに楽 ですね。
ぜひ自分のプロジェクト流の ScreenFader に育ててみてください。
