【Cocos Creator】アタッチするだけ!ImpactThud (衝突音)の実装方法【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】ImpactThudの実装:RigidBodyの衝突強度に応じて「ドン」という効果音を自動再生する汎用スクリプト

このガイドでは、RigidBody が何かにぶつかったとき、その衝撃の強さ(速度)に応じて音量を自動調整して「ドン」という効果音を鳴らすコンポーネントを実装します。
ノードに ImpactThud.ts をアタッチするだけで動作し、他のカスタムスクリプトには一切依存しません。

物理パズルやアクションゲームで、「強くぶつかったときは大きな音・弱い接触では小さな音」といった自然な SE 演出を簡単に導入できるようになります。


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

機能要件の整理

  • このコンポーネントをアタッチしたノードに RigidBody / RigidBody2D があり、衝突イベントが発生したときに音を鳴らす。
  • 衝突の「強さ」に応じて、音量を 0〜1 の範囲で自動調整する。
  • ごく弱い接触(カツッ程度)は無音、あるいはほとんど聞こえないようにできる。
  • 3D / 2D のどちらでも使えるように、RigidBody と RigidBody2D の両方に対応する。
  • 音の再生には AudioSource を使用し、同じノードにアタッチされた AudioSource を自動取得する。
  • 外部の GameManager やシングルトンなどには依存せず、このスクリプト単体で完結させる。
  • 必要なコンポーネントが見つからない場合は エラーログを出して動作を安全に中断する。

衝撃強度と音量の関係

衝突イベントから得られる情報(3D と 2D で異なる)を、「衝撃強度」→「0〜1 の音量」に変換します。

  • 3D 物理IContactEquation から getImpactVelocityAlongNormal() を取得し、その絶対値を衝撃強度とみなす。
  • 2D 物理Contact2DType.BEGIN_CONTACT のときに、RigidBody2D.linearVelocity の長さ(length())を衝撃強度の近似値とする。

その上で、次のようなパラメータで音量を決定します。

  • minImpact:これ未満の衝撃は無音(もしくは限りなく小さい音)
  • maxImpact:これ以上の衝撃は最大音量(1.0)
  • 実測値を [minImpact, maxImpact] にクランプし、0〜1 に正規化して volumeScale を掛ける。

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

ImpactThud コンポーネントに用意する @property 一覧と役割です。

  • use3DPhysics: boolean
    • 3D 物理(RigidBody)と 2D 物理(RigidBody2D)のどちらを使うかを指定。
    • true:3D 物理(RigidBody)を使用。
    • false:2D 物理(RigidBody2D)を使用。
  • minImpact: number
    • この値未満の衝撃は音を鳴らさない、もしくは無視する閾値。
    • 例:0.31.0 あたりから調整。
  • maxImpact: number
    • この衝撃強度以上は常に最大音量で鳴らす。
    • 例:5.010.0 など、ゲームのスケールに応じて設定。
  • volumeScale: number
    • 最終的に AudioSource に設定する音量の倍率。
    • 0〜1 の範囲で指定。1.0 でフルボリューム、0.5 で半分。
  • cooldown: number
    • 連続衝突時に「ドドドド…」と鳴りすぎるのを防ぐためのクールダウン時間(秒)。
    • この時間内に再び衝突しても音を鳴らさない。
  • enableLog: boolean
    • デバッグ用に、衝撃強度や音量をログ出力するかどうか。
    • 開発中のみ true にし、リリース時は false 推奨。

この他に、AudioSource 自体はインスペクタから設定するのではなく、同じノードにアタッチされた AudioSource を自動取得する設計にします。


TypeScriptコードの実装

以下が完成版の ImpactThud.ts です。


import {
    _decorator,
    Component,
    Node,
    RigidBody,
    RigidBody2D,
    Collider,
    Collider2D,
    IContactEquation,
    ICollisionEvent,
    Contact2DType,
    AudioSource,
    log,
    warn,
    error,
    Vec2,
} from 'cc';
const { ccclass, property } = _decorator;

/**
 * ImpactThud
 * RigidBody / RigidBody2D の衝突強度に応じて AudioSource の音量を調整して再生する汎用コンポーネント。
 *
 * このスクリプトをアタッチしたノードには、以下のコンポーネントを追加してください。
 * - 3D 物理を使う場合:
 *   - RigidBody
 *   - Collider (BoxCollider / SphereCollider など)
 *   - AudioSource (衝突音の AudioClip を設定)
 * - 2D 物理を使う場合:
 *   - RigidBody2D
 *   - Collider2D (BoxCollider2D / CircleCollider2D など)
 *   - AudioSource (衝突音の AudioClip を設定)
 */
