【Cocos Creator 3.8】ReverbZone(残響エリア)の実装:アタッチするだけで「エリアに入った時だけリバーブがかかる」3Dサウンド空間を実現する汎用スクリプト
本記事では、特定のエリアに入った時だけリバーブ(残響)がかかる「ReverbZone」コンポーネントを、Cocos Creator 3.8 + TypeScript で実装します。
リスナー(カメラやプレイヤー)ノードを指定しておくだけで、そのノードがエリアに入ると AudioSource のリバーブを自動的にオン/オフしてくれる汎用スクリプトです。
洞窟・ホール・トンネル・屋内など、「ここに入ったら残響が増える」といった演出を、ノードにアタッチして半径を設定するだけで簡単に実現できます。
目次
コンポーネントの設計方針
1. 機能要件の整理
- ReverbZone をアタッチしたノードを中心に、球形の有効範囲(半径)を持つ。
- 指定した「ターゲットノード」(通常はカメラやプレイヤー)がこの範囲に入ったら、指定された AudioSource 群にリバーブを有効化する。
- 範囲から出たら、リバーブを無効化する。
- Cocos Creator 3.8 の
AudioSourceコンポーネントのreverbプロパティを利用する。 - 外部の GameManager やシングルトンに依存せず、このコンポーネント単体で完結する。
- Inspector から以下を調整できる:
- 有効半径
- フェード時間(リバーブを徐々にかける/切る)
- リバーブの強さ(ターゲットの AudioSource.reverb へ反映)
- 対象となる AudioSource のリスト
- 距離判定に使うターゲットノード(リスナー)
2. 外部依存をなくすためのアプローチ
- ゲーム全体のオーディオ管理クラスには依存しない。
- リバーブをかけたい AudioSource は、Inspector で 配列プロパティに直接ドラッグ&ドロップして指定する。
- リスナー(距離判定に使うノード)も Inspector で指定する。
- 通常はカメラやプレイヤーノードを指定。
- 未設定の場合、
onLoadで警告ログを出し、処理を止める。
- リバーブのオン/オフは、AudioSource.reverb を
0.0 ~ 1.0の範囲で制御する。 - フェード用の補間は
update()内で行い、Time.deltaTime によるスムーズな変化を実現する。
3. Inspector で設定可能なプロパティ設計
enabledDebugGizmo: boolean- シーンビュー上で簡易的な「範囲表示」を行うかどうか(ログのみの簡易デバッグ)。
radius: number- このノードを中心とした ReverbZone の半径。
- 単位:ワールド座標系の距離(メートル相当)。
- 例:5.0 に設定すると、半径 5 の球形エリア。
fadeTime: number- リバーブを 0 → targetReverb / targetReverb → 0 に変化させるのにかける時間(秒)。
- 0 を指定すると、即座に切り替える。
targetReverb: number- エリア内に入ったときの最終的なリバーブ量。
0.0 ~ 1.0の範囲で指定。- 例:0.8 にするとかなり強めの残響。
listener: Node | null- 距離判定に使うノード(通常はカメラやプレイヤー)。
- このノードの位置と ReverbZone の位置の距離で「エリア内かどうか」を判定する。
targetAudioSources: AudioSource[]- リバーブ量を制御したい AudioSource コンポーネントの配列。
- BGM や環境音など、リバーブをかけたい全ての AudioSource をここに登録する。
TypeScriptコードの実装
以下が完成した ReverbZone.ts の実装です。
import { _decorator, Component, Node, Vec3, AudioSource, math } from 'cc';
const { ccclass, property } = _decorator;
/**
* ReverbZone
*
* 指定した listener ノードがこのノードを中心とした半径内に入ると、
* 指定された AudioSource 群の reverb 値をフェードさせて変更するコンポーネントです。
*
* - 外部の GameManager やシングルトンには依存しません。
* - 対象 AudioSource や listener ノードは Inspector から設定します。
*/
@ccclass('ReverbZone')
export class ReverbZone extends Component {
@property({
tooltip: 'シーンビューでのデバッグ用フラグ。true のとき、ゾーンの状態をログ出力します。'
})
public enabledDebugGizmo: boolean = false;
@property({
tooltip: 'このノードを中心とした ReverbZone の半径(ワールド座標系)。'
})
public radius: number = 5.0;
@property({
tooltip: 'リバーブのオン/オフを切り替えるときのフェード時間(秒)。0 で即時切り替え。'
})
public fadeTime: number = 0.5;
@property({
tooltip: 'エリア内に入ったときの最終的なリバーブ量(0.0 ~ 1.0)。'
})
public targetReverb: number = 0.8;
@property({
type: Node,
tooltip: '距離判定に使うノード(通常はカメラやプレイヤー)。このノードが半径内に入るとリバーブが有効になります。'
})
public listener: Node | null = null;
@property({
type: [AudioSource],
tooltip: 'リバーブを制御したい AudioSource コンポーネントの配列。'
})
public targetAudioSources: AudioSource[] = [];
// 内部状態管理用
private _isInside: boolean = false; // 現在 listener がゾーン内かどうか
private _currentReverb: number = 0.0; // 現在のリバーブ値(内部的な補間用)
private _velocity: number = 0.0; // スムーズダンピング用の速度(オプション)
private _tempListenerWorldPos: Vec3 = new Vec3();
private _tempZoneWorldPos: Vec3 = new Vec3();
onLoad() {
// listener が設定されていない場合は警告を出す
if (!this.listener) {
console.warn('[ReverbZone] listener ノードが設定されていません。ReverbZone は動作しません。', this.node);
}
// targetAudioSources が空の場合も警告
if (!this.targetAudioSources || this.targetAudioSources.length === 0) {
console.warn('[ReverbZone] targetAudioSources が設定されていません。リバーブを制御する AudioSource がありません。', this.node);
}
// targetReverb のクランプ
this.targetReverb = math.clamp01(this.targetReverb);
// 開始時はすべての AudioSource の reverb を 0 にしておく(安全のため)
this._currentReverb = 0.0;
this._applyReverbToAll(0.0);
}
start() {
// 特に初期化は onLoad で済ませているが、
// 必要に応じてここで追加のセットアップを行える。
if (this.enabledDebugGizmo) {
console.log('[ReverbZone] start. radius =', this.radius, 'targetReverb =', this.targetReverb);
}
}
update(deltaTime: number) {
// listener が設定されていない場合は何もしない
if (!this.listener) {
return;
}
// Reverb を制御する AudioSource が一つもない場合も何もしない
if (!this.targetAudioSources || this.targetAudioSources.length === 0) {
return;
}
// ワールド座標で listener とこのノードの位置を取得
this.listener.getWorldPosition(this._tempListenerWorldPos);
this.node.getWorldPosition(this._tempZoneWorldPos);
const distance = Vec3.distance(this._tempListenerWorldPos, this._tempZoneWorldPos);
// 現在ゾーン内かどうかを計算
const isInsideNow = distance <= this.radius;
// 状態が変わったときだけログを出す
if (isInsideNow !== this._isInside) {
this._isInside = isInsideNow;
if (this.enabledDebugGizmo) {
console.log('[ReverbZone] listener is now', isInsideNow ? 'INSIDE' : 'OUTSIDE', 'zone. distance =', distance.toFixed(2));
}
}
// 目標リバーブ値を決定
const target = this._isInside ? this.targetReverb : 0.0;
// フェード時間が 0 または非常に小さい場合は即時切り替え
if (this.fadeTime <= 0.0001) {
if (this._currentReverb !== target) {
this._currentReverb = target;
this._applyReverbToAll(this._currentReverb);
}
return;
}
// 線形補間で徐々に目標値に近づける
const diff = target - this._currentReverb;
if (Math.abs(diff) > 0.0001) {
const step = deltaTime / this.fadeTime;
// クランプ付き補間
if (diff > 0) {
this._currentReverb += Math.min(diff, step * this.targetReverb);
} else {
this._currentReverb += Math.max(diff, -step * this.targetReverb);
}
this._currentReverb = math.clamp01(this._currentReverb);
this._applyReverbToAll(this._currentReverb);
}
}
/**
* すべての targetAudioSources に同じ reverb 値を適用する。
* 無効な参照は自動的にスキップする。
*/
private _applyReverbToAll(value: number) {
if (!this.targetAudioSources) {
return;
}
for (let i = 0; i < this.targetAudioSources.length; i++) {
const src = this.targetAudioSources[i];
if (!src) {
continue;
}
// AudioSource に reverb プロパティが存在するか防御的にチェック
if ('reverb' in src) {
// @ts-ignore - 型定義に無い場合でも実行時に存在すれば利用する
src.reverb = value;
} else {
// もし古いバージョンやカスタムビルドで reverb が存在しない場合は警告
console.warn('[ReverbZone] AudioSource に reverb プロパティが存在しません。Cocos Creator のバージョンを確認してください。', src.node);
}
}
}
}
コードのポイント解説
onLoad()listenerとtargetAudioSourcesが設定されていない場合に警告を出すことで、エディタ上での設定ミスに気づきやすくしています。targetReverbを0.0 ~ 1.0にクランプし、開始時に_applyReverbToAll(0.0)で全ての AudioSource のリバーブを 0 に初期化します。
update(deltaTime)- 毎フレーム、
listenerノードと ReverbZone ノードのワールド座標を取得し、Vec3.distanceで距離を算出します。 - 距離が
radius以下なら_isInside = true、それ以外はfalseとしてゾーン内外を判定します。 fadeTimeが 0 の場合は即座にtargetReverb/ 0 に切り替え、0 より大きい場合は 線形補間で徐々に_currentReverbを目標値に近づけます。- 補間後の
_currentReverbを_applyReverbToAll()で全 AudioSource に適用します。
- 毎フレーム、
_applyReverbToAll(value)targetAudioSources配列をループし、各 AudioSource のreverbプロパティに値を代入します。- 型定義に
reverbが無い場合を考慮し、'reverb' in srcで存在チェックしつつ、// @ts-ignoreで TypeScript の型エラーを抑制しています。 - 存在しない場合は警告ログを出し、開発時にバージョンや環境の問題に気づけるようにしています。
使用手順と動作確認
1. スクリプトファイルの作成
- Assets パネルで右クリック → Create → TypeScript を選択します。
- ファイル名を
ReverbZone.tsとして作成します。 - 作成された
ReverbZone.tsをダブルクリックしてエディタ(VS Code など)で開き、前章のコードをそのまま貼り付けて保存します。
2. テスト用シーンの準備
- Hierarchy で右クリック → Create → 3D Object → Node(もしくは 2D 用の Node)で、プレイヤー/カメラ用のノードを作成します。
- 名前例:
PlayerやMainCamera。
- 名前例:
- 同様に、リバーブをかけたい音源用のノードを作成します。
- Hierarchy で右クリック → Create → 3D Object → Node。
- 名前例:
BGMSource。
- 音源ノードに AudioSource コンポーネントを追加します。
- 音源ノード(例:
BGMSource)を選択。 - Inspector の Add Component ボタンをクリック。
- Audio → AudioSource を選択。
- AudioSource の Clip に BGM や環境音の AudioClip を設定します。
- Play On Awake を ON にしておくと、シーン開始と同時に再生されます。
- 音源ノード(例:
3. ReverbZone ノードの作成とアタッチ
- Hierarchy で右クリック → Create → 3D Object → Node で、新しいノードを作成します。
- 名前例:
CaveReverbZone。
- 名前例:
- このノードを「洞窟の入口」や「ホールの中央」など、残響が増えてほしいエリアの中心に配置します。
- Scene ビューで移動ツールを使い、位置を調整します。
CaveReverbZoneノードを選択した状態で、Inspector の Add Component ボタンをクリックします。- Custom → ReverbZone を選択してアタッチします。
4. Inspector で ReverbZone のプロパティを設定
CaveReverbZone ノードを選択し、Inspector の ReverbZone コンポーネントを以下のように設定します。
- Enabled Debug Gizmo:
- 動作確認中は ON(チェック) にしておくと、コンソールにログが出て状態が分かりやすくなります。
- Radius:
- 例:
5と入力。 - これで、
CaveReverbZoneを中心に半径 5 のエリアが ReverbZone になります。
- 例:
- Fade Time:
- 例:
0.5秒。 - エリアに入ったとき/出たときに、0.5 秒かけてリバーブが徐々に変化します。
- カチッと切り替えたい場合は
0に設定します。
- 例:
- Target Reverb:
- 例:
0.8。 - エリア内での最終的なリバーブ量です。0.3〜0.6 くらいが自然な残響、0.8 以上はかなり強いエコー感になります。
- 例:
- Listener:
- Hierarchy から
PlayerやMainCameraノードをドラッグ&ドロップして設定します。 - このノードの位置が ReverbZone の中心から Radius 以下になったときにリバーブが有効になります。
- Hierarchy から
- Target Audio Sources:
- サイズを
1に設定し、要素 0 にBGMSourceノード上の AudioSource をドラッグ&ドロップします。 - 複数の音源にリバーブをかけたい場合は、サイズを 2, 3… と増やし、それぞれの AudioSource を登録します。
- サイズを
5. ゲームビューでの動作確認
- シーンに、listener ノード(Player や Camera)が移動できる仕組みがある場合は、それを使って ReverbZone に近づけてみます。
- キーボード入力で移動するスクリプトが無い場合は、一旦エディタ上で listener ノードをドラッグして位置を変え、実行中に移動させて確認しても構いません。
- メニューの Play ボタンを押してゲームを再生します。
- Console パネルを開き、[ReverbZone] listener is now INSIDE/OUTSIDE zone というログが出るか確認します。
- listener が半径内に入ると
INSIDE、出るとOUTSIDEが表示されます。
- listener が半径内に入ると
- 音を聞きながら、listener を ReverbZone の内外に移動させてみてください。
- エリアに入ると BGM や環境音に残響が増え、ホールや洞窟のような音になるはずです。
- エリアから出ると、徐々に(または即座に)リバーブが消え、通常の音に戻ります。
- 必要に応じて Radius / Fade Time / Target Reverb を微調整し、狙った雰囲気になるように調整します。
まとめ
今回実装した ReverbZone コンポーネントは、
- ノードにアタッチし、半径・リスナー・対象 AudioSource を Inspector で指定するだけで動作する。
- 外部の GameManager やシングルトンに依存しないため、どのプロジェクトにもそのまま持ち込んで再利用できる。
- 複数のゾーンをシーン内に配置することで、「洞窟 → 通路 → 大広間」といったシーンでも、エリアごとに異なる残響を簡単に表現できる。
応用例としては、
- 屋外から屋内に入ったときだけ BGM に残響を足す。
- ボス部屋に入ると、BGM と環境音の両方に強いリバーブをかけて緊張感を演出する。
- トンネルや洞窟の途中に複数の ReverbZone を配置し、場所によって残響の強さを変える。
といった使い方ができます。
このように、単体で完結する汎用コンポーネントとして設計しておくと、別プロジェクトへの持ち込みやチーム内での共有が非常に楽になります。
本記事の ReverbZone をベースに、ディレイやローパスフィルタなど、他のオーディオエフェクト用ゾーンコンポーネントを増やしていくのもおすすめです。
