【Cocos Creator】アタッチするだけ!BGMTransition (クロスフェード)の実装方法【TypeScript】

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時点)

【Cocos Creator 3.8】BGMTransition(クロスフェード)の実装:アタッチするだけでエリア移動時のBGMを滑らかに切り替える汎用スクリプト

本記事では、ノードにアタッチするだけで「BGMのクロスフェード切り替え」ができる汎用コンポーネントBGMTransition」を実装します。
エリア移動やシーン遷移のたびにBGMをフェードアウト → フェードインするコードを書くのは面倒ですが、このコンポーネントを使えば、InspectorでBGMとフェード時間を設定して、トリガーを呼ぶだけで滑らかなBGM切り替えを実現できます。


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

前提とゴール

  • 使用エンジン: Cocos Creator 3.8.7
  • 言語: TypeScript
  • 依存なし: 他の GameManager やシングルトンに一切依存しない。
  • 汎用設計: 任意のノードにアタッチして使える。
  • AudioSource を親ノードから取得し、その音量を操作してクロスフェードを実現する。

このコンポーネントは、以下のような使い方を想定しています。

  • シーン内に AudioSource を持つ BGM ノードを 1 つ用意しておく。
  • その BGM ノードの子(または同じノード)に BGMTransition をアタッチする。
  • エリア移動などのタイミングで triggerTransition() を呼び出して、指定した BGM にクロスフェードする。

「親のBGMプレイヤーの音量を操作する」という仕様に合わせ、親ノードまたは自ノードの AudioSource を自動取得し、見つからなければエラーログを出す防御的実装にします。

