RigidBody で「ガシャーン!」と気持ちいい衝突音を鳴らしたいのに、毎回同じようなスクリプトを書いていませんか?
Godot 標準だと、

  • 各 RigidBody ごとに body_entered / contact_monitor / max_contacts_reported の設定をしたり
  • 衝突強度に応じて音量を変えたいけど、どこで速度差を計算するか悩んだり
  • プレイヤー、敵、オブジェクト…それぞれに似たようなコードをコピペしてメンテ地獄

みたいな「ちょっとした面倒」が積み重なりがちです。

そこで今回は、「衝突音だけ」を責務にしたコンポーネント ImpactSound を用意して、
RigidBody にポンとアタッチするだけで「衝撃力に応じた音量の衝突音」を鳴らせるようにしてみましょう。

継承ベースで Player, Enemy, BreakableCrate それぞれにロジックを書くのではなく、
「音の振る舞い」は ImpactSound コンポーネント にまとめて合成(Composition)していくスタイルですね。

【Godot 4】衝突の気持ちよさを一括管理!「ImpactSound」コンポーネント

フルコード(GDScript)


extends Node
class_name ImpactSound
"""
ImpactSound コンポーネント
親の RigidBody2D / RigidBody3D が何かに衝突したとき、
衝撃の強さに応じた音量で効果音を再生するコンポーネント。

・親には RigidBody2D か RigidBody3D を想定
・自分の子として AudioStreamPlayer(2D/3D) を持つ構成がおすすめ
"""

# === 基本設定 ===

@export var enabled: bool = true:
	set(value):
		enabled = value

@export var min_impact_speed: float = 1.0:
	## この速度差(衝突の強さ)未満では音を鳴らさない
	## 「カツン」レベルの小さな当たりを無視するための下限
	set(value):
		min_impact_speed = max(value, 0.0)

@export var max_impact_speed: float = 20.0:
	## この速度差を「最大インパクト」とみなし、ここを超えたら常に最大音量
	set(value):
		max_impact_speed = max(value, 0.01)

@export_range(0.0, 1.0, 0.01)
var min_volume: float = 0.1:
	## 鳴らすときの最小音量(0.0〜1.0)
	set(value):
		min_volume = clamp(value, 0.0, 1.0)

@export_range(0.0, 1.0, 0.01)
var max_volume: float = 1.0:
	## 最大インパクト時の音量(0.0〜1.0)
	set(value):
		max_volume = clamp(value, 0.0, 1.0)

@export var cooldown_time: float = 0.05:
	## 連続衝突で「ババババッ」と鳴りすぎないようにするクールダウン秒数
	set(value):
		cooldown_time = max(value, 0.0)

@export var random_pitch_variation: float = 0.05:
	## ピッチにランダム揺らぎを加える範囲
	## 0.05 なら 0.95〜1.05 の範囲でランダム
	set(value):
		random_pitch_variation = max(value, 0.0)

# === 高度な設定 ===

@export var use_parent_linear_velocity: bool = true:
	## true: RigidBody の linear_velocity を使って衝撃を推定
	## false: contact の normal, local_velocity から計算(3D向け)
	set(value):
		use_parent_linear_velocity = value

@export var debug_print_impact: bool = false

# === 内部状態 ===
var _rigid_body: RigidBody2D = null
var _rigid_body_3d: RigidBody3D = null
var _audio_player: AudioStreamPlayer = null
var _audio_player_2d: AudioStreamPlayer2D = null
var _audio_player_3d: AudioStreamPlayer3D = null

var _last_velocity: Vector2 = Vector2.ZERO
var _last_velocity_3d: Vector3 = Vector3.ZERO
var _time_since_last_sound: float = 0.0

