Godot 4で物理挙動を使ったゲームを作っていると、RigidBody がぶつかったときに「それっぽい音」を鳴らしたい場面って多いですよね。
でも、毎回こういう実装をしていると、だんだんツラくなってきます。
- 各 RigidBody ごとにスクリプトを継承して「_integrate_forces」を書く
- 衝突検知ロジックをコピペしまくる
- 音量調整ロジックもコピペして、あとで一括調整が地獄
さらに、「プレイヤー用RigidBody」「木箱用RigidBody」「岩用RigidBody」…みたいにクラスを増やしていくと、
クラス継承ツリーもノードツリーもどんどん深くなって、メンテが大変になっていきます。
そこで今回は、「継承じゃなくてコンポーネントを1個ポンと付けるだけ」で、
衝突の強さに応じた「ドン」音を鳴らせるコンポーネント ImpactThud を用意しました。
【Godot 4】物理衝突に“重さ”を足す!「ImpactThud」コンポーネント
ImpactThud は、RigidBody3D / RigidBody2D にアタッチして使う「衝突音コンポーネント」です。
衝突時の相対速度(インパルスの簡易版)をもとに、音量を 0〜1 の範囲で自動スケーリングしてくれます。
- 軽くコツン → 小さい音
- 勢いよくドン! → 大きい音
という挙動を、RigidBody 本体のスクリプトを一切いじらずに実現できます。
フルコード(GDScript / Godot 4)
extends Node
class_name ImpactThud
## RigidBody が衝突したとき、その衝撃の強さに応じて
## 「ドン」音を再生するコンポーネント。
##
## - RigidBody2D / RigidBody3D のどちらにも対応
## - 衝突ごとに自動で音量スケーリング
## - 最低速度以下の衝突は無音(小さなガタガタ音をカット)
@export_group("基本設定")
## 衝突音として再生する AudioStream。
## 例: 単発の「ドン」という効果音。
@export var thud_stream: AudioStream
## 衝突音を再生する AudioStreamPlayer ノード。
## 未設定の場合、実行時に自動生成して自分の子として追加します。
@export var audio_player: AudioStreamPlayer
## どの Rigidbody ノードを監視するか。
## 未設定の場合、親ノードが RigidBody2D / RigidBody3D なら自動でそれを使用します。
@export var target_body: Node
@export_group("感度・スケーリング")
## この速度(相対速度)未満の衝突は無視します。
## 小さなガタガタ音を拾わないためのしきい値。
@export var min_impact_speed: float = 2.0
## この速度以上の衝突は、最大音量(= max_volume_db)で再生します。
## それより小さい速度は線形補間で音量を決めます。
@export var max_impact_speed: float = 20.0
## 実際に再生する最大音量(dB)。
## 0dB がフル音量。-6dB などにすると控えめになります。
@export_range(-40.0, 0.0, 0.1, "or_greater") var max_volume_db: float = 0.0
## 同じフレームで複数衝突があったときに、
## 「一番大きい衝突だけ鳴らすか?」のフラグ。
@export var only_loudest_per_frame: bool = true
@export_group("デバッグ")
## 衝突ログをコンソールに出すかどうか。
@export var debug_print: bool = false
# 内部状態
var _is_2d: bool = true
var _last_frame_id: int = -1
var _loudest_volume_this_frame: float = -1000.0
func _ready() -> void:
_resolve_target_body()
_resolve_audio_player()
_detect_dimension()
_connect_body_signals()
func _process(_delta: float) -> void:
# フレームが変わったら、そのフレーム用の最大音量記録をリセット
if Engine.get_frames_drawn() != _last_frame_id:
_last_frame_id = Engine.get_frames_drawn()
_loudest_volume_this_frame = -1000.0
func _resolve_target_body() -> void:
# target_body が未設定なら、親ノードから自動推定
if target_body == null:
if owner is RigidBody2D or owner is RigidBody3D:
target_body = owner
elif get_parent() is RigidBody2D or get_parent() is RigidBody3D:
target_body = get_parent()
if target_body == null:
push_warning("[ImpactThud] target_body が設定されていません。RigidBody2D / RigidBody3D を指定してください。")
func _resolve_audio_player() -> void:
if audio_player == null:
# まだ AudioStreamPlayer が無ければ自動生成
# 2D/3D によって適切なプレイヤーを作る
if _is_body_3d():
audio_player = AudioStreamPlayer3D.new()
else:
audio_player = AudioStreamPlayer2D.new()
audio_player.name = "ImpactThudPlayer"
add_child(audio_player)
if thud_stream:
audio_player.stream = thud_stream
func _detect_dimension() -> void:
# 2D か 3D かを target_body から判定
if target_body is RigidBody3D:
_is_2d = false
else:
_is_2d = true
func _is_body_3d() -> bool:
return target_body is RigidBody3D
func _connect_body_signals() -> void:
if target_body == null:
return
# Godot 4 では、RigidBody2D / 3D に "body_entered" などのシグナルがありますが
# 衝突の「強さ」を取りたいので、_integrate_forces を使って
# Contact 情報から相対速度を計算する方法をとります。
#
# ここでは、PhysicsDirectBodyState の更新を受け取るために
# "body_shape_entered" を利用しつつ、速度差からインパクトを推定します。
#
# ただし、簡易実装として _physics_process 内で前フレーム速度との差分から
# 「急減速 = 衝突」とみなす方式もあります。
# 今回は簡易かつ汎用性の高い「速度差方式」でいきます。
if target_body.has_method("get_linear_velocity"):
# 速度監視のために _physics_process を有効化
set_physics_process(true)
else:
push_warning("[ImpactThud] target_body は RigidBody ではないようです。衝突音は動作しません。")
var _prev_velocity: Vector3 = Vector3.ZERO
var _prev_velocity_2d: Vector2 = Vector2.ZERO
func _physics_process(_delta: float) -> void:
if target_body == null:
return
if _is_body_3d():
var body := target_body as RigidBody3D
var v: Vector3 = body.linear_velocity
var impact_speed := (v - _prev_velocity).length()
_prev_velocity = v
_maybe_play_thud(impact_speed)
else:
var body2d := target_body as RigidBody2D
var v2: Vector2 = body2d.linear_velocity
var impact_speed_2d := (v2 - _prev_velocity_2d).length()
_prev_velocity_2d = v2
_maybe_play_thud(impact_speed_2d)
func _maybe_play_thud(impact_speed: float) -> void:
# しきい値未満なら無視
if impact_speed < min_impact_speed:
return
# impact_speed を 0〜1 に正規化
var t := clamp((impact_speed - min_impact_speed) / max(0.001, max_impact_speed - min_impact_speed), 0.0, 1.0)
# 0〜1 を -40dB〜max_volume_db にマッピング(小さい衝突も少しは聞こえるようにする)
var volume_db := lerp(-40.0, max_volume_db, t)
if only_loudest_per_frame:
# このフレームで既にもっと大きな音が鳴っていればスキップ
if volume_db <= _loudest_volume_this_frame:
return
_loudest_volume_this_frame = volume_db
if debug_print:
print("[ImpactThud] impact_speed=", impact_speed, " volume_db=", volume_db)
_play_thud(volume_db)
func _play_thud(volume_db: float) -> void:
if audio_player == null:
return
if thud_stream and audio_player.stream != thud_stream:
audio_player.stream = thud_stream
audio_player.volume_db = volume_db
# 連続で鳴らすときのために、再生中なら一度止める
if audio_player.playing:
audio_player.stop()
audio_player.play()
※ 上記は「急な速度変化 = 衝突」とみなす簡易版です。
より正確に「接触相手との相対速度」を取りたい場合は、_integrate_forces で Contact 情報を読む方式に差し替えることもできます。
使い方の手順
① 効果音(ドン音)を用意する
- WAV / OGG などで、単発の「ドン」「ゴン」といった衝突音を1つ用意します。
- Godot の FileSystem にインポートしておきましょう(例:
res://audio/thud.wav)。
② ImpactThud.gd をプロジェクトに追加する
- 上のスクリプトを
res://components/ImpactThud.gdなどに保存します。 - Godot が自動的に
class_name ImpactThudを認識するので、ノード追加ダイアログから直接追加できるようになります。
③ RigidBody にコンポーネントをアタッチする
たとえば、転がる木箱のシーンがこんな構成だとします:
Crate (RigidBody3D) ├── MeshInstance3D ├── CollisionShape3D └── ImpactThud (Node)
あるいは 2D のプレイヤーなら:
Player (RigidBody2D) ├── Sprite2D ├── CollisionShape2D └── ImpactThud (Node)
手順:
- 対象のシーンを開く(例:
Crate.tscn)。 - ルートの
RigidBody3D(またはRigidBody2D)の子として- +ボタン → Node を追加 → ImpactThud を選択して追加。
- インスペクタで
ImpactThudを選択し、以下を設定:- thud_stream に「ドン音」の AudioStream を指定
- target_body は空でOK(親が RigidBody なら自動で拾います)
- 音が小さすぎる/大きすぎる場合は
- min_impact_speed と max_impact_speed を調整
- max_volume_db を -3dB 〜 0dB あたりで調整
④ 実際のシーンで試す
例として、物理パズルステージ:
Level01 (Node3D)
├── Floor (StaticBody3D)
│ └── CollisionShape3D
├── Crate01 (RigidBody3D)
│ ├── MeshInstance3D
│ ├── CollisionShape3D
│ └── ImpactThud
└── Crate02 (RigidBody3D)
├── MeshInstance3D
├── CollisionShape3D
└── ImpactThud
- ゲームを再生して、木箱を床に落としてみましょう。
- 高い位置から落とすと大きく「ドン」、低い位置からだと小さく「コトン」と鳴るはずです。
- 木箱が連続でガタガタ揺れるようなシーンでも、
min_impact_speedをうまく調整すると、不快な連続音をかなり抑えられます。
同じコンポーネントを、そのままプレイヤーや敵キャラにも貼るだけでOKです:
Enemy (RigidBody2D) ├── Sprite2D ├── CollisionShape2D └── ImpactThud
これで、敵が壁にぶつかったり、プレイヤーとぶつかったときも、
衝突の強さに応じた「ドン」音が自動で鳴るようになります。
メリットと応用
ImpactThud をコンポーネントとして切り出すことで、かなり嬉しいポイントがあります。
1. RigidBody のスクリプトを汚さない
本来なら、こんな感じで各 RigidBody にロジックを書きがちです:
# こういうのを各RigidBodyに書きたくない…
func _physics_process(delta: float) -> void:
# 速度差から衝突を検知して音を鳴らす処理…
これをやると、「物理挙動のロジック」と「サウンド演出のロジック」がベッタリ結合してしまいます。
ImpactThud を使えば、物理は物理、サウンドはサウンドでキレイに分離できますね。
2. どんな RigidBody にも“後付け”できる
既に出来上がっているシーンに対しても、子ノードとして ImpactThud を追加するだけで衝突音を付けられます。
- 既存のプレイヤーシーンに「ちょっとだけ重みのある音を足したい」
- ステージ中の木箱・岩・樽などに一括で衝突音を付けたい
こういうケースで、継承ツリーをいじらずに済むのはかなり大きいです。
3. レベルデザイン時の視認性が高い
シーンツリーを見ただけで、
Crate (RigidBody3D) ├── MeshInstance3D ├── CollisionShape3D └── ImpactThud
と、「このオブジェクトは衝突音を持っている」と一目で分かります。
深い継承ツリーのどこかにサウンド処理が埋まっている…という状態より、遥かにデバッグしやすいですね。
4. パラメータをオブジェクトごとに変えやすい
例えば:
- 木箱:
min_impact_speed = 2.0/ 柔らかく小さめの音 - 鉄球:
min_impact_speed = 0.5,max_volume_db = 0.0/ 重くて大きな音
など、同じコンポーネントでもパラメータだけ変えて「キャラ付け」できるのもコンポーネント指向の強みです。
改造案:接触したマテリアルごとに音を変える
もう一歩踏み込むと、「床が金属なら金属音」「床が木なら木箱音」といった
接触相手のマテリアルごとに音を変えることもできます。
例えば、こんなヘルパー関数を追加して、将来的に _maybe_play_thud() から呼ぶようにしても良いですね:
## (改造案)接触した相手のマテリアルタグに応じて
## 再生する AudioStream を切り替える例。
func _choose_stream_by_material(other: Node) -> AudioStream:
# 相手ノードに "material_tag" という export 文字列がある想定
if not other or not other.has_meta("material_tag"):
return thud_stream # デフォルト
var tag: String = str(other.get_meta("material_tag"))
match tag:
"metal":
return preload("res://audio/thud_metal.wav")
"wood":
return preload("res://audio/thud_wood.wav")
"stone":
return preload("res://audio/thud_stone.wav")
_:
return thud_stream
このように、ImpactThud 自体を1つの「衝突サウンド・プラットフォーム」として育てていくと、
プロジェクト全体の物理演出が一気にリッチになります。
しかも、どのオブジェクトにも「コンポーネントをポン付けするだけ」で広げていけるので、
継承地獄にハマらずに済むのが良いところですね。
ぜひ自分のプロジェクト向けに、ImpactThud をベースにしたオリジナル衝突音コンポーネントを育ててみてください。




