【Cocos Creator 3.8】VolumeFader の実装:アタッチするだけでシーン切り替え時にBGMを数秒かけて滑らかにフェードアウトする汎用スクリプト

このガイドでは、Cocos Creator 3.8.7 + TypeScript で「シーン切り替え時に BGM をいきなり止めず、指定秒数かけて音量を 0 にしてから停止する」ための汎用コンポーネント VolumeFader を実装します。

任意の BGM 用ノード(AudioSource を持つノード)にこのコンポーネントをアタッチしておくだけで、

  • シーン遷移前にフェードアウト
  • 任意のタイミングでスクリプトからフェードアウト

といった処理を、外部の GameManager やシングルトンに依存せずに実現できます。


コンポーネントの設計方針

要件整理

  • 対象は 同じノードに付いている AudioSource(標準コンポーネント)。
  • フェードアウト時間(秒)を Inspector から調整可能にする。
  • フェードアウト完了後は AudioSource.stop() し、音量を 0 に固定する。
  • シーン切り替え時(onDestroy / onDisable)にも自動でフェードアウトできるようにする。
  • 外部スクリプトへの依存禁止:
    • シーン管理用の別クラスに依存しない。
    • 全ての設定は @property で Inspector から行う。
  • 防御的実装:
    • AudioSource が見つからない場合はエラーログを出し、フェード処理は行わない。
    • フェード中に同じノードが破棄されても例外が出ないようにする。

フェードロジックの概要

  • 開始時点の音量を記録(_startVolume)。
  • 設定されたフェード時間(fadeDuration)の間、update() で経過時間を積算し、音量を線形補間する。
    • t = elapsed / fadeDuration
    • currentVolume = startVolume * (1 - t)
  • 経過時間がフェード時間を超えたら:
    • 音量を 0 に固定
    • audioSource.stop() を呼ぶ
    • フェードフラグを解除

Inspector で設定可能なプロパティ設計

  • fadeDuration (number)
    • ツールチップ: 「フェードアウトにかける時間(秒)。0 以下の場合は即時停止。」
    • 役割: 音量を 0 にするまでの時間(秒)。
    • 例: 1.5, 2.0, 3.0 など。
  • autoFadeOnDisable (boolean)
    • ツールチップ: 「ノードが無効化されたときに自動でフェードアウトするかどうか。」
    • 役割: シーン切り替え時やノードが無効化されるタイミングで自動フェードを行うか。
  • autoFadeOnDestroy (boolean)
    • ツールチップ: 「ノードが破棄されるときに自動でフェードアウトするかどうか。」
    • 役割: シーン切り替えなどでノードが破棄される時に自動フェードを行うか。
  • stopAfterFade (boolean)
    • ツールチップ: 「フェードアウト完了後に AudioSource.stop() を呼ぶかどうか。」
    • 役割: フェード後に音を完全に止めるか、音量 0 で再生状態のままにするか。
    • 通常は true 推奨。
  • initialVolume (number)
    • ツールチップ: 「再生開始時の基準音量(0〜1)。負値の場合は現在の AudioSource.volume を使用。」
    • 役割: コンポーネント開始時に音量を初期化したい場合に使用。
    • 例: 1.0(最大), 0.5(半分)。デフォルトは -1(変更しない)。

また、スクリプトから任意のタイミングでフェードアウトを開始できるように、

  • public startFadeOut(duration?: number): void
    • 引数の duration が指定されていれば、その時間でフェードアウト。
    • 指定されなければ fadeDuration を使用。

を公開メソッドとして用意します。


TypeScriptコードの実装


import { _decorator, Component, AudioSource, clamp01 } from 'cc';
const { ccclass, property } = _decorator;

/**
 * VolumeFader
 * 
 * 任意の AudioSource を持つノードにアタッチして使用します。
 * - startFadeOut() を呼ぶと、指定時間で音量を 0 にフェードアウト
 * - オプションで onDisable / onDestroy 時に自動フェードアウト
 */