func _ready() -> void:
	# 親が RigidBody2D か RigidBody3D かを判定
	if owner is RigidBody2D:
		_rigid_body = owner as RigidBody2D
	elif owner is RigidBody3D:
		_rigid_body_3d = owner as RigidBody3D
	else:
		push_warning("ImpactSound: 親ノードは RigidBody2D か RigidBody3D を想定しています。現在: %s" % [owner])
	
	# 子ノードから AudioStreamPlayer を探す(2D / 3D 両対応)
	_audio_player = get_node_or_null("AudioStreamPlayer")
	_audio_player_2d = get_node_or_null("AudioStreamPlayer2D")
	_audio_player_3d = get_node_or_null("AudioStreamPlayer3D")

	if not _audio_player and not _audio_player_2d and not _audio_player_3d:
		push_warning("ImpactSound: 子に AudioStreamPlayer / AudioStreamPlayer2D / AudioStreamPlayer3D が見つかりません。音は鳴りません。")

	# 初期速度を記録
	if _rigid_body:
		_last_velocity = _rigid_body.linear_velocity
	elif _rigid_body_3d:
		_last_velocity_3d = _rigid_body_3d.linear_velocity

	set_process(true)

func _process(delta: float) -> void:
	if not enabled:
		return

	_time_since_last_sound += delta

	if _rigid_body:
		_process_2d()
	elif _rigid_body_3d:
		_process_3d()

func _process_2d() -> void:
	if not _rigid_body:
		return

	# RigidBody2D は direct に衝突コールバックが取りにくいので、
	# 速度の変化量(減速の大きさ)を「疑似インパクト」として扱う。
	var current_velocity := _rigid_body.linear_velocity
	var delta_v := current_velocity - _last_velocity
	var impact_speed := delta_v.length()

	if impact_speed > 0.0:
		_handle_impact(impact_speed)

	_last_velocity = current_velocity

func _process_3d() -> void:
	if not _rigid_body_3d:
		return

	# RigidBody3D も同様に、速度変化から衝撃を推定
	var current_velocity := _rigid_body_3d.linear_velocity
	var delta_v := current_velocity - _last_velocity_3d
	var impact_speed := delta_v.length()

	if impact_speed > 0.0:
		_handle_impact(impact_speed)

	_last_velocity_3d = current_velocity

func _handle_impact(impact_speed: float) -> void:
	# クールダウン中は無視
	if _time_since_last_sound < cooldown_time:
		return

	# 一定以下のインパクトは無視
	if impact_speed < min_impact_speed:
		return

	# 衝撃強度を 0.0〜1.0 に正規化
	var t := (impact_speed - min_impact_speed) / (max_impact_speed - min_impact_speed)
	t = clamp(t, 0.0, 1.0)

	# 音量を補間(線形)
	var volume := lerp(min_volume, max_volume, t)

	if debug_print_impact:
		print("ImpactSound: impact_speed=%.2f, t=%.2f, volume=%.2f" % [impact_speed, t, volume])

	_play_sound(volume)
	_time_since_last_sound = 0.0

func _play_sound(volume: float) -> void:
	var player := _get_any_audio_player()
	if not player:
		return

	# 0.0〜1.0 の音量を dB に変換
	# 0.0 → -80dB (ほぼ無音), 1.0 → 0dB
	var db := linear_to_db(clamp(volume, 0.0001, 1.0))

	# ピッチにランダム変化を加える(少しだけ毎回違う音に)
	var pitch := 1.0
	if random_pitch_variation > 0.0:
		var r := randf_range(-random_pitch_variation, random_pitch_variation)
		pitch += r

	if player is AudioStreamPlayer:
		player.volume_db = db
		player.pitch_scale = pitch
		player.play()
	elif player is AudioStreamPlayer2D:
		player.volume_db = db
		player.pitch_scale = pitch
		player.play()
	elif player is AudioStreamPlayer3D:
		player.volume_db = db
		player.pitch_scale = pitch
		player.play()

func _get_any_audio_player() -> Object:
	# 優先順位: 2D → 3D → 汎用
	if _audio_player_2d:
		return _audio_player_2d
	if _audio_player_3d:
		return _audio_player_3d
	if _audio_player:
		return _audio_player
	return null

使い方の手順

