【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.3〜1.0あたりから調整。
- maxImpact: number
- この衝撃強度以上は常に最大音量で鳴らす。
- 例:
5.0〜10.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 の衝突イベントを登録。minImpactとmaxImpactの関係が逆転していた場合は自動補正。
- AudioSource / RigidBody / Collider / RigidBody2D / Collider2D を
- update(deltaTime)
- 内部カウンタ
_timeを増加させ、cooldown判定に使用。 - Time クラスに依存せず、このコンポーネント単体で時間管理を完結させています。
- 内部カウンタ
- _onCollisionEnter3D()
- 3D 物理の衝突イベントから
IContactEquationを取得。 getImpactVelocityAlongNormal()の絶対値を衝撃強度として_handleImpact()に渡します。
- 3D 物理の衝突イベントから
- _onBeginContact2D()
- 2D の Contact には直接の衝撃速度が無いため、
RigidBody2D.linearVelocity.length()を衝撃強度の近似として使用。
- 2D の Contact には直接の衝撃速度が無いため、
- _handleImpact()
cooldown中ならログを出して音を鳴らさない。rawImpactがminImpact未満なら無視。rawImpactを[minImpact, maxImpact]にクランプ → 0〜1 に正規化 →volumeScaleを乗算して最終音量を決定。AudioSource.volumeに設定し、play()で再生。- デバッグ時は
enableLogをtrueにして衝撃強度と音量を確認可能。
使用手順と動作確認
1. TypeScript スクリプトの作成
- エディタ左下の Assets パネルで、スクリプトを置きたいフォルダを選択します(例:
assets/scripts)。 - フォルダ上で右クリック → Create → TypeScript を選択します。
- 新規スクリプトの名前を ImpactThud.ts に変更します。
- ダブルクリックして開き、先ほどの
ImpactThudのコード全文を貼り付けて保存します。
2. テスト用ノードの作成(3D 物理の場合)
- Hierarchy パネルで右クリック → Create → 3D Object → Cube などを選択し、テスト用の 3D オブジェクトを作成します。
- 作成したノードを選択し、Inspector を確認します。
- Add Component ボタンを押して、以下のコンポーネントを追加します。
- Physics → RigidBody
- Physics → BoxCollider(または他の 3D Collider)
- Audio → AudioSource
- AudioSource の Clip プロパティに、「ドン」という音の AudioClip を設定します。
- 同じノードで Add Component → Custom → ImpactThud を選択してアタッチします。
- Inspector 上の ImpactThud のプロパティを設定します。
- Use 3D Physics:チェックを ON(デフォルトで ON のはず)
- Min Impact:例として
0.5 - Max Impact:例として
6.0 - Volume Scale:
1.0 - Cooldown:
0.05〜0.1 - Enable Log:最初は
trueにして挙動を確認すると便利です。
3. テスト用ノードの作成(2D 物理の場合)
- Project Settings → Feature → Physics で 2D 物理が有効になっていることを確認します。
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite などを選択し、テスト用の 2D オブジェクトを作成します。
- 作成したノードを選択し、Inspector を確認します。
- Add Component ボタンを押して、以下のコンポーネントを追加します。
- Physics 2D → RigidBody2D
- Physics 2D → BoxCollider2D(または他の 2D Collider)
- Audio → AudioSource
- AudioSource の Clip プロパティに、「ドン」という音の AudioClip を設定します。
- 同じノードで Add Component → Custom → ImpactThud を選択してアタッチします。
- Inspector 上の ImpactThud のプロパティを設定します。
- Use 3D Physics:チェックを OFF(2D 物理を使うため)
- Min Impact:例として
1.0(2D では速度ベースなのでやや大きめに) - Max Impact:例として
10.0 - Volume Scale:
1.0 - Cooldown:
0.05〜0.1 - Enable Log:必要に応じて
true。
4. シーン内に当たり判定用の床を用意する
衝突を起こすために、床や壁となるオブジェクトを用意します。
3D の場合
- Hierarchy で右クリック → Create → 3D Object → Plane を作成し、「Floor」などの名前にします。
- Inspector で Add Component → Physics → BoxCollider を追加します。
- 動かない床にしたい場合は RigidBody は不要ですが、「衝突相手」として Collider は必須です。
2D の場合
- Hierarchy で右クリック → Create → 2D Object → Sprite を作成し、「Ground」などの名前にします。
- Inspector で Add Component → Physics 2D → BoxCollider2D を追加します。
- 静的な床にしたい場合は RigidBody2D は付けなくても衝突判定は行われます(ただしプロジェクト設定による)。必要に応じて RigidBody2D(Type: Static) を付けても構いません。
5. 再生して動作確認
- ImpactThud を付けたオブジェクトを、床より上に配置しておきます。
- 上部の Play ボタンを押してゲームを再生します。
- 再生が始まると、重力でオブジェクトが落下し、床と衝突したタイミングで「ドン」という音が鳴るはずです。
- より高い位置から落とすと衝撃が大きくなり、音量も大きくなっているか確認します。
- Inspector で Min Impact / Max Impact / Volume Scale / Cooldown を調整しながら、好みのフィーリングになるように調整してください。
- Enable Log を ON にしている場合は、Console に衝撃強度と音量が表示されます。値を見ながら閾値を詰めると調整しやすくなります。
まとめ
ImpactThud コンポーネントは、
- RigidBody / RigidBody2D の衝突イベントをフックして、
- 衝撃強度(速度)を測り、
- それを 0〜1 の音量にマッピングして AudioSource を再生する、
という一連の処理を 1 つのスクリプトに完結させた汎用コンポーネントです。
このコンポーネントを使うことで、
- 箱、岩、キャラクターなど「ぶつかるもの」すべてに簡単に衝突音を付けられる。
- 音量調整ロジックを毎回書かずに済み、ゲーム全体で一貫したフィードバックを実現できる。
- 他のカスタムスクリプトに依存しないため、どのプロジェクトにもコピペで持ち込んで再利用しやすい。
応用として、
- 衝撃強度に応じて 異なる AudioClip を再生する(小さい衝撃は「コトッ」、大きい衝撃は「ガシャーン」など)。
- 衝突相手のタグやグループを見て、床・壁・敵などで音を変える。
- 音量だけでなく、パーティクルやスクリーンシェイクの強さにも同じ衝撃強度を流用する。
といった拡張も容易です。まずはこの基本形をプロジェクトに組み込んで、物理挙動に「気持ちよさ」を足してみてください。