@ccclass('ImpactThud')
export class ImpactThud extends Component {

    @property({
        tooltip: '3D物理(RigidBody)を使う場合はON、2D物理(RigidBody2D)を使う場合はOFFにします。',
    })
    public use3DPhysics: boolean = true;

    @property({
        tooltip: 'この衝撃強度未満の衝突では音を鳴らしません。(0 にすると全ての衝突で鳴ります)',
        min: 0,
    })
    public minImpact: number = 0.5;

    @property({
        tooltip: 'この衝撃強度以上の衝突では常に最大音量(=volumeScale)で鳴らします。',
        min: 0.1,
    })
    public maxImpact: number = 5.0;

    @property({
        tooltip: '最終的な音量の倍率。1.0 でフルボリューム、0.5 で半分の音量になります。',
        min: 0,
        max: 1,
    })
    public volumeScale: number = 1.0;

    @property({
        tooltip: '連続した衝突音を抑制するためのクールダウン時間(秒)。この時間内の再衝突では音を鳴らしません。',
        min: 0,
    })
    public cooldown: number = 0.05;

    @property({
        tooltip: '衝撃強度や音量などのデバッグ情報をログに出力するかどうか。',
    })
    public enableLog: boolean = false;

    private _audioSource: AudioSource | null = null;
    private _rigidBody3D: RigidBody | null = null;
    private _rigidBody2D: RigidBody2D | null = null;
    private _collider3D: Collider | null = null;
    private _collider2D: Collider2D | null = null;

    private _lastPlayTime: number = -9999;
    private _time: number = 0;

    onLoad() {
        // 毎フレームの時間を自前でカウント(cooldown 判定用)
        this._time = 0;

        // AudioSource の取得
        this._audioSource = this.getComponent(AudioSource);
        if (!this._audioSource) {
            error('[ImpactThud] AudioSource が見つかりません。このノードに AudioSource コンポーネントを追加し、衝突音の AudioClip を設定してください。');
        }

        if (this.use3DPhysics) {
            // 3D 物理用コンポーネント取得
            this._rigidBody3D = this.getComponent(RigidBody);
            this._collider3D = this.getComponent(Collider);

            if (!this._rigidBody3D) {
                error('[ImpactThud] RigidBody (3D) が見つかりません。3D物理を使用する場合、このノードに RigidBody コンポーネントを追加してください。');
            }
            if (!this._collider3D) {
                error('[ImpactThud] Collider (3D) が見つかりません。3D物理を使用する場合、このノードに BoxCollider / SphereCollider などの Collider を追加してください。');
            }

            // 3D 衝突イベント登録
            if (this._collider3D) {
                this._collider3D.on('onCollisionEnter', this._onCollisionEnter3D, this);
            }
        } else {
            // 2D 物理用コンポーネント取得
            this._rigidBody2D = this.getComponent(RigidBody2D);
            this._collider2D = this.getComponent(Collider2D);

            if (!this._rigidBody2D) {
                error('[ImpactThud] RigidBody2D が見つかりません。2D物理を使用する場合、このノードに RigidBody2D コンポーネントを追加してください。');
            }
            if (!this._collider2D) {
                error('[ImpactThud] Collider2D が見つかりません。2D物理を使用する場合、このノードに BoxCollider2D / CircleCollider2D などの Collider2D を追加してください。');
            }

            // 2D 衝突イベント登録
            if (this._collider2D) {
                this._collider2D.on(Contact2DType.BEGIN_CONTACT, this._onBeginContact2D, this);
            }
        }

        // minImpact と maxImpact の関係をチェック
        if (this.maxImpact <= this.minImpact) {
            warn('[ImpactThud] maxImpact が minImpact 以下になっています。自動的に maxImpact = minImpact + 0.1 に調整します。');
            this.maxImpact = this.minImpact + 0.1;
        }
    }

    onDestroy() {
        // イベント登録を解除(メモリリーク防止)
        if (this._collider3D) {
            this._collider3D.off('onCollisionEnter', this._onCollisionEnter3D, this);
        }
        if (this._collider2D) {
            this._collider2D.off(Contact2DType.BEGIN_CONTACT, this._onBeginContact2D, this);
        }
    }

    update(deltaTime: number) {
        // 経過時間をカウント(cooldown 判定用)
        this._time += deltaTime;
    }

    /**
     * 3D物理: 衝突開始時のコールバック
     */
    private _onCollisionEnter3D(event: ICollisionEvent) {
        if (!this._audioSource) {
            return;
        }
        if (!event || !event.contacts || event.contacts.length === 0) {
            return;
        }

        // 最初のコンタクトから衝撃強度を取得
        const contact: IContactEquation = event.contacts[0];
        const impactVelocity = Math.abs(contact.getImpactVelocityAlongNormal());
        this._handleImpact(impactVelocity, '3D');
    }

