【Godot 4】ImpactThud (衝突音) コンポーネントの作り方

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時点)

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)

手順:

  1. 対象のシーンを開く(例: Crate.tscn)。
  2. ルートの RigidBody3D(または RigidBody2D)の子として
    • +ボタン → Node を追加 → ImpactThud を選択して追加。
  3. インスペクタで ImpactThud を選択し、以下を設定:
    • thud_stream に「ドン音」の AudioStream を指定
    • target_body は空でOK(親が RigidBody なら自動で拾います)
    • 音が小さすぎる/大きすぎる場合は
      • min_impact_speedmax_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 をベースにしたオリジナル衝突音コンポーネントを育ててみてください。

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