@ccclass('VolumeFader')
export class VolumeFader extends Component {

    @property({
        tooltip: 'フェードアウトにかける時間(秒)。0 以下の場合は即時停止します。',
    })
    public fadeDuration: number = 2.0;

    @property({
        tooltip: 'ノードが無効化されたときに自動でフェードアウトするかどうか。',
    })
    public autoFadeOnDisable: boolean = false;

    @property({
        tooltip: 'ノードが破棄されるときに自動でフェードアウトするかどうか。',
    })
    public autoFadeOnDestroy: boolean = true;

    @property({
        tooltip: 'フェードアウト完了後に AudioSource.stop() を呼ぶかどうか。',
    })
    public stopAfterFade: boolean = true;

    @property({
        tooltip: '再生開始時の基準音量(0〜1)。負の値の場合は現在の AudioSource.volume を使用します。',
        range: [0, 1, 0.01],
    })
    public initialVolume: number = -1;

    private _audioSource: AudioSource | null = null;
    private _isFading: boolean = false;
    private _fadeElapsed: number = 0;
    private _currentFadeDuration: number = 0;
    private _startVolume: number = 1;

    onLoad() {
        // 必要なコンポーネント AudioSource を取得
        this._audioSource = this.getComponent(AudioSource);
        if (!this._audioSource) {
            console.error(
                '[VolumeFader] AudioSource コンポーネントが見つかりません。' +
                '同じノードに AudioSource を追加してください。'
            );
            return;
        }
    }

    start() {
        if (!this._audioSource) {
            return;
        }

        // initialVolume が 0〜1 の範囲なら、開始時に音量を設定
        if (this.initialVolume >= 0) {
            // clamp01 で 0〜1 に制限
            this._audioSource.volume = clamp01(this.initialVolume);
        }
    }

    update(dt: number) {
        if (!this._isFading || !this._audioSource) {
            return;
        }

        if (this._currentFadeDuration <= 0) {
            // フェード時間が 0 以下の場合は即時停止
            this._finishFadeImmediately();
            return;
        }

        this._fadeElapsed += dt;
        const t = this._fadeElapsed / this._currentFadeDuration;

        if (t >= 1) {
            // フェード完了
            this._audioSource.volume = 0;
            if (this.stopAfterFade) {
                this._audioSource.stop();
            }
            this._isFading = false;
            return;
        }

        // 線形補間で音量を下げる
        const newVolume = this._startVolume * (1 - t);
        this._audioSource.volume = Math.max(0, newVolume);
    }

    /**
     * 外部から呼び出してフェードアウトを開始する。
     * @param duration フェードアウトにかける時間(秒)。省略時は fadeDuration を使用。
     */
    public startFadeOut(duration?: number): void {
        if (!this._audioSource) {
            console.warn('[VolumeFader] AudioSource が存在しないため、フェードアウトできません。');
            return;
        }

        // すでにフェード中なら、リスタートする
        this._isFading = true;
        this._fadeElapsed = 0;

        this._currentFadeDuration = (duration !== undefined) ? duration : this.fadeDuration;
        this._startVolume = this._audioSource.volume;

        // フェード時間が 0 以下なら即時停止
        if (this._currentFadeDuration <= 0) {
            this._finishFadeImmediately();
        }
    }

    /**
     * 即時フェードアウト完了処理(フェード時間 0 以下の場合など)
     */
    private _finishFadeImmediately(): void {
        if (!this._audioSource) {
            return;
        }
        this._audioSource.volume = 0;
        if (this.stopAfterFade) {
            this._audioSource.stop();
        }
        this._isFading = false;
    }

    /**
     * ノードが無効化されたときのコールバック。
     * autoFadeOnDisable が true の場合に自動フェードアウト。
     */
    onDisable() {
        if (this.autoFadeOnDisable) {
            this.startFadeOut();
        }
    }

