【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.reverb0.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()
    • listenertargetAudioSources が設定されていない場合に警告を出すことで、エディタ上での設定ミスに気づきやすくしています。
    • targetReverb0.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. スクリプトファイルの作成

  1. Assets パネルで右クリック → Create → TypeScript を選択します。
  2. ファイル名を ReverbZone.ts として作成します。
  3. 作成された ReverbZone.ts をダブルクリックしてエディタ(VS Code など)で開き、前章のコードをそのまま貼り付けて保存します。

2. テスト用シーンの準備

  1. Hierarchy で右クリック → Create → 3D Object → Node(もしくは 2D 用の Node)で、プレイヤー/カメラ用のノードを作成します。
    • 名前例:PlayerMainCamera
  2. 同様に、リバーブをかけたい音源用のノードを作成します。
    • Hierarchy で右クリック → Create → 3D Object → Node
    • 名前例:BGMSource
  3. 音源ノードに AudioSource コンポーネントを追加します。
    • 音源ノード(例:BGMSource)を選択。
    • Inspector の Add Component ボタンをクリック。
    • Audio → AudioSource を選択。
    • AudioSource の Clip に BGM や環境音の AudioClip を設定します。
    • Play On Awake を ON にしておくと、シーン開始と同時に再生されます。

3. ReverbZone ノードの作成とアタッチ

  1. Hierarchy で右クリック → Create → 3D Object → Node で、新しいノードを作成します。
    • 名前例:CaveReverbZone
  2. このノードを「洞窟の入口」や「ホールの中央」など、残響が増えてほしいエリアの中心に配置します。
    • Scene ビューで移動ツールを使い、位置を調整します。
  3. CaveReverbZone ノードを選択した状態で、Inspector の Add Component ボタンをクリックします。
  4. 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 から PlayerMainCamera ノードをドラッグ&ドロップして設定します。
    • このノードの位置が ReverbZone の中心から Radius 以下になったときにリバーブが有効になります。
  • Target Audio Sources:
    • サイズを 1 に設定し、要素 0 に BGMSource ノード上の AudioSource をドラッグ&ドロップします。
    • 複数の音源にリバーブをかけたい場合は、サイズを 2, 3… と増やし、それぞれの AudioSource を登録します。

5. ゲームビューでの動作確認

  1. シーンに、listener ノード(Player や Camera)が移動できる仕組みがある場合は、それを使って ReverbZone に近づけてみます。
    • キーボード入力で移動するスクリプトが無い場合は、一旦エディタ上で listener ノードをドラッグして位置を変え、実行中に移動させて確認しても構いません。
  2. メニューの Play ボタンを押してゲームを再生します。
  3. Console パネルを開き、[ReverbZone] listener is now INSIDE/OUTSIDE zone というログが出るか確認します。
    • listener が半径内に入ると INSIDE、出ると OUTSIDE が表示されます。
  4. 音を聞きながら、listener を ReverbZone の内外に移動させてみてください。
    • エリアに入ると BGM や環境音に残響が増え、ホールや洞窟のような音になるはずです。
    • エリアから出ると、徐々に(または即座に)リバーブが消え、通常の音に戻ります。
  5. 必要に応じて Radius / Fade Time / Target Reverb を微調整し、狙った雰囲気になるように調整します。

まとめ

今回実装した ReverbZone コンポーネントは、

  • ノードにアタッチし、半径・リスナー・対象 AudioSource を Inspector で指定するだけで動作する。
  • 外部の GameManager やシングルトンに依存しないため、どのプロジェクトにもそのまま持ち込んで再利用できる
  • 複数のゾーンをシーン内に配置することで、「洞窟 → 通路 → 大広間」といったシーンでも、エリアごとに異なる残響を簡単に表現できる。

応用例としては、

  • 屋外から屋内に入ったときだけ BGM に残響を足す。
  • ボス部屋に入ると、BGM と環境音の両方に強いリバーブをかけて緊張感を演出する。
  • トンネルや洞窟の途中に複数の ReverbZone を配置し、場所によって残響の強さを変える。

といった使い方ができます。

このように、単体で完結する汎用コンポーネントとして設計しておくと、別プロジェクトへの持ち込みやチーム内での共有が非常に楽になります。
本記事の ReverbZone をベースに、ディレイやローパスフィルタなど、他のオーディオエフェクト用ゾーンコンポーネントを増やしていくのもおすすめです。