【Cocos Creator】アタッチするだけ!ImpactSound (衝突音)の実装方法【TypeScript】

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

【Cocos Creator 3.8】ImpactSoundの実装:アタッチするだけで「衝突の強さに応じた効果音再生」を実現する汎用スクリプト

このコンポーネントは、RigidBody(2D/3D問わず)を持つノードにアタッチするだけで衝突の強さ(インパルス量)に応じた音量で効果音を鳴らすための汎用スクリプトです。
2Dアクション・3D物理パズルなどで、物が強くぶつかった時だけ大きな音を鳴らし、弱い接触では小さい音か無音にするといった演出を、他のスクリプトに依存せずこの1本だけで実現できます。


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

1. 要件の整理

  • このコンポーネントをアタッチしたノードのRigidBody(2D/3D対応)が、何かに衝突した時に音を鳴らす。
  • 衝突の「強さ(インパルス)」に応じて、音量を自動スケーリングする。
  • 弱い衝突は無音または小音量強い衝突は大きな音量になるように、閾値とスケールをインスペクタで調整できる。
  • 音声再生には Cocos Creator 標準の AudioSource を使用し、AudioSource が見つからない場合は警告を出す
  • 外部スクリプト(GameManager など)には一切依存せず、このコンポーネント単体で完結させる。
  • 2D物理(RigidBody2D + Collider2D)と 3D物理(RigidBody + Collider)両方で使えるように、防御的に実装する。

2. 衝突強度から音量へのマッピング仕様

物理エンジンから得られる衝突情報には、「衝突インパルス(衝撃量)」に相当する値があります。
2D と 3D で API が少し異なりますが、概ね以下のような値を利用します。

  • 2D: ICollision2D.contacts[i].normalImpulse の合計など
  • 3D: IContactEquation.getImpulse() から取得(Creator 3.8 では ContactEquation 経由)

本コンポーネントでは、衝突強度 impact から音量 volume への変換を以下のように定義します。

if impact < minImpact:   // 閾値未満 → 再生しない
    再生しない
else:
    // minImpact 〜 maxImpact を 0.0 〜 1.0 に正規化して、0〜maxVolume にクランプ
    t = (impact - minImpact) / (maxImpact - minImpact)
    volume = clamp(t, 0, 1) * maxVolume

これにより、小さな衝突は無音〜小音量、大きな衝突は最大音量といった自然な音量変化が可能になります。

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

本コンポーネント ImpactSound に用意する @property は以下の通りです。

  • use2DPhysics: boolean
    • 2D物理(RigidBody2D + Collider2D)を使う場合は true
    • 3D物理(RigidBody + Collider)を使う場合は false
    • プロジェクトでどちらか一方しか使わない場合、デフォルト値を固定しておくとよいです。
  • audioSource: AudioSource | null
    • 衝突音を再生するための AudioSource コンポーネント。
    • 未設定の場合は、自ノード上の AudioSource を自動取得しようと試みる。
    • 最終的に見つからなければ、エラーログを出して何も再生しない。
  • minImpact: number
    • この値未満の衝撃では音を鳴らさない「感度のしきい値」。
    • 例: 0.1〜1.0 程度から調整するとよい。
  • maxImpact: number
    • この値以上の衝撃は、常に maxVolume で再生される上限値。
    • minImpact より大きい値に設定する必要がある。
    • 例: 5〜20 程度から調整。
  • maxVolume: number
    • 衝突が最大強度の時の音量(0〜1)。
    • デフォルトは 1.0(フルボリューム)。
  • cooldown: number
    • 連続衝突で効果音が鳴りすぎるのを防ぐためのクールダウン時間(秒)。
    • この秒数以内に再度衝突しても、新しい音は鳴らさない。
    • 例: 0.05〜0.2 秒程度。
  • debugLog: boolean
    • 有効にすると、衝突のインパクト値や計算された音量を console.log に出力する。
    • チューニング時だけオンにし、本番ではオフにする想定。

TypeScriptコードの実装

以下が完成した ImpactSound コンポーネントの全コードです。


