【Cocos Creator 3.8】LowPassFilter(水中音響)の実装:アタッチするだけで「水中に入ったときだけ音がこもる」環境音演出を実現する汎用スクリプト

このコンポーネントは、プレイヤーなどの対象ノードが「水中エリア」に入ったときにだけ、指定した AudioBus にローパスフィルタをかけて音をこもらせるための汎用スクリプトです。
水中ステージや、水たまり・池に飛び込んだときの「こもった音」の演出を、ノードにアタッチしてプロパティを設定するだけで簡単に実現できます。


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

機能要件の整理

  • このコンポーネントをアタッチしたノード(=水中エリアのコライダー)に、プレイヤーなどの対象が入ったら「水中状態」と判定する。
  • 水中状態の間だけ、指定した AudioBus のローパスフィルタを有効化し、カットオフ周波数などを設定する。
  • 水中から出たら、ローパスフィルタを元の値に戻す(もしくは無効化)する。
  • 他のカスタムスクリプトに一切依存せず、このスクリプト単体で完結する。
  • 水中エリアの「対象判定」は 2D 物理(Collider2D)の トリガー を用いて行う。
  • ローパスフィルタは AudioBus のプロパティ(cutoff, resonance など)を書き換える形で制御する。

AudioBus は Cocos Creator 3.8 で導入されたオーディオルーティング機能で、AudioSource の出力先として利用できます。
このコンポーネントは、Inspector から対象の AudioBus をドラッグ&ドロップで指定し、かつ「水中用のパラメータ」「元のパラメータ」を保持しておくことで、シーン内のどの AudioSource にも干渉せず、バス単位で音響を切り替える設計にします。

外部依存をなくす設計アプローチ

  • 「プレイヤー管理スクリプト」や「GameManager」などには依存しない。
  • 「誰を水中判定の対象とするか」は Collider2D のグループ・タグ・ノード名などでフィルタリングできるようにする。
  • 必要な標準コンポーネント(Collider2D)は、onLoad で自動取得し、存在しなければエラーログを出す。
  • AudioBus への参照も Inspector から設定する形にし、未設定の場合はエラーログを出して何もしない。

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

このコンポーネントで用意する主な @property は以下の通りです。

  • targetBus: AudioBus
    • ローパスフィルタをかけたい AudioBus を指定。
    • ここにルーティングされた全ての AudioSource が、水中時にこもった音になる。
  • useEnterExitCallbacks: boolean
    • Collider2D の onTriggerEnter/Exit コールバックを利用するかどうか。
    • 通常は true のままで良い。
  • filterOnEnter: boolean
    • 水中に入ったときにローパスフィルタを有効化するかどうか。
    • デバッグ用途で一時的に OFF にするためのスイッチ。
  • underwaterCutoff: number
    • 水中時のローパスカットオフ周波数(Hz)。
    • 例: 600〜1200Hz 程度に設定すると、かなりこもった水中感が出る。
  • underwaterResonance: number
    • 水中時のローパスフィルタのレゾナンス(Q)。
    • 値が大きいほどカットオフ付近が強調される。0.5〜2.0 程度が自然。
  • transitionTime: number
    • 現在の値から水中/非水中の値へ補間する時間(秒)。
    • 0 にすると即時切り替え、0.2〜0.5 くらいにすると自然なフェード。
  • filterOnlySpecificGroup: boolean
    • 水中判定の対象を特定の物理グループに限定するかどうか。
  • targetGroup: number
    • 水中判定の対象とする PhysicsGroup のビット番号。
    • 例: プレイヤーの Collider2D をグループ 1、敵をグループ 2、などに分けた場合に利用。
  • debugLog: boolean
    • 水中への出入りや AudioBus の状態変更をログに出すかどうか。

さらに、AudioBus の「元の値」を自動的に保存しておき、非水中に戻った時に復元するための内部変数を持ちます。これにより、既に他で設定されているバスの音響設計を壊さずに一時的に水中用に上書きすることができます。


TypeScriptコードの実装


