【Godot 4】ExplosionForce (爆風物理) コンポーネントの作り方

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

脱・初心者!Godot 4 ゲーム開発の「2歩目」

新品価格
¥831から
(2025/12/13 21:39時点)

Godot4ローグライク入門 ~ダンジョン自動生成~

新品価格
¥831から
(2025/12/13 21:44時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Godotで爆発を実装しようとすると、ついこういう構成になりがちですよね。

Explosion (Area3D)
 ├── CollisionShape3D
 ├── Timer
 ├── アニメーション再生用の何か
 └── 爆風処理専用のスクリプト

そしてそのスクリプトの中で、body_entered を拾って、RigidBody3D か判定して、位置を計算して…と、毎回似たようなコードを書くことになります。
さらに「敵のグレネード」「プレイヤーの爆弾」「環境ギミックの爆発バレル」など、爆発を使うオブジェクトが増えるたびにコピペが増殖していきます。

継承でまとめようとしても、「爆発するRigidBody3D」「爆発するStaticBody3D」「爆発するNode3D」…とクラス設計がややこしくなりがちです。
そこで発想を変えて、「爆発できる能力」をコンポーネントとして切り出してしまいましょう。

この記事では、どんなノードにもアタッチして使える「ExplosionForce」コンポーネントを紹介します。
爆発処理を 1 クラスに閉じ込めておけば、あとは「付けるだけ」で爆風物理を再利用できるようになります。

【Godot 4】コピペ卒業の爆風物理!「ExplosionForce」コンポーネント

今回のコンポーネントは:

  • 爆心地の位置と半径をもとに、周囲の RigidBody3D に外向きの衝撃力を与える
  • 減衰(距離が遠いほど弱くなる)をサポート
  • オプションで「上方向の補正」や「爆発レイヤーのフィルタ」も設定可能
  • どのノードにもアタッチ可能(Node3D ベース)

コンポーネント指向で「爆発する能力」をパッケージ化しておけば、

  • グレネード
  • ロケット弾
  • 爆発バレル
  • 地雷

などに同じコンポーネントをポン付けして使い回せます。

フルコード:ExplosionForce.gd


extends Node3D
class_name ExplosionForce
## 爆風物理コンポーネント
## - 任意のNode3Dにアタッチして使用
## - trigger() を呼ぶと、その位置を爆心地として爆風を適用する

@export_category("Explosion Settings")
## 爆風の有効半径(メートル)
@export var radius: float = 5.0

## 爆風の最大インパルス(中心付近での強さ)
## 値が大きいほど、RigidBody3Dが強く吹き飛びます。
@export var impulse_strength: float = 30.0

## 距離による減衰を有効にするか
## true の場合、中心から遠いほど弱くなります。
@export var use_distance_attenuation: bool = true

## 減衰の指数
## 1.0 = 線形, 2.0 = 二乗で急減衰, 0.5 = 緩やか など
@export_range(0.1, 4.0, 0.1)
var attenuation_power: float = 1.5

## 垂直方向(上方向)の力を追加する係数
## 0.0 なら水平のみ, 1.0 なら水平方向と同等の強さで上向きに力を加える
@export_range(0.0, 2.0, 0.05)
var upward_boost: float = 0.2

@export_category("Collision Filter")
## どのレイヤーのオブジェクトに爆風を適用するか
## 0 の場合、全レイヤーにヒット
@export_flags_3d_physics var collision_mask: int = 0

## 自分自身のRigidBody3Dには爆風を適用しない
@export var ignore_self_bodies: bool = true

@export_category("Debug")
## デバッグ用に、エディタ&ゲーム中に爆風の半径を可視化
@export var debug_draw_gizmo: bool = false

## 爆風で影響を与えるボディの最大数(パフォーマンス対策)
@export_range(1, 128, 1)
var max_affected_bodies: int = 32

## 内部で使うPhysicsDirectSpaceState3D
var _space_state: PhysicsDirectSpaceState3D

func _ready() -> void:
    _space_state = get_world_3d().direct_space_state

func _physics_process(_delta: float) -> void:
    # ワールドが切り替わったときなどに備えて、毎フレーム更新しておく
    if get_world_3d() and get_world_3d().direct_space_state:
        _space_state = get_world_3d().direct_space_state

func _notification(what: int) -> void:
    if what == NOTIFICATION_EDITOR_DRAW and debug_draw_gizmo:
        _draw_debug_gizmo()

## 爆風を発生させるメイン関数
## - origin: 爆心地。省略時はこのノードのグローバル位置。
## - custom_radius: 一時的に半径を上書きしたい場合に指定。
func trigger(origin: Vector3 = Vector3.INF, custom_radius: float = -1.0) -> void:
    if origin == Vector3.INF:
        origin = global_transform.origin
    var r := custom_radius > 0.0 ? custom_radius : radius

    if r <= 0.0 or impulse_strength == 0.0:
        return  # 無効な設定なら何もしない

    var query := PhysicsShapeQueryParameters3D.new()
    var sphere_shape := SphereShape3D.new()
    sphere_shape.radius = r
    query.shape = sphere_shape
    query.transform = Transform3D(Basis(), origin)
    query.collision_mask = collision_mask
    query.collide_with_areas = false
    query.collide_with_bodies = true

    # 半径内にいるボディを取得
    var results := _space_state.intersect_shape(query, max_affected_bodies)

    for result in results:
        var body := result.get("collider")
        if body == null:
            continue
        if not (body is RigidBody3D):
            continue

        # 自分自身のRigidBody3Dを無視したい場合
        if ignore_self_bodies and _is_own_body(body):
            continue

        _apply_explosion_to_body(body, origin, r)

## 自分のシーン階層内のRigidBody3Dかどうかを判定
func _is_own_body(body: RigidBody3D) -> bool:
    # 爆発元のルートをたどり、同じツリー内のRigidBody3Dかをざっくり判定
    var root := get_owner()
    if root == null:
        root = get_tree().current_scene
    if root == null:
        return false
    return body.is_ancestor_of(root) or root.is_ancestor_of(body)

## 実際にインパルスを与える処理
func _apply_explosion_to_body(body: RigidBody3D, origin: Vector3, r: float) -> void:
    var body_pos: Vector3 = body.global_transform.origin
    var to_body: Vector3 = body_pos - origin
    var distance: float = to_body.length()

    if distance == 0.0:
        # 完全に同じ位置にいる場合は、適当な方向にランダムで飛ばす
        to_body = Vector3.FORWARD.rotated(Vector3.UP, randf() * TAU)
        distance = 0.01

    var dir: Vector3 = to_body.normalized()

    # 上方向の補正を加える
    if upward_boost > 0.0:
        dir.y += upward_boost
        dir = dir.normalized()

    # 減衰計算
    var strength := impulse_strength
    if use_distance_attenuation:
        var t := clamp(distance / r, 0.0, 1.0)
        # 0 で最大、1 で最小になるように反転してから指数をかける
        var factor := pow(1.0 - t, attenuation_power)
        strength *= factor

    if strength <= 0.0:
        return

    # インパルスベクトル
    var impulse := dir * strength

    # 質量を考慮してインパルスを与える(中心に対して)
    body.apply_impulse(impulse, body_pos - body.global_transform.origin)

## デバッグ用の半径表示
func _draw_debug_gizmo() -> void:
    var color := Color(1.0, 0.6, 0.1, 0.6)
    var steps := 32
    var r := radius
    for i in steps:
        var a1 := TAU * float(i) / float(steps)
        var a2 := TAU * float(i + 1) / float(steps)
        var p1 := Vector3(cos(a1) * r, 0.0, sin(a1) * r)
        var p2 := Vector3(cos(a2) * r, 0.0, sin(a2) * r)
        draw_line(p1, p2, color, 0.03, true)

使い方の手順

ここからは、実際に「爆発するグレネード」と「爆発バレル」を例に使い方を見ていきましょう。

手順①:コンポーネントスクリプトを用意する

  1. 上記の ExplosionForce.gd をプロジェクトのどこか(例: res://components/ExplosionForce.gd)に保存します。
  2. Godotエディタで開き、エラーが出ていないことを確認します。

手順②:グレネードに ExplosionForce をアタッチする

例として、物理的に転がるグレネードを作るとします。

Grenade (RigidBody3D)
 ├── MeshInstance3D
 ├── CollisionShape3D
 ├── Timer (爆発までの時間)
 └── ExplosionForce (Node3D)  <-- コンポーネント
  1. Grenade シーンを作成(ルート: RigidBody3D)。
  2. 子ノードとして MeshInstance3DCollisionShape3D を追加。
  3. さらに子ノードとして Node3D を追加し、スクリプトに ExplosionForce.gd をアタッチする。名前を ExplosionForce に変更しておくと分かりやすいです。
  4. Timer ノードを追加し、wait_time = 2.0 などに設定、one_shot = true にします。

次に、グレネード本体のスクリプトを作ります。


extends RigidBody3D

@onready var explosion_force: ExplosionForce = $ExplosionForce
@onready var timer: Timer = $Timer

func _ready() -> void:
    timer.timeout.connect(_on_timer_timeout)

func _on_timer_timeout() -> void:
    # 爆発を発生させる
    explosion_force.trigger()
    # 爆発エフェクトやサウンドをここで再生してもOK
    # ひとまず、自分自身は消してしまう
    queue_free()

これで、グレネードが 2 秒後に爆発し、周囲の RigidBody3D を吹き飛ばしてくれるようになります。

手順③:爆発バレルに使い回す

同じコンポーネントを、今度は爆発バレルに使い回してみましょう。

ExplosiveBarrel (RigidBody3D)
 ├── MeshInstance3D
 ├── CollisionShape3D
 └── ExplosionForce (Node3D)

バレルが一定以上のダメージを受けたときに爆発する、という想定でスクリプトを書きます。


extends RigidBody3D

@onready var explosion_force: ExplosionForce = $ExplosionForce

var health: float = 50.0

func apply_damage(amount: float) -> void:
    health -= amount
    if health <= 0.0:
        _explode()

func _explode() -> void:
    explosion_force.trigger()
    queue_free()

このように、「ダメージ判定」や「タイマーでの起爆」は各オブジェクト側で自由に実装しつつ、
「爆風物理」という共通部分だけを ExplosionForce コンポーネントとして共有できます。

手順④:レベルシーンでの構成例

最後に、レベルシーン全体の構成例です。

MainLevel (Node3D)
 ├── Player (CharacterBody3D)
 │    ├── Camera3D
 │    ├── CollisionShape3D
 │    └── ...
 ├── GrenadeSpawner (Node3D)
 │    └── (プレイヤーが投げるグレネードをインスタンス)
 ├── ExplosiveBarrel (RigidBody3D)
 │    ├── MeshInstance3D
 │    ├── CollisionShape3D
 │    └── ExplosionForce (Node3D)
 └── PhysicsProps (Node3D)
      ├── Box1 (RigidBody3D)
      ├── Box2 (RigidBody3D)
      └── ...

どのオブジェクトが爆発を起こすかに関係なく、ExplosionForce を持っていれば同じ挙動になります。
「爆発ギミックを増やしたい」となったときも、新しいシーンにこのコンポーネントを追加するだけでOKですね。

メリットと応用

ExplosionForce をコンポーネントとして切り出すことで、次のようなメリットがあります。

  • シーン構造がスッキリ:爆発のロジックを1か所に集約できるので、各グレネードやバレル側のスクリプトは「いつ爆発するか」だけに集中できます。
  • 再利用性が高い:どのノードにもアタッチ可能な Node3D ベースなので、プレイヤーの必殺技や環境トラップなどにもそのまま使い回せます。
  • 調整が一括で済む:半径や強さ、減衰の仕方などをコンポーネント側でまとめて調整できるため、「ゲーム全体の爆発バランス」を一括でいじれます。
  • 継承ツリーが増えないExplosiveRigidBody3D などの派生クラスを作らなくてよいので、クラス設計がシンプルなまま保てます。

応用としては、例えば「爆発がダメージも与える」ように改造するのも簡単です。
Area3D を併用してもいいですが、物理判定の結果からダメージを飛ばすこともできます。

例えば、ExplosionForce に「Health」的なコンポーネントを探してダメージを与える処理を追加する改造案はこんな感じです。


## 改造案:RigidBody3D にアタッチされた Health コンポーネントにダメージを与える
func _apply_damage_if_possible(body: RigidBody3D, base_damage: float, distance: float, r: float) -> void:
    var health_node := body.get_node_or_null("Health")
    if health_node == null:
        return
    var damage := base_damage
    if use_distance_attenuation:
        var t := clamp(distance / r, 0.0, 1.0)
        damage *= pow(1.0 - t, attenuation_power)
    if damage > 0.0:
        if health_node.has_method("apply_damage"):
            health_node.apply_damage(damage)

このように、爆風物理だけでなく「爆風ダメージ」もコンポーネント同士の連携で表現できるようになります。
継承に頼らず、「爆発する能力」「ダメージを持つ能力」をそれぞれ独立コンポーネントに分けていくと、プロジェクトがどんどん整理されていきますね。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

脱・初心者!Godot 4 ゲーム開発の「2歩目」

新品価格
¥831から
(2025/12/13 21:39時点)

Godot4ローグライク入門 ~ダンジョン自動生成~

新品価格
¥831から
(2025/12/13 21:44時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

URLをコピーしました!