【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。 - プロジェクトでどちらか一方しか使わない場合、デフォルト値を固定しておくとよいです。
- 2D物理(RigidBody2D + Collider2D)を使う場合は
- 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側イベント
onPostSolveでcontact.getImpulse()からnormalImpulsesを合計し、インパクト値を算出。- 算出したインパクトを
_handleImpactに渡して共通処理へ。
- 3D側イベント
onCollisionEnter/onCollisionStayでIContactEquationのリストを受け取り、各ContactEquationのgetImpulse()を合計してインパクト値とする。- インパクト値を
_handleImpactに渡す。
- _handleImpact
- クールダウン時間内であれば再生しない。
minImpact未満なら再生しない。minImpact〜maxImpactを 0〜1 に正規化し、maxVolumeを掛けて最終音量を算出。audioSource.playOneShotで、他の音量設定を壊さずに一度だけ音を再生。debugLogが true の場合、インパクト値と音量をログ出力。
使用手順と動作確認
1. スクリプトファイルの作成
- エディタ左下の Assets パネルで、スクリプトを置きたいフォルダ(例:
assets/scripts)を選択します。 - フォルダ上で右クリック → Create → TypeScript を選択します。
- 新規スクリプトの名前を ImpactSound.ts に変更します。
- 作成された
ImpactSound.tsをダブルクリックし、エディタ(VSCode など)で開きます。 - 中身をすべて削除し、前節の
ImpactSoundコードをそのまま貼り付けて保存します。
2. テスト用ノードの準備(2D物理の例)
ここでは 2D 物理(RigidBody2D)を例に説明します。3D の場合も流れはほぼ同じです。
- 上部メニューから File → New Scene で新しいシーンを作成するか、既存シーンを開きます。
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、テスト用のスプライトノードを作成します。
- 名前を
TestBoxなどにしておくと分かりやすいです。
- 名前を
- Inspector で
TestBoxノードを選択し、Add Component ボタンをクリックします。 - Physics 2D → RigidBody2D を追加します。
- Body Type は
Dynamicにしておきます。
- Body Type は
- 同様に Add Component → Physics 2D → BoxCollider2D を追加します。
- Sprite のサイズに合わせてコライダーが自動調整されますが、必要に応じて
Sizeを調整します。
- Sprite のサイズに合わせてコライダーが自動調整されますが、必要に応じて
- さらに Add Component → Audio → AudioSource を追加します。
- Clip に、衝突音として使いたい
.mp3 / .wav / .oggなどの AudioClip を指定します。 Play On Awakeは OFF にしておきます(衝突時だけ鳴らしたいため)。Loopも OFF にします。
- Clip に、衝突音として使いたい
- 最後に、Add Component → Custom → ImpactSound を選択してアタッチします。
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. 衝突相手(床など)の準備
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、床用のノードを作成します。
- 名前を
Groundにして、シーン下部に配置し、横長にスケールして床っぽくします。 - Inspector で
Groundを選択し、Add Component → Physics 2D → RigidBody2D を追加します。Body Typeを Static に設定します(動かない床)。
- Add Component → Physics 2D → BoxCollider2D を追加し、床全体を覆うようにサイズを調整します。
5. 再生して動作確認(2D)
- シーン上で
TestBoxを少し高い位置に移動させ、Play ボタンでゲームを再生します。 TestBoxが落下してGroundに衝突した瞬間に、衝突音が鳴ることを確認します。- Console パネルに
[ImpactSound] 2D impact=..., volume=...というログが出ていれば、インパクト値と音量の計算も正常です。 - 衝突音が小さすぎる / 大きすぎる場合:
- minImpact を下げると、より弱い衝突でも鳴るようになります。
- maxImpact を下げると、中程度の衝突でも最大音量に近づきます。
- maxVolume で全体の音量上限を調整します。
- 調整が終わったら debugLog を
falseにしてログ出力を止めておくとよいです。
6. 3D物理での使用手順
3D でも基本は同じですが、コンポーネントとプロパティが少し変わります。
- Hierarchy パネルで右クリック → Create → 3D Object → Cube を選択し、テスト用の立方体を作成します(例:
TestCube)。 - Inspector で
TestCubeを選択し、Add Component → Physics → RigidBody を追加します。 - 同様に Add Component → Physics → BoxCollider を追加します。
- Add Component → Audio → AudioSource を追加し、2D のときと同様に Clip や Play On Awake を設定します。
- Add Component → Custom → ImpactSound を追加します。
- ImpactSound の use2DPhysics を OFF にします(3D 用)。
- 床となる
Ground3Dを Create → 3D Object → Plane で作成し、RigidBody(Static) と BoxCollider を追加します。 - 再生して、
TestCubeが Plane に落下して衝突したときに音が鳴るか確認します。
まとめ
本記事では、Cocos Creator 3.8.7 / TypeScript で、ImpactSound という汎用コンポーネントを実装しました。
- RigidBody(2D/3D)を持つ任意のノードにアタッチするだけで、衝突の強さに応じた効果音が鳴る。
- 感度(
minImpact / maxImpact)、音量上限(maxVolume)、クールダウン(cooldown)をインスペクタから調整可能。 - AudioSource が見つからない・RigidBody/Collider がないなどのケースでは、警告ログを出して安全に失敗する防御的な実装。
- 外部の GameManager やシングルトンに一切依存せず、このスクリプト1本だけをプロジェクトに追加すれば使える完全独立コンポーネント。
このコンポーネントをベースに、
- 「材質ごとに別の音を鳴らす」(タグやグループで分岐)
- 「同時に複数のオブジェクトが衝突したときのミックス調整」
- 「速度ベースで音程(Pitch)も変える」
といった拡張も簡単に行えます。
物理挙動のあるゲームで「当たりの気持ちよさ」を上げたいときに、まずこの ImpactSound をポンとアタッチしてみてください。音のチューニングだけで、ゲーム全体のクオリティがぐっと上がります。