import { _decorator, Component, Collider2D, IPhysics2DContact, AudioBus, log, error, Node, math } from 'cc';
const { ccclass, property } = _decorator;

/**
 * LowPassFilter(水中音響)
 * 
 * このコンポーネントを「水中エリア」としたいノードにアタッチし、
 * Trigger に設定した Collider2D を追加することで、
 * 指定した AudioBus にローパスフィルタを適用 / 解除します。
 */
@ccclass('LowPassFilter')
export class LowPassFilter extends Component {

    @property({
        type: AudioBus,
        tooltip: '水中時のローパスフィルタを適用する対象の AudioBus。\nここにルーティングされた AudioSource の音がこもります。'
    })
    public targetBus: AudioBus | null = null;

    @property({
        tooltip: 'Collider2D の onTriggerEnter/onTriggerExit コールバックを使用して、水中への出入りを検出します。'
    })
    public useEnterExitCallbacks: boolean = true;

    @property({
        tooltip: 'true の場合、水中に入ったときにローパスフィルタを有効化します。\nデバッグ用途で一時的に OFF にできます。'
    })
    public filterOnEnter: boolean = true;

    @property({
        tooltip: '水中時のローパスカットオフ周波数(Hz)。\n値を小さくするほど高音が削られてこもった音になります。'
    })
    public underwaterCutoff: number = 800.0;

    @property({
        tooltip: '水中時のローパスフィルタのレゾナンス(Q)。\n0.5~2.0 程度を推奨。'
    })
    public underwaterResonance: number = 1.0;

    @property({
        tooltip: '水中 / 非水中状態の切り替えにかける補間時間(秒)。\n0 で即時切り替え。'
    })
    public transitionTime: number = 0.3;

    @property({
        tooltip: 'true の場合、指定した Physics グループの Collider2D のみを水中判定対象とします。'
    })
    public filterOnlySpecificGroup: boolean = false;

    @property({
        tooltip: '水中判定対象とする Physics グループのビット番号。\n例: デフォルトグループ=0, 1番グループ=1 など。\nfilterOnlySpecificGroup が true のときのみ使用されます。'
    })
    public targetGroup: number = 0;

    @property({
        tooltip: 'true の場合、水中への出入りやフィルタ適用状態をログ出力します。'
    })
    public debugLog: boolean = false;

    // --- 内部状態管理用 ---

    private _collider: Collider2D | null = null;

    // AudioBus の元のパラメータを保存
    private _originalCutoff: number | null = null;
    private _originalResonance: number | null = null;

    // 補間用
    private _currentCutoff: number = 0;
    private _currentResonance: number = 0;
    private _targetCutoff: number = 0;
    private _targetResonance: number = 0;
    private _isUnderwater: boolean = false;

    // 物理接触中の対象数(複数の Collider が同時に水中にいるケースを考慮)
    private _insideCount: number = 0;

    onLoad() {
        // Collider2D を取得
        this._collider = this.getComponent(Collider2D);
        if (!this._collider) {
            error('[LowPassFilter] Collider2D がアタッチされていません。このノードに Collider2D (Trigger) を追加してください。');
        } else {
            // Trigger であることを推奨
            if (!this._collider.isTrigger) {
                if (this.debugLog) {
                    log('[LowPassFilter] Collider2D が Trigger ではありません。isTrigger = true を推奨します。');
                }
            }

            if (this.useEnterExitCallbacks) {
                this._collider.on('onTriggerEnter', this._onTriggerEnter, this);
                this._collider.on('onTriggerExit', this._onTriggerExit, this);
            }
        }

        if (!this.targetBus) {
            error('[LowPassFilter] targetBus が設定されていません。Inspector から AudioBus を割り当ててください。');
        } else {
            // AudioBus の現在値を保存しておく
            this._originalCutoff = this.targetBus.lowpassCutoff;
            this._originalResonance = this.targetBus.lowpassResonance;

            this._currentCutoff = this.targetBus.lowpassCutoff;
            this._currentResonance = this.targetBus.lowpassResonance;

            this._targetCutoff = this._currentCutoff;
            this._targetResonance = this._currentResonance;

            if (this.debugLog) {
                log(`[LowPassFilter] 初期化: cutoff=${this._originalCutoff}, resonance=${this._originalResonance}`);
            }
        }
    }

