【Godot 4】VolumeFader (フェード処理) コンポーネントの作り方

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

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です)

  1. ① BGMシーンに VolumeFader を追加する
    例えば、プレイヤーとは別に「BGM専用シーン」を作っておくと管理しやすいです。
    BGMController (Node)
     └── BGMPlayer (AudioStreamPlayer)
          └── VolumeFader (Node)
        
    • BGMPlayer に BGMのAudioStreamを設定
    • VolumeFader ノードに、先ほどのスクリプトをアタッチ
    • インスペクタで default_fade_out_timedefault_fade_in_time をお好みに調整

    この構成なら、BGMのロジック(再生・フェード)はすべて VolumeFader 側に閉じ込められるので、
    メインシーン側は「再生開始」「フェードアウト指示」だけを投げればOKになります。

  2. ② シーン開始時にフェードインさせる
    「ステージ開始時にBGMをふわっと入れたい」場合は、auto_start_fade_in を使うと楽です。
    • VolumeFader.auto_start_fade_in = true
    • VolumeFader.autostart_if_not_playing = true(デフォルト)

    こうすると、シーンがロードされたときに BGMPlayer が自動で play() され、
    default_fade_in_time 秒かけて target_volume までフェードインします。

  3. ③ シーン切り替え時にフェードアウトを呼ぶ
    シーン遷移を管理しているスクリプト(例えば 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にフェードアウトをお願いする」だけ。
    これがコンポーネント指向の気持ちよさですね。

  4. ④ プレイヤーや敵にも流用する
    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 自体を少し拡張していくだけで、
「フェード」と「マスターボリューム」を両立した柔軟なオーディオ制御コンポーネントに育てていけます。
ぜひプロジェクトの「標準コンポーネント」として育てていきましょう。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!