    /**
     * ノードが破棄されるときのコールバック。
     * autoFadeOnDestroy が true の場合に自動フェードアウト。
     * 
     * 注意: onDestroy が呼ばれるタイミングでは update が呼ばれない場合もあるため、
     *       「確実にフェード時間を取る」目的には向きません。
     *       シーン切り替え前に任意のタイミングで startFadeOut() を呼ぶ運用が推奨です。
     */
    onDestroy() {
        if (this.autoFadeOnDestroy) {
            // ここでフェードを開始しても、すぐにノードが破棄される可能性があるため、
            // 実際のシーン遷移前に明示的に startFadeOut() を呼ぶほうが安全です。
            this.startFadeOut();
        }
    }
}

コードのポイント解説

  • onLoad()
    • this.getComponent(AudioSource) で同じノードの AudioSource を取得。
    • 見つからない場合は console.error を出し、以後の処理では何もしないように防御。
  • start()
    • initialVolume が 0 以上なら、その値で AudioSource.volume を初期化。
    • 負の値のときは「何もしない」(既存設定を尊重)。
  • update(dt)
    • _isFading フラグが立っている間だけフェード処理を行う。
    • 経過時間 _fadeElapsed を積算し、t = elapsed / duration で進行度を計算。
    • 線形補間で volume を徐々に 0 に近づける。
    • フェード完了後は volume=0、必要なら stop() を呼んでフラグを下ろす。
  • startFadeOut(duration?)
    • 外部からフェードアウトを開始したいときに呼ぶメソッド。
    • 引数があればそれを優先し、なければ fadeDuration を使用。
    • すでにフェード中でも呼び出せるように、毎回フェード状態をリセット。
    • フェード時間が 0 以下なら _finishFadeImmediately() で即時停止。
  • onDisable() / onDestroy()
    • それぞれのフラグ(autoFadeOnDisable, autoFadeOnDestroy)に応じて startFadeOut() を自動で呼ぶ。
    • ただし onDestroy() のタイミングではフレーム更新が止まる場合もあるため、「確実なフェード時間」を確保したい場合は、シーン切り替え前に自前で startFadeOut() を呼ぶ運用が推奨。

使用手順と動作確認

1. スクリプトファイルの作成

  1. エディタ左下の Assets パネルで、任意のフォルダ(例: scripts/audio)を右クリックします。
  2. Create > TypeScript を選択し、ファイル名を VolumeFader.ts にします。
  3. 作成された VolumeFader.ts をダブルクリックして開き、既存コードをすべて削除して、上記の TypeScript コードを丸ごと貼り付けて保存します。

2. テスト用ノードと AudioSource の用意

  1. Hierarchy パネルで右クリックし、Create > Empty Node を選択して、ノード名を BGM など分かりやすい名前に変更します。
  2. BGM ノードを選択した状態で、Inspector の Add Component ボタンをクリックし、Audio > AudioSource を追加します。
  3. AudioSource の Clip に BGM 用のオーディオクリップ(wav/mp3 など)を設定し、Loop にチェックを入れておきます。
  4. 必要に応じて Play On Awake にチェックを入れておくと、シーン開始時に自動再生されます。

3. VolumeFader コンポーネントをアタッチ

  1. 再び BGM ノードを選択した状態で、Inspector の Add Component をクリックします。
  2. Custom カテゴリの中から VolumeFader を選択して追加します。
    • もし Custom に表示されない場合は、スクリプト保存後に一度エディタをフォーカスし直すか、Project > Reload で再読み込みしてください。

