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 をシーンに組み込む手順を見ていきましょう。

手順①: スクリプトを作成してリソース化

  1. 上記コードを res://components/screen_fader.gd などに保存します。
  2. class_name ScreenFader を定義しているので、エディタから直接「ScreenFader」をノードとして追加できます。

手順②: 任意のシーンにコンポーネントとして追加

例として、「タイトル画面」から「ゲーム本編」へフェード付きで遷移するケースを考えます。

TitleScreen (Control)
 ├── VBoxContainer
 │    ├── Label
 │    └── StartButton (Button)
 └── ScreenFader (ScreenFader)  <-- このコンポーネントを追加
  • TitleScreenシーンを開き、「+」ボタンから ScreenFader ノードを追加します。
  • インスペクタで default_durationfade_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 に育ててみてください。