import { _decorator, Component, Node, AudioSource, clamp01, log, warn, error } from 'cc';
import { RigidBody2D, Collider2D, IPhysics2DContact, ICollision2D } from 'cc';
import { RigidBody, Collider, IContactEquation, PhysicsSystem } from 'cc';

const { ccclass, property } = _decorator;

@ccclass('ImpactSound')
export class ImpactSound extends Component {

    @property({
        tooltip: '2D物理(RigidBody2D + Collider2D)を使用する場合はON。\n3D物理(RigidBody + Collider)の場合はOFF。'
    })
    public use2DPhysics: boolean = true;

    @property({
        type: AudioSource,
        tooltip: '衝突音を再生するAudioSource。\n未設定の場合、このノード上から自動取得を試みます。'
    })
    public audioSource: AudioSource | null = null;

    @property({
        tooltip: 'この値未満の衝撃では音を鳴らしません(感度のしきい値)。'
    })
    public minImpact: number = 0.5;

    @property({
        tooltip: 'この値以上の衝撃は常に最大音量(maxVolume)で再生されます。\nminImpactより大きい値を設定してください。'
    })
    public maxImpact: number = 10.0;

    @property({
        tooltip: '最大音量(0〜1)。衝突が最大強度のときの再生音量です。'
    })
    public maxVolume: number = 1.0;

    @property({
        tooltip: '連続した衝突音を抑制するクールダウン時間(秒)。\nこの秒数以内の連続衝突では新たに音を鳴らしません。'
    })
    public cooldown: number = 0.1;

    @property({
        tooltip: 'ONにすると、衝突インパクト値や音量をログ出力します(デバッグ用)。'
    })
    public debugLog: boolean = false;

    private _lastPlayTime: number = -9999;

    private _rb2d: RigidBody2D | null = null;
    private _col2d: Collider2D | null = null;

    private _rb3d: RigidBody | null = null;
    private _col3d: Collider | null = null;

    onLoad() {
        // AudioSource の自動取得
        if (!this.audioSource) {
            this.audioSource = this.getComponent(AudioSource);
            if (!this.audioSource) {
                warn('[ImpactSound] AudioSource が設定されておらず、このノード上にも見つかりません。衝突音は再生されません。');
            }
        }

        // 2D / 3D それぞれに必要なコンポーネントを取得
        if (this.use2DPhysics) {
            this._rb2d = this.getComponent(RigidBody2D);
            this._col2d = this.getComponent(Collider2D);

            if (!this._rb2d) {
                warn('[ImpactSound] use2DPhysics=true ですが RigidBody2D が見つかりません。このノードに RigidBody2D を追加してください。');
            }
            if (!this._col2d) {
                warn('[ImpactSound] use2DPhysics=true ですが Collider2D が見つかりません。このノードに Collider2D(BoxCollider2Dなど) を追加してください。');
            }
        } else {
            this._rb3d = this.getComponent(RigidBody);
            this._col3d = this.getComponent(Collider);

            if (!this._rb3d) {
                warn('[ImpactSound] use2DPhysics=false ですが RigidBody が見つかりません。このノードに RigidBody を追加してください。');
            }
            if (!this._col3d) {
                warn('[ImpactSound] use2DPhysics=false ですが Collider が見つかりません。このノードに Collider(BoxColliderなど) を追加してください。');
            }
        }

        // 2Dコライダーの衝突イベント登録
        if (this.use2DPhysics && this._col2d) {
            this._col2d.on('onBeginContact', this._onBeginContact2D, this);
            this._col2d.on('onEndContact', this._onEndContact2D, this);
            this._col2d.on('onPreSolve', this._onPreSolve2D, this);
            this._col2d.on('onPostSolve', this._onPostSolve2D, this);
        }

        // 3Dコライダーの衝突イベント登録
        if (!this.use2DPhysics && this._col3d) {
            this._col3d.on('onCollisionEnter', this._onCollisionEnter3D, this);
            this._col3d.on('onCollisionStay', this._onCollisionStay3D, this);
            this._col3d.on('onCollisionExit', this._onCollisionExit3D, this);
        }
    }