    /**
     * 2D物理: 衝突開始時のコールバック
     * 2D の Contact には直接的な衝撃速度が無いため、
     * RigidBody2D の線形速度の大きさを近似値として使用します。
     */
    private _onBeginContact2D() {
        if (!this._audioSource || !this._rigidBody2D) {
            return;
        }

        const v: Vec2 = this._rigidBody2D.linearVelocity;
        const speed = v.length();
        this._handleImpact(speed, '2D');
    }

    /**
     * 衝撃強度から音量を計算し、必要であれば AudioSource を再生する共通処理。
     */
    private _handleImpact(rawImpact: number, dimensionLabel: '2D' | '3D') {
        // クールダウン中なら何もしない
        if (this.cooldown > 0 && this._time - this._lastPlayTime < this.cooldown) {
            if (this.enableLog) {
                log(`[ImpactThud] cooldown 中のため音を再生しません。impact=${rawImpact.toFixed(3)}`);
            }
            return;
        }

        // 閾値未満なら音を鳴らさない
        if (rawImpact < this.minImpact) {
            if (this.enableLog) {
                log(`[ImpactThud] impact が minImpact 未満のため音を再生しません。impact=${rawImpact.toFixed(3)}, minImpact=${this.minImpact}`);
            }
            return;
        }

        // impact を [minImpact, maxImpact] にクランプして 0〜1 に正規化
        const clamped = Math.min(Math.max(rawImpact, this.minImpact), this.maxImpact);
        const t = (clamped - this.minImpact) / (this.maxImpact - this.minImpact); // 0〜1
        const volume = t * this.volumeScale;

        if (this.enableLog) {
            log(`[ImpactThud] ${dimensionLabel} impact=${rawImpact.toFixed(3)}, clamped=${clamped.toFixed(3)}, volume=${volume.toFixed(3)}`);
        }

        // volume がほぼ 0 なら再生しない
        if (volume <= 0.001) {
            return;
        }

        // AudioSource に音量を設定して再生
        this._audioSource.volume = volume;

        // すでに再生中でも「ドン」を重ねたい場合は play()、重ねたくない場合は playOneShot を使うなど好みで調整
        // ここでは「一発鳴らす」イメージとして play() を使用
        this._audioSource.play();

        // 最終再生時刻を更新
        this._lastPlayTime = this._time;
    }
}

コードのポイント解説

  • onLoad()
    • AudioSource / RigidBody / Collider / RigidBody2D / Collider2D を getComponent で取得。
    • 見つからない場合は error() でエラーログを出し、何が不足しているかを明示。
    • use3DPhysics に応じて、3D か 2D の衝突イベントを登録。
    • minImpactmaxImpact の関係が逆転していた場合は自動補正。
  • update(deltaTime)
    • 内部カウンタ _time を増加させ、cooldown 判定に使用。
    • Time クラスに依存せず、このコンポーネント単体で時間管理を完結させています。
  • _onCollisionEnter3D()
    • 3D 物理の衝突イベントから IContactEquation を取得。
    • getImpactVelocityAlongNormal() の絶対値を衝撃強度として _handleImpact() に渡します。
  • _onBeginContact2D()
    • 2D の Contact には直接の衝撃速度が無いため、RigidBody2D.linearVelocity.length() を衝撃強度の近似として使用。
  • _handleImpact()
    • cooldown 中ならログを出して音を鳴らさない。
    • rawImpactminImpact 未満なら無視。
    • rawImpact[minImpact, maxImpact] にクランプ → 0〜1 に正規化 → volumeScale を乗算して最終音量を決定。
    • AudioSource.volume に設定し、play() で再生。
    • デバッグ時は enableLogtrue にして衝撃強度と音量を確認可能。

使用手順と動作確認

1. TypeScript スクリプトの作成

  1. エディタ左下の Assets パネルで、スクリプトを置きたいフォルダを選択します(例:assets/scripts)。
  2. フォルダ上で右クリック → CreateTypeScript を選択します。
  3. 新規スクリプトの名前を ImpactThud.ts に変更します。
  4. ダブルクリックして開き、先ほどの ImpactThud のコード全文を貼り付けて保存します。

