【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 / fadeDurationcurrentVolume = 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. スクリプトファイルの作成
- エディタ左下の Assets パネルで、任意のフォルダ(例:
scripts/audio)を右クリックします。 - Create > TypeScript を選択し、ファイル名を
VolumeFader.tsにします。 - 作成された
VolumeFader.tsをダブルクリックして開き、既存コードをすべて削除して、上記の TypeScript コードを丸ごと貼り付けて保存します。
2. テスト用ノードと AudioSource の用意
- Hierarchy パネルで右クリックし、Create > Empty Node を選択して、ノード名を
BGMなど分かりやすい名前に変更します。 BGMノードを選択した状態で、Inspector の Add Component ボタンをクリックし、Audio > AudioSource を追加します。- AudioSource の Clip に BGM 用のオーディオクリップ(wav/mp3 など)を設定し、Loop にチェックを入れておきます。
- 必要に応じて Play On Awake にチェックを入れておくと、シーン開始時に自動再生されます。
3. VolumeFader コンポーネントをアタッチ
- 再び
BGMノードを選択した状態で、Inspector の Add Component をクリックします。 - Custom カテゴリの中から VolumeFader を選択して追加します。
- もし Custom に表示されない場合は、スクリプト保存後に一度エディタをフォーカスし直すか、Project > Reload で再読み込みしてください。
4. Inspector でプロパティを設定
- fadeDuration:
- 例:
2.0に設定すると、2 秒かけて音量が 0 になります。
- 例:
- autoFadeOnDisable:
- シーン切り替え時に BGM ノードを
setActive(false)で無効化する運用なら、ONにします。 - 今回はまず OFF のままでも構いません(手動テストを優先)。
- シーン切り替え時に BGM ノードを
- autoFadeOnDestroy:
- シーン切り替え時にノードが破棄されるケースで自動フェードしたい場合は
ONにします。 - ただし前述の通り、確実なフェード時間を取りたい場合は、シーン遷移前に明示的に
startFadeOut()を呼ぶ設計の方が安全です。
- シーン切り替え時にノードが破棄されるケースで自動フェードしたい場合は
- stopAfterFade:
- 通常は
ON(デフォルトのまま)で問題ありません。 - フェード後も「再生状態のまま volume 0」を維持したい特殊なケースでのみ OFF にします。
- 通常は
- initialVolume:
- デフォルトの
-1のままだと、AudioSource に設定済みの volume をそのまま使います。 - 開始時に必ず 0.5 にしたいなどの要件がある場合は
0.5など 0〜1 の値を設定します。
- デフォルトの
5. シンプルな動作確認(エディタ上で手動テスト)
まずは「ノードを無効化した時にフェードアウトする」挙動を確認してみます。
- Scene を再生(▶ボタン)して、BGM が再生されていることを確認します。
- 再生中に Hierarchy パネルで
BGMノードを選択し、Inspector の一番上にあるチェックボックス(Enabled)を OFF に切り替えます。 - もし autoFadeOnDisable を ON にしていれば、このタイミングでフェードアウトが始まり、2 秒かけて音が小さくなっていくはずです。
- フェード時間を
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 の「切れ目のない」シーン遷移を体験してみてください。