    onDestroy() {
        // 2Dイベント解除
        if (this._col2d) {
            this._col2d.off('onBeginContact', this._onBeginContact2D, this);
            this._col2d.off('onEndContact', this._onEndContact2D, this);
            this._col2d.off('onPreSolve', this._onPreSolve2D, this);
            this._col2d.off('onPostSolve', this._onPostSolve2D, this);
        }
        // 3Dイベント解除
        if (this._col3d) {
            this._col3d.off('onCollisionEnter', this._onCollisionEnter3D, this);
            this._col3d.off('onCollisionStay', this._onCollisionStay3D, this);
            this._col3d.off('onCollisionExit', this._onCollisionExit3D, this);
        }
    }

    // ===== 2D物理イベント =====

    private _onBeginContact2D(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
        // 実際のインパクトは onPostSolve で取得するので、ここでは何もしない
    }

    private _onEndContact2D(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
        // ここも特に処理しない
    }

    private _onPreSolve2D(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
        // ここも特に処理しない
    }

    private _onPostSolve2D(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
        if (!contact) {
            return;
        }

        // 2Dの衝突インパクトは contact.getImpulse() から取得できるが、
        // Cocos Creator 3.8 では ICollision2D 経由の normalImpulse などを使うこともできる。
        // ここでは単純に normalImpulse の合計をインパクトとみなす。
        const collision: ICollision2D | null = contact.getWorldManifold();
        // getWorldManifold() は位置/法線情報のため、実際のインパルスは contact.getImpulse() を使用
        const impulse = contact.getImpulse ? contact.getImpulse() : null;

        let impact = 0;
        if (impulse) {
            // impulse.normalImpulses は配列
            for (let i = 0; i < impulse.normalImpulses.length; i++) {
                impact += Math.abs(impulse.normalImpulses[i]);
            }
        }

        this._handleImpact(impact, '2D');
    }

    // ===== 3D物理イベント =====

    private _onCollisionEnter3D(selfCollider: Collider, otherCollider: Collider, contactPairs: IContactEquation[]) {
        this._process3DContacts(contactPairs, 'enter');
    }

    private _onCollisionStay3D(selfCollider: Collider, otherCollider: Collider, contactPairs: IContactEquation[]) {
        // 継続接触でもそこそこ大きなインパクトがあれば鳴らしたい場合はここも処理する
        this._process3DContacts(contactPairs, 'stay');
    }

    private _onCollisionExit3D(selfCollider: Collider, otherCollider: Collider) {
        // 離れたタイミングでは特に処理しない
    }

    private _process3DContacts(contactPairs: IContactEquation[], phase: 'enter' | 'stay') {
        if (!contactPairs || contactPairs.length === 0) {
            return;
        }

        let impact = 0;
        for (let i = 0; i < contactPairs.length; i++) {
            const eq = contactPairs[i];
            if (!eq) continue;

            // ContactEquation.getImpulse() でインパルス取得
            const imp = (eq as any).getImpulse ? (eq as any).getImpulse() : 0;
            impact += Math.abs(imp);
        }

        this._handleImpact(impact, '3D');
    }

    // ===== 共通:インパクト値から音量を計算して再生 =====

    private _handleImpact(rawImpact: number, dimension: '2D' | '3D') {
        if (!this.audioSource) {
            // 既に onLoad で警告しているので、ここでは何もしない
            return;
        }

        if (rawImpact <= 0) {
            return;
        }

        // クールダウンチェック
        const now = this._getTime();
        if (now - this._lastPlayTime < this.cooldown) {
            return;
        }

        // minImpact / maxImpact のバリデーション
        if (this.maxImpact <= this.minImpact) {
            warn('[ImpactSound] maxImpact は minImpact より大きい値にしてください。現在 minImpact=' +
                this.minImpact + ', maxImpact=' + this.maxImpact);
            return;
        }

        if (rawImpact < this.minImpact) {
            if (this.debugLog) {
                log(`[ImpactSound] ${dimension} impact too small: ${rawImpact.toFixed(3)} < minImpact=${this.minImpact}`);
            }
            return;
        }

        // 0〜1 に正規化してから maxVolume を掛ける
        let t = (rawImpact - this.minImpact) / (this.maxImpact - this.minImpact);
        t = clamp01(t);
        const volume = t * this.maxVolume;

        if (volume <= 0) {
            return;
        }

        if (this.debugLog) {
            log(`[ImpactSound] ${dimension} impact=${rawImpact.toFixed(3)}, volume=${volume.toFixed(3)}`);
        }

        // 実際に再生
        this.audioSource.volume = volume;
        this.audioSource.playOneShot(this.audioSource.clip, volume);

        this._lastPlayTime = now;
    }