4. Inspector でプロパティを設定

  1. fadeDuration:
    • 例: 2.0 に設定すると、2 秒かけて音量が 0 になります。
  2. autoFadeOnDisable:
    • シーン切り替え時に BGM ノードを setActive(false) で無効化する運用なら、ON にします。
    • 今回はまず OFF のままでも構いません(手動テストを優先)。
  3. autoFadeOnDestroy:
    • シーン切り替え時にノードが破棄されるケースで自動フェードしたい場合は ON にします。
    • ただし前述の通り、確実なフェード時間を取りたい場合は、シーン遷移前に明示的に startFadeOut() を呼ぶ設計の方が安全です。
  4. stopAfterFade:
    • 通常は ON(デフォルトのまま)で問題ありません。
    • フェード後も「再生状態のまま volume 0」を維持したい特殊なケースでのみ OFF にします。
  5. initialVolume:
    • デフォルトの -1 のままだと、AudioSource に設定済みの volume をそのまま使います。
    • 開始時に必ず 0.5 にしたいなどの要件がある場合は 0.5 など 0〜1 の値を設定します。

5. シンプルな動作確認(エディタ上で手動テスト)

まずは「ノードを無効化した時にフェードアウトする」挙動を確認してみます。

  1. Scene を再生(▶ボタン)して、BGM が再生されていることを確認します。
  2. 再生中に Hierarchy パネルで BGM ノードを選択し、Inspector の一番上にあるチェックボックス(Enabled)を OFF に切り替えます。
  3. もし autoFadeOnDisable を ON にしていれば、このタイミングでフェードアウトが始まり、2 秒かけて音が小さくなっていくはずです。
  4. フェード時間を 0.2 などに短くすると、すぐに音が消える挙動を確認できます。

6. スクリプトから任意タイミングでフェードアウトする例

「シーン切り替えボタンを押したら BGM を 1.5 秒かけてフェードアウトしてからシーンをロードする」といった処理も、VolumeFader 単体で実装できます。

例として、ボタンにアタッチする簡易スクリプトを示します(あくまで参考用であり、VolumeFader 自体はこのスクリプトに依存しません)。


import { _decorator, Component, Node, director } from 'cc';
import { VolumeFader } from './VolumeFader';
const { ccclass, property } = _decorator;

@ccclass('SceneChangeButton')
export class SceneChangeButton extends Component {

    @property({ tooltip: 'BGM ノード(VolumeFader がアタッチされているノード)' })
    public bgmNode: Node | null = null;

    @property({ tooltip: 'フェードアウトにかける時間(秒)' })
    public fadeTime: number = 1.5;

    @property({ tooltip: 'フェード完了後に遷移するシーン名' })
    public nextSceneName: string = 'NextScene';

    async onClick() {
        if (this.bgmNode) {
            const fader = this.bgmNode.getComponent(VolumeFader);
            if (fader) {
                fader.startFadeOut(this.fadeTime);
                // フェード時間だけ待ってからシーン遷移
                await new Promise((resolve) => setTimeout(resolve, this.fadeTime * 1000));
            }
        }

        director.loadScene(this.nextSceneName);
    }
}

このように、VolumeFader は 他の管理クラスに依存せず、必要に応じて任意のスクリプトから呼び出せる「音量フェード専用コンポーネント」として扱えます。


まとめ

  • VolumeFader は、AudioSource を持つノードにアタッチするだけで、
    • 指定秒数のフェードアウト
    • 無効化/破棄時の自動フェード
    • 外部からの任意タイミングでのフェード開始

    を実現する、完全に独立した汎用コンポーネントです。

  • Inspector からフェード時間や自動フェードの有無を調整できるため、シーンごと・BGM ごとに挙動を簡単に変えられます。
  • 外部の GameManager やシングルトンに依存しない設計なので、プロジェクトのどこにでも気軽に持ち込んで再利用できます。
  • 「シーン遷移時に BGM がプツンと切れてしまう」問題は、VolumeFader をアタッチして startFadeOut() を呼ぶだけで解消できます。

このコンポーネントをベースに、フェードイン機能やクロスフェード機能を追加するなど、よりリッチなサウンド演出に発展させていくことも容易です。まずは本記事の VolumeFader を導入し、BGM の「切れ目のない」シーン遷移を体験してみてください。