Godotでシーン切り替えをしているとき、BGMが「プツッ」といきなり止まってしまって、ちょっと興ざめ…という経験はありませんか?
一番シンプルな実装は AudioStreamPlayer.stop() を直接呼ぶことですが、これだと音が即時に切れてしまいますし、
シーンごとに「フェード用のタイマー」「Tween」「専用スクリプト」を毎回書くのも面倒ですよね。
さらに、シーンをまたいでBGMノードを持ち回ったり、autoload でBGMマネージャーを作ったりすると、
「どのシーンで止めるんだっけ?」「フェード中にシーンが変わったら?」みたいな状態管理も複雑になりがちです。
そこで今回は、どの AudioStreamPlayer にもポン付けできて、
「フェードアウトさせてから止める」という処理をコンポーネント化した VolumeFader を用意しました。
BGMだけでなく、環境音や一時的なSEループにも使い回せるようにしてあります。
【Godot 4】BGMをプツ切り禁止!なめらかフェードアウト「VolumeFader」コンポーネント
フルコード(GDScript / Godot 4)
extends Node
class_name VolumeFader
## 任意の AudioStreamPlayer / AudioStreamPlayer2D / AudioStreamPlayer3D を
## なめらかにフェードイン・フェードアウトさせるためのコンポーネント。
##
## 想定使用例:
## - シーン切り替え時にBGMを数秒かけてフェードアウトしてから停止
## - ステージ開始時にBGMをフェードイン
## - 環境音(雨・風)を状況に応じて音量を滑らかに変化させる
@export_range(0.0, 10.0, 0.1)
var default_fade_out_time: float = 2.0:
## デフォルトのフェードアウト時間(秒)
## 引数で時間を指定しなかった場合に使われます。
set(value):
default_fade_out_time = max(value, 0.0)
@export_range(0.0, 10.0, 0.1)
var default_fade_in_time: float = 1.5:
## デフォルトのフェードイン時間(秒)
set(value):
default_fade_in_time = max(value, 0.0)
@export_range(0.0, 1.0, 0.01)
var target_volume: float = 1.0:
## フェードイン完了時の音量 (0.0〜1.0)
## 通常は1.0(100%)でOK。BGMを常に少し小さめにしたい場合は0.6〜0.8などにしておく。
set(value):
target_volume = clamp(value, 0.0, 1.0)
@export
var auto_start_fade_in: bool = false:
## true の場合、ready時に自動でフェードインを開始します。
## BGMの「最初からフェードインしたい」シーンで便利です。
set(value):
auto_start_fade_in = value
@export
var autostart_if_not_playing: bool = true:
## auto_start_fade_in が true のとき、
## AudioStreamPlayer が再生中でなければ自動的に再生を開始してからフェードインします。
set(value):
autostart_if_not_playing = value
@export
var auto_stop_on_fade_out_end: bool = true:
## フェードアウト完了時に AudioStreamPlayer.stop() を呼ぶかどうか。
set(value):
auto_stop_on_fade_out_end = value
## 対象となる AudioStreamPlayer ノード。
## 未指定の場合は、親ノード(または自身)から自動で探します。
var audio_player: AudioStreamPlayer = null
## 内部状態管理
var _tween: Tween = null
var _initial_volume: float = 1.0
var _is_fading: bool = false
func _ready() -> void:
# 対象AudioStreamPlayerを自動検出
# 1. すでに外部からセットされていればそれを使う
# 2. なければ親ノードに AudioStreamPlayer がいないか探す
# 3. それでもなければ、自分自身が AudioStreamPlayer の場合はそれを使う
if audio_player == null:
if owner is AudioStreamPlayer:
audio_player = owner
elif get_parent() is AudioStreamPlayer:
audio_player = get_parent()
else:
# シーン内から一番近い AudioStreamPlayer を探す(任意)
audio_player = _find_audio_player_in_tree()
if audio_player == null:
push_warning("VolumeFader: AudioStreamPlayer が見つかりませんでした。このコンポーネントを AudioStreamPlayer の子、または同じノードに付けてください。")
return
_initial_volume = audio_player.volume_db_to_linear(audio_player.volume_db)
if auto_start_fade_in:
# 自動フェードイン
if autostart_if_not_playing and not audio_player.playing:
audio_player.play()
if audio_player.playing:
fade_in(default_fade_in_time)
func _exit_tree() -> void:
# ノード削除時にTweenをクリーンアップ
if _tween and _tween.is_valid():
_tween.kill()
func _find_audio_player_in_tree() -> AudioStreamPlayer:
# ownerの子孫から最初に見つかった AudioStreamPlayer を返すヘルパー
if owner:
for node in owner.get_children():
if node is AudioStreamPlayer:
return node
return null
# --- 公開API -------------------------------------------------------------
func fade_out(duration: float = -1.0) -> void:
## BGMをフェードアウトさせる。
## durationを省略すると default_fade_out_time が使われます。
if audio_player == null:
push_warning("VolumeFader.fade_out(): AudioStreamPlayer が設定されていません。")
return
if duration < 0.0:
duration = default_fade_out_time
# すでにフェード中なら一度止める
_stop_tween()
_is_fading = true
_initial_volume = audio_player.volume_db_to_linear(audio_player.volume_db)
_tween = create_tween()
_tween.set_trans(Tween.TRANS_SINE)
_tween.set_ease(Tween.EASE_OUT)
# Audioのvolume_dbはdBなので、線形ボリュームで補間してからdBに変換する
_tween.tween_method(
Callable(self, "_set_volume_linear"),
_initial_volume,
0.0,
duration
)
if auto_stop_on_fade_out_end:
_tween.tween_callback(Callable(self, "_on_fade_out_finished"))
func fade_in(duration: float = -1.0) -> void:
## BGMをフェードインさせる。
## durationを省略すると default_fade_in_time が使われます。
if audio_player == null:
push_warning("VolumeFader.fade_in(): AudioStreamPlayer が設定されていません。")
return
if duration < 0.0:
duration = default_fade_in_time
# すでにフェード中なら一度止める
_stop_tween()
_is_fading = true
# 現在の音量を取得し、そこから target_volume までフェード
var current_volume_linear := audio_player.volume_db_to_linear(audio_player.volume_db)
_tween = create_tween()
_tween.set_trans(Tween.TRANS_SINE)
_tween.set_ease(Tween.EASE_IN)
_tween.tween_method(
Callable(self, "_set_volume_linear"),
current_volume_linear,
target_volume,
duration
).tween_callback(Callable(self, "_on_fade_in_finished"))
func stop_immediately() -> void:
## フェードを使わず、即座に停止したいとき用のヘルパー。
_stop_tween()
if audio_player:
audio_player.stop()
func is_fading() -> bool:
## 現在フェード中かどうかを返します。
return _is_fading
# --- 内部処理 ------------------------------------------------------------
func _set_volume_linear(value: float) -> void:
# 0.0〜1.0 の線形ボリュームを dB に変換して AudioStreamPlayer に適用
value = clamp(value, 0.0, 1.0)
if not audio_player:
return
if value <= 0.0:
audio_player.volume_db = -80.0 # 実質無音
else:
audio_player.volume_db = linear_to_db(value)
func _on_fade_out_finished() -> void:
_is_fading = false
if audio_player and auto_stop_on_fade_out_end:
audio_player.stop()
func _on_fade_in_finished() -> void:
_is_fading = false
func _stop_tween() -> void:
if _tween and _tween.is_valid():
_tween.kill()
_tween = null
_is_fading = false
使い方の手順
ここでは 2DゲームのBGM用 AudioStreamPlayer にアタッチする例で説明します。
(3Dや AudioStreamPlayer2D/3D でも同じ考え方でOKです)
-
① BGMシーンに VolumeFader を追加する
例えば、プレイヤーとは別に「BGM専用シーン」を作っておくと管理しやすいです。BGMController (Node) └── BGMPlayer (AudioStreamPlayer) └── VolumeFader (Node)BGMPlayerに BGMのAudioStreamを設定VolumeFaderノードに、先ほどのスクリプトをアタッチ- インスペクタで
default_fade_out_timeやdefault_fade_in_timeをお好みに調整
この構成なら、BGMのロジック(再生・フェード)はすべて VolumeFader 側に閉じ込められるので、
メインシーン側は「再生開始」「フェードアウト指示」だけを投げればOKになります。 -
② シーン開始時にフェードインさせる
「ステージ開始時にBGMをふわっと入れたい」場合は、auto_start_fade_inを使うと楽です。VolumeFader.auto_start_fade_in = trueVolumeFader.autostart_if_not_playing = true(デフォルト)
こうすると、シーンがロードされたときに BGMPlayer が自動で
play()され、
default_fade_in_time秒かけてtarget_volumeまでフェードインします。 -
③ シーン切り替え時にフェードアウトを呼ぶ
シーン遷移を管理しているスクリプト(例えば GameManager など)から、
VolumeFader に向けてfade_out()を呼び出しましょう。# 例: GameManager.gd extends Node @onready var bgm_fader: VolumeFader = $"../BGMController/BGMPlayer/VolumeFader" func goto_next_scene() -> void: # まずBGMをフェードアウト bgm_fader.fade_out(2.0) # 2秒かけてフェードアウト(省略するとデフォルト値) # 例えば、フェードアウト完了を待たずにすぐシーンを切り替えても、 # BGMは同じシーン内にあればフェードし続けます。 # BGMをAutoloadシーンにしておくのもアリですね。 get_tree().change_scene_to_file("res://scenes/NextStage.tscn")ポイントは、フェードロジックをBGM側に閉じ込めておくことです。
GameManager は「BGMを止める」ではなく「BGMにフェードアウトをお願いする」だけ。
これがコンポーネント指向の気持ちよさですね。 -
④ プレイヤーや敵にも流用する
BGM専用で終わらせるのはもったいないので、例えば「ボスの咆哮ループSE」にも使えます。Boss (Node2D) ├── Sprite2D ├── RoarLoop (AudioStreamPlayer2D) │ └── VolumeFader (Node) └── BossAI (Script)# BossAI.gd(ボスが登場するときに咆哮SEをフェードイン、退場時にフェードアウト) extends Node2D @onready var roar_player: AudioStreamPlayer2D = $RoarLoop @onready var roar_fader: VolumeFader = $RoarLoop/VolumeFader func start_battle() -> void: roar_player.play() roar_fader.fade_in(1.0) func end_battle() -> void: roar_fader.fade_out(1.5)このように、「音のフェード」という関心事だけを VolumeFader に任せることで、
ボスAI のコードは「いつ鳴らすか」だけに集中できます。
メリットと応用
VolumeFader をコンポーネントとして切り出すメリットはかなり多いです。
- シーン構造がスッキリ
各シーンで「Tweenノード」「Timerノード」「フェード専用スクリプト」を用意する必要がなくなり、
AudioStreamPlayer + VolumeFaderのセットさえあればどこでも同じ操作で扱えます。 - 再利用性が高い
BGM、環境音、ループSE、UIのBGMなど、
「音量を滑らかに変えたい」という場面すべてで同じコンポーネントを再利用できます。 - 継承地獄を回避できる
「BGMPlayerBase」を継承した「StageBGMPlayer」「MenuBGMPlayer」…と増やしていく代わりに、
AudioStreamPlayerは素のままにして、VolumeFaderをアタッチするだけで済みます。
まさに「継承より合成(Composition over Inheritance)」ですね。 - シーン切り替えの責務分離
シーン管理側は「BGMをどう止めるか」を知らなくてよくなり、
「BGMにフェードアウトを依頼する」という1行のAPI呼び出しだけで完結します。
応用として、「ゲーム全体のマスターボリューム」や「オプション画面のボリュームスライダー」と組み合わせるのもアリです。
例えば、VolumeFader に「ゲーム全体の音量係数」を渡して、徐々にミュートする機能を足すこともできます。
改造案:マスターボリュームに追従してフェードする
例えば、ゲーム全体のマスターボリューム(0.0〜1.0)を AudioSettings というシングルトンで管理しているとします。
その値を掛け合わせて最終的な音量にしたい場合、以下のような関数を追加できます。
# VolumeFader.gd 内の改造案の一例
@export_range(0.0, 1.0, 0.01)
var master_volume: float = 1.0
func set_master_volume(value: float) -> void:
## 外部(オプション画面など)からマスターボリュームを更新するための関数。
master_volume = clamp(value, 0.0, 1.0)
# 現在のフェード位置を維持したまま、マスターボリュームだけ再反映する
var current_linear := audio_player.volume_db_to_linear(audio_player.volume_db)
if target_volume > 0.0:
var fade_ratio := current_linear / target_volume
var new_linear := target_volume * master_volume * fade_ratio
_set_volume_linear(new_linear)
このように、VolumeFader 自体を少し拡張していくだけで、
「フェード」と「マスターボリューム」を両立した柔軟なオーディオ制御コンポーネントに育てていけます。
ぜひプロジェクトの「標準コンポーネント」として育てていきましょう。