    private _getTime(): number {
        // PhysicsSystem の時間、もしくは performance.now に近いものを使う。
        // 簡易的に PhysicsSystem の累積時間を利用。
        return PhysicsSystem.instance.accumulateTime;
    }
}

コードのポイント解説

  • onLoad
    • AudioSource がインスペクタで指定されていない場合、自ノードから自動取得。
    • use2DPhysics に応じて、RigidBody2D / Collider2D または RigidBody / Collider を取得し、存在しなければ warn で警告。
    • 2D/3D それぞれに対して、衝突イベントを登録。
  • 2D側イベント
    • onPostSolvecontact.getImpulse() から normalImpulses を合計し、インパクト値を算出。
    • 算出したインパクトを _handleImpact に渡して共通処理へ。
  • 3D側イベント
    • onCollisionEnter / onCollisionStayIContactEquation のリストを受け取り、各 ContactEquationgetImpulse() を合計してインパクト値とする。
    • インパクト値を _handleImpact に渡す。
  • _handleImpact
    • クールダウン時間内であれば再生しない。
    • minImpact 未満なら再生しない。
    • minImpactmaxImpact を 0〜1 に正規化し、maxVolume を掛けて最終音量を算出。
    • audioSource.playOneShot で、他の音量設定を壊さずに一度だけ音を再生。
    • debugLog が true の場合、インパクト値と音量をログ出力。

使用手順と動作確認

1. スクリプトファイルの作成

  1. エディタ左下の Assets パネルで、スクリプトを置きたいフォルダ(例: assets/scripts)を選択します。
  2. フォルダ上で右クリック → CreateTypeScript を選択します。
  3. 新規スクリプトの名前を ImpactSound.ts に変更します。
  4. 作成された ImpactSound.ts をダブルクリックし、エディタ(VSCode など)で開きます。
  5. 中身をすべて削除し、前節の ImpactSound コードをそのまま貼り付けて保存します。

2. テスト用ノードの準備(2D物理の例)

ここでは 2D 物理(RigidBody2D)を例に説明します。3D の場合も流れはほぼ同じです。

  1. 上部メニューから File → New Scene で新しいシーンを作成するか、既存シーンを開きます。
  2. Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、テスト用のスプライトノードを作成します。
    • 名前を TestBox などにしておくと分かりやすいです。
  3. InspectorTestBox ノードを選択し、Add Component ボタンをクリックします。
  4. Physics 2D → RigidBody2D を追加します。
    • Body Type は Dynamic にしておきます。
  5. 同様に Add ComponentPhysics 2D → BoxCollider2D を追加します。
    • Sprite のサイズに合わせてコライダーが自動調整されますが、必要に応じて Size を調整します。
  6. さらに Add ComponentAudio → AudioSource を追加します。
    • Clip に、衝突音として使いたい .mp3 / .wav / .ogg などの AudioClip を指定します。
    • Play On AwakeOFF にしておきます(衝突時だけ鳴らしたいため)。
    • Loop も OFF にします。
  7. 最後に、Add ComponentCustomImpactSound を選択してアタッチします。

3. ImpactSound プロパティの設定(2D)