    onDestroy() {
        // イベントの解除
        if (this._collider && this.useEnterExitCallbacks) {
            this._collider.off('onTriggerEnter', this._onTriggerEnter, this);
            this._collider.off('onTriggerExit', this._onTriggerExit, this);
        }

        // シーンから削除される際に AudioBus の値を元に戻す
        if (this.targetBus && this._originalCutoff !== null && this._originalResonance !== null) {
            this.targetBus.lowpassCutoff = this._originalCutoff;
            this.targetBus.lowpassResonance = this._originalResonance;
            if (this.debugLog) {
                log('[LowPassFilter] onDestroy: AudioBus のローパス設定を初期値に戻しました。');
            }
        }
    }

    /**
     * 物理トリガーに入ったときのコールバック
     */
    private _onTriggerEnter(selfCollider: Collider2D, otherCollider: Collider2D, contact?: IPhysics2DContact | null) {
        if (!this.filterOnEnter) {
            return;
        }
        if (!this._isTargetCollider(otherCollider)) {
            return;
        }

        this._insideCount++;
        if (this._insideCount === 1) {
            // 初めて水中エリアに入った
            this._setUnderwater(true);
        }

        if (this.debugLog) {
            log(`[LowPassFilter] onTriggerEnter: insideCount=${this._insideCount}, underwater=${this._isUnderwater}`);
        }
    }

    /**
     * 物理トリガーから出たときのコールバック
     */
    private _onTriggerExit(selfCollider: Collider2D, otherCollider: Collider2D, contact?: IPhysics2DContact | null) {
        if (!this.filterOnEnter) {
            return;
        }
        if (!this._isTargetCollider(otherCollider)) {
            return;
        }

        this._insideCount = Math.max(0, this._insideCount - 1);
        if (this._insideCount === 0) {
            // 全ての対象が水中エリアから出た
            this._setUnderwater(false);
        }

        if (this.debugLog) {
            log(`[LowPassFilter] onTriggerExit: insideCount=${this._insideCount}, underwater=${this._isUnderwater}`);
        }
    }

    /**
     * 対象 Collider が水中判定対象かどうかをチェック
     */
    private _isTargetCollider(other: Collider2D): boolean {
        if (!this.filterOnlySpecificGroup) {
            return true;
        }
        return other.group === this.targetGroup;
    }

    /**
     * 水中状態の切り替え
     */
    private _setUnderwater(isUnderwater: boolean) {
        if (!this.targetBus) {
            return;
        }

        this._isUnderwater = isUnderwater;

        if (isUnderwater) {
            // 水中用パラメータへ
            this._targetCutoff = this.underwaterCutoff;
            this._targetResonance = this.underwaterResonance;
        } else {
            // 元のパラメータへ戻す
            if (this._originalCutoff !== null) {
                this._targetCutoff = this._originalCutoff;
            } else {
                this._targetCutoff = this.targetBus.lowpassCutoff;
            }

            if (this._originalResonance !== null) {
                this._targetResonance = this._originalResonance;
            } else {
                this._targetResonance = this.targetBus.lowpassResonance;
            }
        }

        // transitionTime が 0 の場合は即時反映
        if (this.transitionTime <= 0) {
            this._currentCutoff = this._targetCutoff;
            this._currentResonance = this._targetResonance;
            this._applyToBus();
        }

        if (this.debugLog) {
            log(`[LowPassFilter] _setUnderwater: isUnderwater=${isUnderwater}, targetCutoff=${this._targetCutoff}, targetResonance=${this._targetResonance}`);
        }
    }