機能要件の整理

  • 現在再生中の BGM から、新しい BGM へクロスフェードする。
  • フェードアウトとフェードインは、同じ時間で線形に行う(シンプルな実装)。
  • 同一の AudioSource を使い、「音量フェード」と「クリップ切り替え」で実現する。
  • クロスフェード中に再度トリガーされた場合は、前のフェード処理を上書きし、新しい指定に従う。
  • Inspector から、以下を調整できるようにする:
    • ターゲット BGM(AudioClip
    • フェード時間
    • 最終的な目標ボリューム
    • 自動再生の有無
    • 遷移開始時にすぐにトリガーするかどうか

インスペクタで設定可能なプロパティ設計

BGMTransition に定義する @property 一覧と役割です。

  • @property(AudioClip) targetClip
    • クロスフェード先の BGM 音源。
    • Inspector で任意の AudioClip を指定する。
  • @property({ type: Node }) audioSourceNode
    • 音量を操作する対象の AudioSource を持つノード。
    • 未指定の場合:
      • まず自ノードから AudioSource を探索。
      • なければ親ノードから探索。
  • @property({ tooltip: 'フェード時間(秒)。0 なら即時切り替え。' }) fadeDuration
    • BGM をクロスフェードする時間(秒)。
    • 例: 1.0 で 1 秒かけてフェードアウト&イン。
  • @property({ tooltip: 'フェード完了時の最終ボリューム(0〜1)。' }) targetVolume
    • クロスフェード完了後の AudioSource.volume
    • 通常は 1.0 で最大音量。
  • @property({ tooltip: '開始時に現在のボリュームを記憶し、フェードアウト時の起点とするか。' }) useCurrentVolumeAsStart
    • true: トリガー時点の AudioSource.volume からフェードアウトを開始。
    • false: 常に targetVolume からフェードアウトを開始。
  • @property({ tooltip: 'コンポーネント有効化時に自動でクロスフェードを開始するか。' }) autoPlayOnStart
    • true: start() で自動的に triggerTransition() を呼ぶ。
    • false: スクリプトやイベントから明示的に呼び出す。
  • @property({ tooltip: 'クロスフェード中に別のトリガーが来た場合、前のフェードを中断して上書きするか。' }) interruptPreviousTransition
    • true: 新しいトリガーで前のフェード処理を中断し、最新の設定でやり直す。
    • false: フェード中のトリガーを無視する。

また、内部状態として以下を持ちます(Inspector には出さない)。

  • 取得した AudioSource の参照
  • フェード経過時間
  • フェード中かどうかのフラグ
  • 開始時の音量 / 終了時の音量(フェード計算用)
  • フェード中にクリップ切り替えを行ったかどうかのフラグ

TypeScriptコードの実装

以下が完成した BGMTransition.ts の全コードです。


import { _decorator, Component, Node, AudioSource, AudioClip, warn, error } from 'cc';
const { ccclass, property } = _decorator;

/**
 * BGMTransition
 * 
 * 親(または指定ノード)の AudioSource の音量を操作し、
 * 指定した AudioClip へクロスフェードする汎用コンポーネント。
 * 
 * - 他のスクリプトへの依存なし
 * - Inspector から BGM・フェード時間・最終音量などを設定可能
 * - triggerTransition() を呼ぶだけでクロスフェード開始
 */
@ccclass('BGMTransition')
export class BGMTransition extends Component {

    @property({
        type: AudioClip,
        tooltip: 'クロスフェード先の BGM AudioClip。'
    })
    public targetClip: AudioClip | null = null;

    @property({
        type: Node,
        tooltip: '音量を操作する AudioSource を持つノード。\n未指定の場合は自ノード→親ノードの順で自動探索します。'
    })
    public audioSourceNode: Node | null = null;

    @property({
        tooltip: 'フェード時間(秒)。0 の場合は即時切り替えになります。'
    })
    public fadeDuration: number = 1.0;

    @property({
        tooltip: 'フェード完了時の最終ボリューム(0〜1)。通常は 1.0。'
    })
    public targetVolume: number = 1.0;

    @property({
        tooltip: 'トリガー時の現在ボリュームをフェードアウト開始値として利用するか。'
    })
    public useCurrentVolumeAsStart: boolean = true;

    @property({
        tooltip: 'コンポーネント有効化時(start)に自動でクロスフェードを開始するか。'
    })
    public autoPlayOnStart: boolean = false;

    @property({
        tooltip: 'クロスフェード中に再度トリガーされた場合、前のフェードを中断して上書きするか。'
    })
    public interruptPreviousTransition: boolean = true;

    // --- 内部状態 ---

    /** 操作対象の AudioSource 実体 */
    private _audioSource: AudioSource | null = null;

    /** 現在クロスフェード処理中かどうか */
    private _isTransitioning: boolean = false;

    /** フェード経過時間(秒) */
    private _elapsed: number = 0;

    /** フェードアウト開始時の音量 */
    private _startVolume: number = 1.0;

    /** フェードイン完了時の音量(= targetVolume) */
    private _endVolume: number = 1.0;

    /** フェード処理中にターゲットクリップへ切り替え済みかどうか */
    private _clipSwitched: boolean = false;

    onLoad() {
        // AudioSource の自動取得ロジック
        if (this.audioSourceNode == null) {
            // まず自ノードで探す
            this._audioSource = this.node.getComponent(AudioSource);
            if (!this._audioSource && this.node.parent) {
                // 見つからなければ親ノードで探す
                this._audioSource = this.node.parent.getComponent(AudioSource);
                if (this._audioSource) {
                    this.audioSourceNode = this.node.parent;
                    warn('[BGMTransition] audioSourceNode が未設定のため、親ノードから AudioSource を自動取得しました。');
                }
            } else if (this._audioSource) {
                this.audioSourceNode = this.node;
                warn('[BGMTransition] audioSourceNode が未設定のため、自ノードから AudioSource を自動取得しました。');
            }
        } else {
            this._audioSource = this.audioSourceNode.getComponent(AudioSource);
        }

        if (!this._audioSource) {
            error('[BGMTransition] AudioSource が見つかりません。audioSourceNode に AudioSource を持つノードを設定してください。');
        }
    }

    start() {
        if (this.autoPlayOnStart) {
            this.triggerTransition();
        }
    }

    /**
     * 外部から呼び出してクロスフェードを開始するメソッド。
     * 例: ボタンの onClick や、他スクリプトから this.getComponent(BGMTransition).triggerTransition() など。
     */
    public triggerTransition(): void {
        if (!this._audioSource) {
            error('[BGMTransition] triggerTransition が呼ばれましたが、AudioSource が存在しません。');
            return;
        }

        if (!this.targetClip) {
            error('[BGMTransition] targetClip が設定されていません。Inspector から AudioClip を指定してください。');
            return;
        }

        if (this._isTransitioning && !this.interruptPreviousTransition) {
            // フェード中は新しいトリガーを無視
            warn('[BGMTransition] すでにクロスフェード中のため、新しいトリガーを無視しました。interruptPreviousTransition を true にすると上書きできます。');
            return;
        }

        // フェードパラメータを初期化
        this._elapsed = 0;
        this._clipSwitched = false;
        this._endVolume = this.targetVolume;

        if (this.useCurrentVolumeAsStart) {
            this._startVolume = this._audioSource.volume;
        } else {
            this._startVolume = this.targetVolume;
            this._audioSource.volume = this._startVolume;
        }

        if (this.fadeDuration <= 0) {
            // フェード時間 0 の場合は即時切り替え
            this._audioSource.clip = this.targetClip;
            if (!this._audioSource.playing) {
                this._audioSource.play();
            }
            this._audioSource.volume = this.targetVolume;
            this._isTransitioning = false;
            return;
        }

        // クロスフェード開始
        this._isTransitioning = true;
    }

    update(deltaTime: number) {
        if (!this._isTransitioning || !this._audioSource || !this.targetClip) {
            return;
        }

        this._elapsed += deltaTime;
        const halfDuration = this.fadeDuration * 0.5;

        if (this._elapsed <= halfDuration) {
            // フェードアウトフェーズ: startVolume → 0
            const t = this._elapsed / halfDuration;
            const newVolume = this._lerp(this._startVolume, 0, this._clamp01(t));
            this._audioSource.volume = newVolume;
        } else {
            // クリップをまだ切り替えていなければ、ここで切り替え + 再生開始
            if (!this._clipSwitched) {
                this._audioSource.clip = this.targetClip;
                this._audioSource.play();
                this._clipSwitched = true;
            }

            // フェードインフェーズ: 0 → endVolume
            const t = (this._elapsed - halfDuration) / halfDuration;
            const newVolume = this._lerp(0, this._endVolume, this._clamp01(t));
            this._audioSource.volume = newVolume;
        }

        // 全体のフェード時間を超えたら完了
        if (this._elapsed >= this.fadeDuration) {
            this._audioSource.volume = this._endVolume;
            this._isTransitioning = false;
        }
    }

    // 線形補間
    private _lerp(a: number, b: number, t: number): number {
        return a + (b - a) * t;
    }

    // 0〜1 にクランプ
    private _clamp01(v: number): number {
        if (v < 0) return 0;
        if (v > 1) return 1;
        return v;
    }
}

コードの要点解説

onLoad: AudioSource の防御的取得

  • audioSourceNode が未設定の場合:
    • まず this.node.getComponent(AudioSource) で自ノードを探索。
    • 見つからなければ this.node.parent.getComponent(AudioSource) で親ノードを探索。
    • 見つけた場合は audioSourceNode に自動設定し、warn ログで通知。
  • 最終的に _audioSource が取得できなければ error を出力し、以降の処理で早期リターンするようにしています。

start: 自動再生オプション

  • autoPlayOnStarttrue の場合のみ、start()triggerTransition() を自動呼び出し。
  • タイトルからゲーム本編へ、など「シーン開始時に BGM を切り替えたい」ケースを簡単に設定できます。

triggerTransition: 外部からのトリガーポイント

  • AudioSource と targetClip が存在するかチェックし、不足していれば error で即リターン。
  • すでにフェード中で interruptPreviousTransition == false の場合は、新しいトリガーを warn とともに無視。
  • useCurrentVolumeAsStart に応じて、フェードアウト開始音量を決定。
  • fadeDuration <= 0 の場合は、音量フェードを行わずに即座に:
    • clip の切り替え
    • play() の呼び出し
    • volume の設定
  • 通常ケースでは、内部状態を初期化して _isTransitioning = true とし、update() にフェード処理を任せます。

update: フェードアウト + クリップ切り替え + フェードイン

  • 毎フレーム _elapseddeltaTime を加算。
  • フェード時間を半分に分割し:
    • 前半: startVolume → 0 へ線形フェードアウト。
    • 後半: まだなら cliptargetClip に切り替え、play() を呼んでから 0 → targetVolume へ線形フェードイン。
  • _elapsed >= fadeDuration になったらフェード完了として:
    • 最終音量を targetVolume に固定。
    • _isTransitioning = false にして処理終了。

使用手順と動作確認

ここからは、Cocos Creator 3.8.7 エディタ上での具体的な設定手順と、動作確認の方法を説明します。

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

  1. Assets パネルで任意のフォルダ(例: assets/scripts)を右クリック。
  2. Create → TypeScript を選択。
  3. ファイル名を BGMTransition.ts に変更します。
  4. ダブルクリックして開き、先ほど掲載した TypeScript コード全文 を貼り付けて保存します。

2. BGM 用ノードと AudioSource の準備

  1. Hierarchy パネルで右クリック → Create → Empty Node を選択し、名前を BGMPlayer に変更します。
  2. BGMPlayer ノードを選択し、Inspector の Add Component ボタンをクリック。
  3. Audio → AudioSource を選択し、AudioSource コンポーネントを追加します。
  4. AudioSource のプロパティで:
    • Loop にチェック(BGM をループ再生したい場合)。
    • Volume1 に設定。
    • Play On Awakeオフ を推奨(このコンポーネントから再生を制御するため)。
  5. Assets に BGM 用の AudioClip(例: BGM_Field, BGM_Dungeon など)をインポートしておきます。

3. BGMTransition コンポーネントのアタッチ

今回は BGMPlayer 自身にアタッチする例とします。

  1. Hierarchy で BGMPlayer ノードを選択。
  2. Inspector で Add Component → Custom → BGMTransition を選択して追加します。
  3. 追加された BGMTransition コンポーネントのプロパティを設定します:
    • Target Clip:
      • クロスフェード先にしたい BGM(例: BGM_Field)をドラッグ&ドロップ。
    • Audio Source Node:
      • 空欄のままでも、自ノードに AudioSource があるため自動取得されます。
      • 明示したい場合は BGMPlayer ノードをドラッグ&ドロップ。
    • Fade Duration:
      • 例: 1.5(1.5 秒かけてクロスフェード)。
    • Target Volume:
      • 通常は 1.0。BGM を少し控えめにしたい場合は 0.5 などに調整。
    • Use Current Volume As Start:
      • オンのままで OK。(トリガー時の音量から自然にフェードします)
    • Auto Play On Start:
      • シーン開始と同時に BGM を流したい場合は チェック
      • イベントから手動で切り替える場合は オフ
    • Interrupt Previous Transition:
      • 通常は チェック 推奨(連続でエリア移動が起きたときに自然に追従します)。

4. 動作確認(簡単テスト)

方法 A: Auto Play On Start を使う

  1. BGMTransitionAuto Play On Start にチェックを入れる。
  2. シーンを保存して Play ボタンで再生。
  3. ゲーム開始時に、BGMPlayerAudioSource が:
    • 音量フェードアウト(初期値 → 0)
    • クリップを Target Clip に切り替え
    • フェードイン(0 → Target Volume)

    する動きになっていれば成功です。

方法 B: ボタンから triggerTransition を呼ぶ

「エリアに入ったときに BGM を切り替える」イメージで、UI ボタンから呼び出してみます。

  1. Hierarchy で Canvas を作成(なければ)。
  2. Canvas の子として Button を作成(Right Click → UI → Button)。
  3. Button ノードを選択し、Inspector の Button コンポーネント内 Click Events に新しいイベントを追加。
  4. 追加したイベントの TargetBGMPlayer ノードをドラッグ&ドロップ。
  5. Component ドロップダウンから BGMTransition を選択。
  6. Handler ドロップダウンから triggerTransition を選択。
  7. ゲームを再生し、ボタンをクリックすると、設定した Target Clip にクロスフェードしていれば成功です。

5. エリアごとに BGM を変えたい場合の例

例えば「フィールド」「ダンジョン」「ボス部屋」で BGM を切り替えたい場合:

  1. BGMPlayer ノードは 1 つだけ用意(AudioSource を持つ)。
  2. 各エリア用に空ノードを作成:
    • FieldArea
    • DungeonArea
    • BossArea
  3. それぞれのエリアノードに BGMTransition をアタッチし、以下を設定:
    • Target Clip: そのエリア用の BGM
    • Audio Source Node: 共通の BGMPlayer ノード
    • その他のフェード設定は共通でも可
  4. プレイヤーがエリアに入ったタイミングで、そのエリアノードの BGMTransition.triggerTransition() を呼び出すだけで、共通の BGMPlayer の音量を操作しつつ、滑らかに BGM が切り替わります。

このように、BGM の実体(AudioSource)は 1 箇所にまとめつつ、切り替えロジックだけを各エリアに分散できるため、シーン構成が非常にシンプルになります。


まとめ

本記事では、他のスクリプトに一切依存しない、完全独立な BGM クロスフェード用コンポーネントBGMTransition」を実装しました。

  • Inspector で:
    • クロスフェード先の BGM(AudioClip
    • フェード時間
    • 最終ボリューム
    • 開始時自動再生の有無
    • フェード中の割り込み可否

    を設定するだけで、BGM 切り替えロジックを何度も書く必要がなくなります。

  • AudioSource の取得は:
    • 明示指定(audioSourceNode
    • 未指定時の自動探索(自ノード → 親ノード)

    により、既存プロジェクトにも簡単に組み込み可能です。

  • エリアごとに BGMTransition を配置しておけば、エリア進入時に triggerTransition() を呼ぶだけで、自然な BGM 切り替えを実現できます。

このコンポーネント単体で BGM のクロスフェード制御が完結するため、ゲームの規模が大きくなっても BGM 管理コードが肥大化しにくく、再利用性も高いのがメリットです。
プロジェクトの BGM 制御の共通パーツとして、ぜひそのまま組み込んで活用してみてください。

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をコピーしました!