TestBox ノードを選択し、Inspector の ImpactSound セクションを確認します。

  • use2DPhysics: true(デフォルトのままでOK)
  • audioSource:
    • 空欄でも、このノード上に AudioSource があれば自動取得されます。
    • 確実に指定したい場合は、TestBox ノードの AudioSource をドラッグ&ドロップしてください。
  • minImpact: 0.5 〜 1.0 くらいから試してみると良いです。
  • maxImpact: 5 〜 10 くらいから試してみます。
    • 物理挙動によって適正値が変わるので、後で調整します。
  • maxVolume: 1.0(フルボリューム)。環境によって 0.7 などにしてもよいです。
  • cooldown: 0.1 秒程度(連続接触で鳴りすぎる場合は 0.2〜0.3 に上げる)。
  • debugLog: 最初は true にして、コンソールを見ながら調整すると便利です。

4. 衝突相手(床など)の準備

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、床用のノードを作成します。
  2. 名前を Ground にして、シーン下部に配置し、横長にスケールして床っぽくします。
  3. InspectorGround を選択し、Add ComponentPhysics 2D → RigidBody2D を追加します。
    • Body TypeStatic に設定します(動かない床)。
  4. Add ComponentPhysics 2D → BoxCollider2D を追加し、床全体を覆うようにサイズを調整します。

5. 再生して動作確認(2D)

  1. シーン上で TestBox を少し高い位置に移動させ、Play ボタンでゲームを再生します。
  2. TestBox が落下して Ground に衝突した瞬間に、衝突音が鳴ることを確認します。
  3. Console パネルに [ImpactSound] 2D impact=..., volume=... というログが出ていれば、インパクト値と音量の計算も正常です。
  4. 衝突音が小さすぎる / 大きすぎる場合:
    • minImpact を下げると、より弱い衝突でも鳴るようになります。
    • maxImpact を下げると、中程度の衝突でも最大音量に近づきます。
    • maxVolume で全体の音量上限を調整します。
  5. 調整が終わったら debugLogfalse にしてログ出力を止めておくとよいです。

6. 3D物理での使用手順

3D でも基本は同じですが、コンポーネントとプロパティが少し変わります。

  1. Hierarchy パネルで右クリック → Create → 3D Object → Cube を選択し、テスト用の立方体を作成します(例: TestCube)。
  2. InspectorTestCube を選択し、Add ComponentPhysics → RigidBody を追加します。
  3. 同様に Add ComponentPhysics → BoxCollider を追加します。
  4. Add ComponentAudio → AudioSource を追加し、2D のときと同様に Clip や Play On Awake を設定します。
  5. Add ComponentCustom → ImpactSound を追加します。
  6. ImpactSounduse2DPhysicsOFF にします(3D 用)。
  7. 床となる Ground3DCreate → 3D Object → Plane で作成し、RigidBody(Static)BoxCollider を追加します。
  8. 再生して、TestCube が Plane に落下して衝突したときに音が鳴るか確認します。

まとめ

本記事では、Cocos Creator 3.8.7 / TypeScript で、ImpactSound という汎用コンポーネントを実装しました。

  • RigidBody(2D/3D)を持つ任意のノードにアタッチするだけで、衝突の強さに応じた効果音が鳴る。
  • 感度(minImpact / maxImpact)、音量上限(maxVolume)、クールダウン(cooldown)をインスペクタから調整可能。
  • AudioSource が見つからない・RigidBody/Collider がないなどのケースでは、警告ログを出して安全に失敗する防御的な実装。
  • 外部の GameManager やシングルトンに一切依存せず、このスクリプト1本だけをプロジェクトに追加すれば使える完全独立コンポーネント。

このコンポーネントをベースに、

  • 「材質ごとに別の音を鳴らす」(タグやグループで分岐)
  • 「同時に複数のオブジェクトが衝突したときのミックス調整」
  • 「速度ベースで音程(Pitch)も変える」

といった拡張も簡単に行えます。
物理挙動のあるゲームで「当たりの気持ちよさ」を上げたいときに、まずこの ImpactSound をポンとアタッチしてみてください。音のチューニングだけで、ゲーム全体のクオリティがぐっと上がります。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!