    /**
     * 毎フレーム呼ばれ、AudioBus のパラメータを補間します。
     */
    update(dt: number) {
        if (!this.targetBus) {
            return;
        }

        if (this.transitionTime <= 0) {
            // 即時切り替えの場合は何もしない
            return;
        }

        // 線形補間
        const t = math.clamp01(dt / this.transitionTime);

        this._currentCutoff = math.lerp(this._currentCutoff, this._targetCutoff, t);
        this._currentResonance = math.lerp(this._currentResonance, this._targetResonance, t);

        this._applyToBus();
    }

    /**
     * 現在の補間値を AudioBus に適用
     */
    private _applyToBus() {
        if (!this.targetBus) {
            return;
        }
        this.targetBus.lowpassCutoff = this._currentCutoff;
        this.targetBus.lowpassResonance = this._currentResonance;
    }
}

コードのポイント解説

  • onLoad
    • 同一ノード上の Collider2D を取得し、存在しない場合は error ログを出力。
    • useEnterExitCallbacks が有効なら、onTriggerEnter/onTriggerExit イベントを登録。
    • targetBus が設定されていれば、その lowpassCutoff, lowpassResonance を「元の値」として保存し、内部の現在値・目標値にも反映。
  • onDestroy
    • Collider2D のイベントを解除。
    • AudioBus のローパス設定を初期値に戻す(シーン遷移などでコンポーネントが破棄されたときに副作用を残さない)。
  • _onTriggerEnter / _onTriggerExit
    • 水中エリアに入った対象数を _insideCount でカウントし、0→1 のタイミングで水中 ON、1→0 のタイミングで水中 OFF にすることで、複数 Collider 対応。
    • filterOnlySpecificGroup が有効な場合、targetGroup に一致する物理グループの Collider のみを水中判定対象とする。
  • _setUnderwater
    • 水中 / 非水中状態に応じて、_targetCutoff, _targetResonance を切り替える。
    • transitionTime が 0 以下なら、その場で現在値も書き換え、即座に AudioBus に適用。
  • update
    • transitionTime が 0 より大きい場合に、dt / transitionTime を補間係数として math.lerp で現在値→目標値へ徐々に近づける。
    • 毎フレーム _applyToBus で AudioBus に反映。

使用手順と動作確認

1. TypeScript スクリプトの作成

  1. Editor の Assets パネルで任意のフォルダ(例: assets/scripts/audio)を右クリックします。
  2. Create → TypeScript を選択し、ファイル名を LowPassFilter.ts にします。
  3. 自動生成された中身をすべて削除し、本記事の 実装コード を丸ごと貼り付けて保存します。

2. AudioBus の準備

  1. Project → Project Settings → Audio を開きます。(3.8 での AudioBus 設定画面)
  2. 「Buses」セクションで + ボタンを押し、新しい AudioBus(例: UnderwaterBus)を作成します。
  3. 作成した AudioBus の デフォルトの lowpassCutoff / lowpassResonance は、通常のゲーム状態用の値にしておきます。
    • 例: lowpassCutoff = 22000(ほぼフルレンジ)、lowpassResonance = 1.0 など。
  4. 水中演出を適用したい AudioSource(BGM や環境音など)の Audio Bus を、この UnderwaterBus に設定します。
    • ノードを選択 → Inspector で AudioSource コンポーネント → Bus プロパティに UnderwaterBus を指定。

3. 水中エリア用ノードの作成

  1. Hierarchy パネルで右クリック → Create → Empty Node を選択し、ノード名を WaterArea などに変更します。
  2. WaterArea ノードを選択し、Inspector の Add Component ボタンを押します。
  3. Physics 2D → BoxCollider2D(または CircleCollider2D など)を追加します。
  4. 追加した Collider2D の設定:
    • Is Trigger にチェックを入れて、トリガーにします。
    • 水中エリアの大きさ・位置を Scene ビューで調整します。
    • プレイヤーなどと衝突判定が取れるように、Group/Mask の設定も確認します。

