【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) - フェード時間
- 最終的な目標ボリューム
- 自動再生の有無
- 遷移開始時にすぐにトリガーするかどうか
- ターゲット BGM(
インスペクタで設定可能なプロパティ設計
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: '開始時に現在のボリュームを記憶し、フェードアウト時の起点とするか。' }) useCurrentVolumeAsStarttrue: トリガー時点のAudioSource.volumeからフェードアウトを開始。false: 常にtargetVolumeからフェードアウトを開始。
@property({ tooltip: 'コンポーネント有効化時に自動でクロスフェードを開始するか。' }) autoPlayOnStarttrue:start()で自動的にtriggerTransition()を呼ぶ。false: スクリプトやイベントから明示的に呼び出す。
@property({ tooltip: 'クロスフェード中に別のトリガーが来た場合、前のフェードを中断して上書きするか。' }) interruptPreviousTransitiontrue: 新しいトリガーで前のフェード処理を中断し、最新の設定でやり直す。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: 自動再生オプション
autoPlayOnStartがtrueの場合のみ、start()でtriggerTransition()を自動呼び出し。- タイトルからゲーム本編へ、など「シーン開始時に BGM を切り替えたい」ケースを簡単に設定できます。
triggerTransition: 外部からのトリガーポイント
- AudioSource と
targetClipが存在するかチェックし、不足していればerrorで即リターン。 - すでにフェード中で
interruptPreviousTransition == falseの場合は、新しいトリガーをwarnとともに無視。 useCurrentVolumeAsStartに応じて、フェードアウト開始音量を決定。fadeDuration <= 0の場合は、音量フェードを行わずに即座に:clipの切り替えplay()の呼び出しvolumeの設定
- 通常ケースでは、内部状態を初期化して
_isTransitioning = trueとし、update()にフェード処理を任せます。
update: フェードアウト + クリップ切り替え + フェードイン
- 毎フレーム
_elapsedにdeltaTimeを加算。 - フェード時間を半分に分割し:
- 前半:
startVolume → 0へ線形フェードアウト。 - 後半: まだなら
clipをtargetClipに切り替え、play()を呼んでから0 → targetVolumeへ線形フェードイン。
- 前半:
_elapsed >= fadeDurationになったらフェード完了として:- 最終音量を
targetVolumeに固定。 _isTransitioning = falseにして処理終了。
- 最終音量を
使用手順と動作確認
ここからは、Cocos Creator 3.8.7 エディタ上での具体的な設定手順と、動作確認の方法を説明します。
1. スクリプトファイルの作成
- Assets パネルで任意のフォルダ(例:
assets/scripts)を右クリック。 Create → TypeScriptを選択。- ファイル名を
BGMTransition.tsに変更します。 - ダブルクリックして開き、先ほど掲載した TypeScript コード全文 を貼り付けて保存します。
2. BGM 用ノードと AudioSource の準備
- Hierarchy パネルで右クリック →
Create → Empty Nodeを選択し、名前をBGMPlayerに変更します。 BGMPlayerノードを選択し、Inspector のAdd Componentボタンをクリック。Audio → AudioSourceを選択し、AudioSourceコンポーネントを追加します。AudioSourceのプロパティで:Loopにチェック(BGM をループ再生したい場合)。Volumeを1に設定。Play On Awakeは オフ を推奨(このコンポーネントから再生を制御するため)。
- Assets に BGM 用の
AudioClip(例:BGM_Field,BGM_Dungeonなど)をインポートしておきます。
3. BGMTransition コンポーネントのアタッチ
今回は BGMPlayer 自身にアタッチする例とします。
- Hierarchy で
BGMPlayerノードを選択。 - Inspector で
Add Component → Custom → BGMTransitionを選択して追加します。 - 追加された
BGMTransitionコンポーネントのプロパティを設定します:- Target Clip:
- クロスフェード先にしたい BGM(例:
BGM_Field)をドラッグ&ドロップ。
- クロスフェード先にしたい BGM(例:
- 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 を流したい場合は
チェック。 - イベントから手動で切り替える場合は
オフ。
- シーン開始と同時に BGM を流したい場合は
- Interrupt Previous Transition:
- 通常は
チェック推奨(連続でエリア移動が起きたときに自然に追従します)。
- 通常は
- Target Clip:
4. 動作確認(簡単テスト)
方法 A: Auto Play On Start を使う
BGMTransitionの Auto Play On Start にチェックを入れる。- シーンを保存して
Playボタンで再生。 - ゲーム開始時に、
BGMPlayerのAudioSourceが:- 音量フェードアウト(初期値 → 0)
- クリップを
Target Clipに切り替え - フェードイン(0 → Target Volume)
する動きになっていれば成功です。
方法 B: ボタンから triggerTransition を呼ぶ
「エリアに入ったときに BGM を切り替える」イメージで、UI ボタンから呼び出してみます。
- Hierarchy で
Canvasを作成(なければ)。 Canvasの子としてButtonを作成(Right Click → UI → Button)。- Button ノードを選択し、Inspector の
Buttonコンポーネント内 Click Events に新しいイベントを追加。 - 追加したイベントの Target に
BGMPlayerノードをドラッグ&ドロップ。 - Component ドロップダウンから
BGMTransitionを選択。 - Handler ドロップダウンから
triggerTransitionを選択。 - ゲームを再生し、ボタンをクリックすると、設定した
Target Clipにクロスフェードしていれば成功です。
5. エリアごとに BGM を変えたい場合の例
例えば「フィールド」「ダンジョン」「ボス部屋」で BGM を切り替えたい場合:
BGMPlayerノードは 1 つだけ用意(AudioSourceを持つ)。- 各エリア用に空ノードを作成:
FieldAreaDungeonAreaBossArea
- それぞれのエリアノードに
BGMTransitionをアタッチし、以下を設定:- Target Clip: そのエリア用の BGM
- Audio Source Node: 共通の
BGMPlayerノード - その他のフェード設定は共通でも可
- プレイヤーがエリアに入ったタイミングで、そのエリアノードの
BGMTransition.triggerTransition()を呼び出すだけで、共通の BGMPlayer の音量を操作しつつ、滑らかに BGM が切り替わります。
このように、BGM の実体(AudioSource)は 1 箇所にまとめつつ、切り替えロジックだけを各エリアに分散できるため、シーン構成が非常にシンプルになります。
まとめ
本記事では、他のスクリプトに一切依存しない、完全独立な BGM クロスフェード用コンポーネント「BGMTransition」を実装しました。
- Inspector で:
- クロスフェード先の BGM(
AudioClip) - フェード時間
- 最終ボリューム
- 開始時自動再生の有無
- フェード中の割り込み可否
を設定するだけで、BGM 切り替えロジックを何度も書く必要がなくなります。
- クロスフェード先の BGM(
- AudioSource の取得は:
- 明示指定(
audioSourceNode) - 未指定時の自動探索(自ノード → 親ノード)
により、既存プロジェクトにも簡単に組み込み可能です。
- 明示指定(
- エリアごとに
BGMTransitionを配置しておけば、エリア進入時にtriggerTransition()を呼ぶだけで、自然な BGM 切り替えを実現できます。
このコンポーネント単体で BGM のクロスフェード制御が完結するため、ゲームの規模が大きくなっても BGM 管理コードが肥大化しにくく、再利用性も高いのがメリットです。
プロジェクトの BGM 制御の共通パーツとして、ぜひそのまま組み込んで活用してみてください。




