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)
使い方の手順
ここからは、実際に「爆発するグレネード」と「爆発バレル」を例に使い方を見ていきましょう。
手順①:コンポーネントスクリプトを用意する
- 上記の
ExplosionForce.gdをプロジェクトのどこか(例:res://components/ExplosionForce.gd)に保存します。 - Godotエディタで開き、エラーが出ていないことを確認します。
手順②:グレネードに ExplosionForce をアタッチする
例として、物理的に転がるグレネードを作るとします。
Grenade (RigidBody3D) ├── MeshInstance3D ├── CollisionShape3D ├── Timer (爆発までの時間) └── ExplosionForce (Node3D) <-- コンポーネント
Grenadeシーンを作成(ルート:RigidBody3D)。- 子ノードとして
MeshInstance3DとCollisionShape3Dを追加。 - さらに子ノードとして
Node3Dを追加し、スクリプトにExplosionForce.gdをアタッチする。名前をExplosionForceに変更しておくと分かりやすいです。 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)
このように、爆風物理だけでなく「爆風ダメージ」もコンポーネント同士の連携で表現できるようになります。
継承に頼らず、「爆発する能力」「ダメージを持つ能力」をそれぞれ独立コンポーネントに分けていくと、プロジェクトがどんどん整理されていきますね。