4. LowPassFilter コンポーネントのアタッチ

  1. WaterArea ノードを選択したまま、Inspector の Add Component ボタンを押します。
  2. Custom → LowPassFilter を選択して追加します。
  3. Inspector に表示される LowPassFilter の各プロパティを設定します:
    • Target Bus: 先ほど作成した UnderwaterBus をドラッグ&ドロップで割り当てます。
    • Use Enter Exit Callbacks: 通常は ON(チェック)にします。
    • Filter On Enter: ON(チェック)。
    • Underwater Cutoff: 例として 800 を入力。
    • Underwater Resonance: 例として 1.2 を入力。
    • Transition Time: 例として 0.3 を入力(0.3 秒かけて徐々にこもる)。
    • Filter Only Specific Group:
      • プレイヤーだけが水中判定対象なら ON にし、targetGroup にプレイヤーの物理グループ番号を設定。
      • 誰でも水中エリアに入れば適用したいなら OFF のままで構いません。
    • Target Group: filterOnlySpecificGroup を ON にした場合のみ有効。プレイヤーのグループ番号を入力。
    • Debug Log: 動作確認中は ON にしてログを見やすくすると便利です。本番では OFF 推奨。

5. プレイヤー(または対象オブジェクト)の準備

  1. すでにプレイヤーノードがある場合は、それを利用します。なければ簡易的なノードを用意します:
    • Hierarchy で右クリック → Create → 2D Object → Sprite など。
  2. プレイヤーノードに Collider2D(例: CircleCollider2D)と RigidBody2D を追加して、物理的に移動できるようにします。
  3. この Collider2D の Group を、LowPassFiltertargetGroup と一致させると、プレイヤーだけが水中判定対象になります。
  4. プレイヤーノードに AudioSource を追加し、BusUnderwaterBus を指定します。
    • BGM ノードなど、既に AudioSource を持つ別ノードがある場合は、そちらの Bus を変更するだけでも構いません。

6. 動作確認

  1. Scene ビューで、プレイヤーノードが WaterArea のコライダー範囲に入れるよう、位置を調整します。
  2. Editor 上部の Play ボタンを押してゲームを実行します。
  3. プレイヤーを操作して(または自動移動でも可)、WaterArea の中に入れてみます。
  4. 以下の点を確認します:
    • プレイヤーが水中エリアに入った瞬間から transitionTime 秒かけて徐々に音がこもる。
    • 水中エリアから出ると、同様に transitionTime 秒かけて元のクリアな音に戻る。
    • Debug Log を ON にしている場合、Console に
      • [LowPassFilter] onTriggerEnter: ...
      • [LowPassFilter] onTriggerExit: ...
      • [LowPassFilter] _setUnderwater: ...

      といったログが出力される。

  5. 必要に応じて、以下を調整して好みの水中サウンドに仕上げます:
    • Underwater Cutoff: 値を下げるほど高音が削られ、よりこもった印象に。
    • Underwater Resonance: 少し上げると水中独特の「モワッ」とした感じが出ます。
    • Transition Time: 短くするとパッと切り替わる印象、長くするとゆっくり潜っていく感じになります。

まとめ

この LowPassFilter コンポーネントは、

  • 水中エリア用のノードにアタッチするだけで、
  • 指定した AudioBus に対して、
  • 水中への出入りに合わせてローパスフィルタを自動で適用・解除

できる、完全に独立した汎用スクリプトです。

AudioBus ベースで制御しているため、

  • BGM だけ水中効果をかける
  • 環境音だけ水中効果をかける
  • 特定のキャラクターのボイスだけ水中効果をかける

といった使い分けも、Bus の割り当てを変えるだけで柔軟に行えます。

また、

  • Collider2D のグループ指定で対象を絞り込める
  • transitionTime によるフェードイン・フェードアウト
  • 元の AudioBus 設定を自動保存・復元

といった防御的な設計により、既存プロジェクトにも安全に組み込める構成になっています。

水中ステージや、水たまり/池/海などの演出を入れたいときは、このコンポーネントを水中エリアのノードにアタッチし、AudioBus を割り当てるだけで、「入った瞬間に音がこもる」没入感の高いサウンド演出を簡単に追加できます。ぜひ自分のゲームの世界観に合わせて、カットオフやレゾナンスを調整してみてください。