ここでは 2D 物理(RigidBody2D)を例にしますが、3D でも同じ考え方で使えます。

  1. ImpactSound.gd を用意する
    上記コードをそのまま ImpactSound.gd として保存し、
    Godot エディタで開いて「スクリプト」タブから class_name ImpactSound が認識されていることを確認します。
  2. シーン構成にコンポーネントを追加する
    例として、物理で転がる木箱(Crate)を作るとします。
    Crate (RigidBody2D)
    ├── Sprite2D
    ├── CollisionShape2D
    ├── ImpactSound (Node)
    └── AudioStreamPlayer2D


    • Crate: RigidBody2D

    • ImpactSound: 通常の Node を追加して、スクリプトに ImpactSound.gd をアタッチ

    • AudioStreamPlayer2D: 衝突音の AudioStream(効果音)を設定


    3D の場合は RigidBody3DAudioStreamPlayer3D を使うだけでほぼ同じです。


  3. インスペクタでパラメータを調整する
    ImpactSound ノードを選択すると、以下のようなパラメータが見えます。
    • min_impact_speed: この値未満の衝突は音を鳴らさない(小さなコツンは無視)
    • max_impact_speed: この値以上の衝突は常に最大音量
    • min_volume / max_volume: 音量レンジ
    • cooldown_time: 連続ヒット時のクールダウン(0.05〜0.1 くらいが無難)
    • random_pitch_variation: 音に個性を出すためのピッチ揺らぎ
    • debug_print_impact: 衝撃値をコンソールに出したいときに ON
  4. 実際に動かしてみる
    シーンに床を用意して、Crate を高い位置に置いて落下させてみましょう。

    • 高いところから落とす → 衝撃が大きくなり、音量も大きく

    • 少しだけ転がす → ほとんど音が鳴らない or 小さい音


    プレイヤーや敵にも同じコンポーネントをポン付けできます。


    Player (RigidBody2D or CharacterBody2D)
    ├── Sprite2D
    ├── CollisionShape2D
    ├── ImpactSound (Node)
    └── AudioStreamPlayer2D

    たとえば、プレイヤーが壁に激突したときに「ドンッ」と鳴らしたい場合も、
    プレイヤー本体のスクリプトを汚さずに、ImpactSound コンポーネントだけで完結します。


メリットと応用

ImpactSound コンポーネントを使うメリットはかなりシンプルですが強力です。

  • シーン構造がスッキリする
    衝突音のロジックを RigidBody のスクリプトから完全に分離できるので、
    「物理挙動」と「演出(サウンド)」が混ざってカオスになるのを防げます。
  • あらゆるオブジェクトにコピペなしで適用できる
    Player、Enemy、Crate、MovingPlatform…など、
    「衝突したら音が欲しいもの」に共通のコンポーネントとして使い回せます。
  • パラメータだけで個性を出せる
    同じコンポーネントでも、重い鉄球なら min_impact_speed を小さめに、
    軽いゴムボールなら max_volume を低めに、など、インスペクタ上の調整だけで差別化できます。
  • 「継承ツリー」を増やさずに済む
    「SoundRigidBody」「PlayerWithImpactSound」みたいなクラスを増やさなくていいので、
    継承地獄を避けて、合成(Composition)で機能を積み上げていけます。

さらに、ImpactSound をベースにして、例えば「一定以上の衝撃でだけ壊れるオブジェクト」を作ることもできます。

改造案として、_handle_impact の中でシグナルを飛ばすようにしておくと、
「大きな衝突があったらパーティクルを出す」「HP を減らす」など、別コンポーネントと連携できます。


signal big_impact(impact_speed: float)

func _handle_impact(impact_speed: float) -> void:
	if _time_since_last_sound < cooldown_time:
		return
	if impact_speed < min_impact_speed:
		return

	var t := (impact_speed - min_impact_speed) / (max_impact_speed - min_impact_speed)
	t = clamp(t, 0.0, 1.0)
	var volume := lerp(min_volume, max_volume, t)

	_play_sound(volume)
	_time_since_last_sound = 0.0

	# 例えば、かなり大きい衝撃だった場合だけシグナル発火
	if impact_speed >= max_impact_speed * 0.8:
		emit_signal("big_impact", impact_speed)

このシグナルを別の「破壊コンポーネント」や「エフェクトコンポーネント」とつなげれば、
さらに「継承より合成」な設計に育てていけますね。