2. テスト用ノードの作成(3D 物理の場合)

  1. Hierarchy パネルで右クリック → Create3D ObjectCube などを選択し、テスト用の 3D オブジェクトを作成します。
  2. 作成したノードを選択し、Inspector を確認します。
  3. Add Component ボタンを押して、以下のコンポーネントを追加します。
    • PhysicsRigidBody
    • PhysicsBoxCollider(または他の 3D Collider)
    • AudioAudioSource
  4. AudioSource の Clip プロパティに、「ドン」という音の AudioClip を設定します。
  5. 同じノードで Add ComponentCustomImpactThud を選択してアタッチします。
  6. Inspector 上の ImpactThud のプロパティを設定します。
    • Use 3D Physics:チェックを ON(デフォルトで ON のはず)
    • Min Impact:例として 0.5
    • Max Impact:例として 6.0
    • Volume Scale1.0
    • Cooldown0.050.1
    • Enable Log:最初は true にして挙動を確認すると便利です。

3. テスト用ノードの作成(2D 物理の場合)

  1. Project Settings → Feature → Physics で 2D 物理が有効になっていることを確認します。
  2. Hierarchy パネルで右クリック → Create2D ObjectSprite などを選択し、テスト用の 2D オブジェクトを作成します。
  3. 作成したノードを選択し、Inspector を確認します。
  4. Add Component ボタンを押して、以下のコンポーネントを追加します。
    • Physics 2DRigidBody2D
    • Physics 2DBoxCollider2D(または他の 2D Collider)
    • AudioAudioSource
  5. AudioSource の Clip プロパティに、「ドン」という音の AudioClip を設定します。
  6. 同じノードで Add ComponentCustomImpactThud を選択してアタッチします。
  7. Inspector 上の ImpactThud のプロパティを設定します。
    • Use 3D Physics:チェックを OFF(2D 物理を使うため)
    • Min Impact:例として 1.0(2D では速度ベースなのでやや大きめに)
    • Max Impact:例として 10.0
    • Volume Scale1.0
    • Cooldown0.050.1
    • Enable Log:必要に応じて true

4. シーン内に当たり判定用の床を用意する

衝突を起こすために、床や壁となるオブジェクトを用意します。

3D の場合

  1. Hierarchy で右クリック → Create3D ObjectPlane を作成し、「Floor」などの名前にします。
  2. Inspector で Add ComponentPhysicsBoxCollider を追加します。
  3. 動かない床にしたい場合は RigidBody は不要ですが、「衝突相手」として Collider は必須です。

2D の場合

  1. Hierarchy で右クリック → Create2D ObjectSprite を作成し、「Ground」などの名前にします。
  2. Inspector で Add ComponentPhysics 2DBoxCollider2D を追加します。
  3. 静的な床にしたい場合は RigidBody2D は付けなくても衝突判定は行われます(ただしプロジェクト設定による)。必要に応じて RigidBody2D(Type: Static) を付けても構いません。

5. 再生して動作確認

  1. ImpactThud を付けたオブジェクトを、床より上に配置しておきます。
  2. 上部の Play ボタンを押してゲームを再生します。
  3. 再生が始まると、重力でオブジェクトが落下し、床と衝突したタイミングで「ドン」という音が鳴るはずです。
  4. より高い位置から落とすと衝撃が大きくなり、音量も大きくなっているか確認します。
  5. Inspector で Min Impact / Max Impact / Volume Scale / Cooldown を調整しながら、好みのフィーリングになるように調整してください。
  6. Enable Log を ON にしている場合は、Console に衝撃強度と音量が表示されます。値を見ながら閾値を詰めると調整しやすくなります。

まとめ

ImpactThud コンポーネントは、

  • RigidBody / RigidBody2D の衝突イベントをフックして、
  • 衝撃強度(速度)を測り、
  • それを 0〜1 の音量にマッピングして AudioSource を再生する、

という一連の処理を 1 つのスクリプトに完結させた汎用コンポーネントです。

このコンポーネントを使うことで、

  • 箱、岩、キャラクターなど「ぶつかるもの」すべてに簡単に衝突音を付けられる。
  • 音量調整ロジックを毎回書かずに済み、ゲーム全体で一貫したフィードバックを実現できる。
  • 他のカスタムスクリプトに依存しないため、どのプロジェクトにもコピペで持ち込んで再利用しやすい。

応用として、

  • 衝撃強度に応じて 異なる AudioClip を再生する(小さい衝撃は「コトッ」、大きい衝撃は「ガシャーン」など)。
  • 衝突相手のタグやグループを見て、床・壁・敵などで音を変える
  • 音量だけでなく、パーティクルやスクリーンシェイクの強さにも同じ衝撃強度を流用する。

といった拡張も容易です。まずはこの基本形をプロジェクトに組み込んで、物理挙動に「気持ちよさ」を足してみてください。

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をコピーしました!