弾丸や一時的なエフェクトを作るとき、ついこういうコードを書きがちですよね。
extends CharacterBody2D
var life_time := 1.0
func _process(delta):
life_time -= delta
if life_time <= 0.0:
queue_free()
func _on_body_entered(body):
queue_free()
これでも動きますが、「弾丸ごとに寿命や自爆条件を書く」「敵もエフェクトも全部同じような処理をコピペする」…と、あっという間にスクリプトが増殖していきます。さらに、
- 敵の弾とプレイヤーの弾で挙動を変えたい
- エフェクトは「時間だけで消える」、弾は「時間 or 衝突で消える」
といったバリエーションが出てくると、継承ツリーを増やすか、if だらけの巨大スクリプトになりがちです。
そこで「弾丸の自壊ロジック」は全部コンポーネントに切り出してしまいましょう。SelfDestruct コンポーネントを親ノードにアタッチするだけで、
- 生成から一定時間後に自動で消える
- 衝突したタイミングで消える
といった処理を、継承なし・コピペなしで付け外しできるようにします。
【Godot 4】弾もエフェクトも「寿命」はこれ一つ!「SelfDestruct」コンポーネント
コンポーネントの設計方針
- どのノードにも付けられる汎用コンポーネント(
Node継承) - 親ノードを破棄する(自分自身ではなく、あくまで「持ち主」を消す)
- 時間ベース / 衝突ベースの両方に対応
- 衝突は
Area2D/PhysicsBody2D/Area3D/PhysicsBody3Dのいずれかに対応 - Godot 4 の
@exportで挙動を柔軟に設定
「弾丸の寿命」「敵の断末魔エフェクト」「一時的なバフ表示」など、全部このコンポーネントを付けるだけで完結させるのが狙いです。
SelfDestruct.gd フルコード
extends Node
class_name SelfDestruct
## SelfDestruct コンポーネント
## - 親ノードを一定時間後、または衝突時に自動で queue_free() する
## - 弾丸、エフェクト、一時オブジェクトなどにアタッチして使う
## --- 設定パラメータ ---
@export_category("Lifetime (時間で自爆)")
@export var enable_time_limit: bool = true:
## true のとき、生成から time_to_live 秒後に親を削除します。
## false にすると「時間では消えない」弾丸などを作れます。
set(value):
enable_time_limit = value
_update_timer_state()
@export_range(0.0, 9999.0, 0.1, "or_greater") var time_to_live: float = 2.0:
## 生成から何秒後に自爆するか。
## enable_time_limit が true のときのみ有効です。
set(value):
time_to_live = max(value, 0.0)
_update_timer_state()
@export_category("Collision (衝突で自爆)")
@export var enable_collision_destruct: bool = true:
## true のとき、衝突した瞬間に親を削除します。
## Area2D / PhysicsBody2D / Area3D / PhysicsBody3D に対応します。
set(value):
enable_collision_destruct = value
_update_collision_connections()
@export var one_shot: bool = true:
## true: 最初の自爆条件を満たしたら即削除(通常の弾丸向け)
## false: 条件を満たしても削除せず、別のトリガーを待つ(特殊な用途向け)
## ※通常は true のままでOKです。
set(value):
one_shot = value
@export_category("Debug")
@export var print_debug_log: bool = false
## true にすると、自爆トリガー時に print でログを出します。
## 内部用: タイマー参照
var _timer: Timer
## 内部用: 衝突シグナル接続済みかどうか
var _collision_connected := false
## 内部用: すでに自爆処理を実行したかどうか
var _has_detonated := false
func _ready() -> void:
# 親が存在しない場合は意味がないので警告だけ出す
if get_parent() == null:
push_warning("SelfDestruct: 親ノードがありません。このコンポーネントは意味を持ちません。")
return
_setup_timer()
_update_timer_state()
_update_collision_connections()
## --- タイマー関連セットアップ ---
func _setup_timer() -> void:
# すでに Timer がある場合は再利用
_timer = get_node_or_null("SelfDestructTimer")
if _timer == null:
_timer = Timer.new()
_timer.name = "SelfDestructTimer"
_timer.one_shot = true
add_child(_timer)
# シグナル接続(重複接続を避ける)
if not _timer.timeout.is_connected(_on_timer_timeout):
_timer.timeout.connect(_on_timer_timeout)
func _update_timer_state() -> void:
if not is_inside_tree():
return
if not enable_time_limit:
if _timer:
_timer.stop()
return
if _timer:
_timer.wait_time = max(time_to_live, 0.0)
# ready 後すぐにカウント開始
if not _timer.is_stopped():
_timer.stop()
_timer.start()
func _on_timer_timeout() -> void:
_trigger_self_destruct("time")
## --- 衝突シグナル関連 ---
func _update_collision_connections() -> void:
if not is_inside_tree():
return
if not enable_collision_destruct:
_disconnect_collision_signals()
return
_connect_collision_signals()
func _connect_collision_signals() -> void:
if _collision_connected:
return
var parent := get_parent()
if parent == null:
return
# 2D 物理ノードの場合
if parent is Area2D:
var a2d := parent as Area2D
if not a2d.body_entered.is_connected(_on_body_or_area_entered):
a2d.body_entered.connect(_on_body_or_area_entered)
if not a2d.area_entered.is_connected(_on_body_or_area_entered):
a2d.area_entered.connect(_on_body_or_area_entered)
_collision_connected = true
return
if parent is PhysicsBody2D:
var pb2d := parent as PhysicsBody2D
if not pb2d.body_entered.is_connected(_on_body_or_area_entered):
pb2d.body_entered.connect(_on_body_or_area_entered)
if not pb2d.body_shape_entered.is_connected(_on_body_shape_entered):
pb2d.body_shape_entered.connect(_on_body_shape_entered)
_collision_connected = true
return
# 3D 物理ノードの場合
if parent is Area3D:
var a3d := parent as Area3D
if not a3d.body_entered.is_connected(_on_body_or_area_entered):
a3d.body_entered.connect(_on_body_or_area_entered)
if not a3d.area_entered.is_connected(_on_body_or_area_entered):
a3d.area_entered.connect(_on_body_or_area_entered)
_collision_connected = true
return
if parent is PhysicsBody3D:
var pb3d := parent as PhysicsBody3D
if not pb3d.body_entered.is_connected(_on_body_or_area_entered):
pb3d.body_entered.connect(_on_body_or_area_entered)
if not pb3d.body_shape_entered.is_connected(_on_body_shape_entered):
pb3d.body_shape_entered.connect(_on_body_shape_entered)
_collision_connected = true
return
# どの物理ノードでもない場合は警告のみ
push_warning("SelfDestruct: 親ノードが Area2D / PhysicsBody2D / Area3D / PhysicsBody3D ではありません。衝突自爆は動作しません。")
func _disconnect_collision_signals() -> void:
if not _collision_connected:
return
var parent := get_parent()
if parent == null:
return
if parent is Area2D:
var a2d := parent as Area2D
if a2d.body_entered.is_connected(_on_body_or_area_entered):
a2d.body_entered.disconnect(_on_body_or_area_entered)
if a2d.area_entered.is_connected(_on_body_or_area_entered):
a2d.area_entered.disconnect(_on_body_or_area_entered)
elif parent is PhysicsBody2D:
var pb2d := parent as PhysicsBody2D
if pb2d.body_entered.is_connected(_on_body_or_area_entered):
pb2d.body_entered.disconnect(_on_body_or_area_entered)
if pb2d.body_shape_entered.is_connected(_on_body_shape_entered):
pb2d.body_shape_entered.disconnect(_on_body_shape_entered)
elif parent is Area3D:
var a3d := parent as Area3D
if a3d.body_entered.is_connected(_on_body_or_area_entered):
a3d.body_entered.disconnect(_on_body_or_area_entered)
if a3d.area_entered.is_connected(_on_body_or_area_entered):
a3d.area_entered.disconnect(_on_body_or_area_entered)
elif parent is PhysicsBody3D:
var pb3d := parent as PhysicsBody3D
if pb3d.body_entered.is_connected(_on_body_or_area_entered):
pb3d.body_entered.disconnect(_on_body_or_area_entered)
if pb3d.body_shape_entered.is_connected(_on_body_shape_entered):
pb3d.body_shape_entered.disconnect(_on_body_shape_entered)
_collision_connected = false
func _on_body_or_area_entered(_other: Node) -> void:
_trigger_self_destruct("collision")
func _on_body_shape_entered(_body_id: int, _body: Node, _body_shape: int, _local_shape: int) -> void:
_trigger_self_destruct("collision")
## --- 自爆処理本体 ---
func _trigger_self_destruct(reason: String) -> void:
if _has_detonated and one_shot:
return
_has_detonated = true
if print_debug_log:
var parent_name := get_parent() != null ? get_parent().name : "<no parent>"
print("SelfDestruct: parent=%s reason=%s" % [parent_name, reason])
if not one_shot:
# one_shot=false の場合、「トリガーが来た」ことだけ記録して終わり。
# 実際の queue_free() は、外部から呼び出すなどして行う想定です。
return
var parent := get_parent()
if parent != null and parent.is_inside_tree():
parent.queue_free()
使い方の手順
手順①: スクリプトをプロジェクトに追加
res://components/など、コンポーネント用フォルダを作成します。- その中に
SelfDestruct.gdを作成し、上記コードをコピペして保存します。 - Godot エディタを再読み込みすると、SelfDestruct がスクリプトクラスとして認識されます。
手順②: 弾丸シーンにアタッチする
例として、2D のシンプルなプレイヤー弾シーンを考えます。
Bullet (Area2D) ├── Sprite2D ├── CollisionShape2D └── SelfDestruct (Node)
- 弾丸のルートノードを
Area2Dで作成します(名前:Bullet)。 Sprite2DとCollisionShape2Dを子として追加し、見た目と当たり判定を設定します。- 空の Node を追加し、名前を
SelfDestructに変更します。 - その Node にスクリプトとして
SelfDestruct.gdをアタッチします。
これで、「Bullet の寿命管理」は完全にコンポーネント側に切り離されました。弾丸本体のスクリプトは、移動ロジックだけに集中できます。
手順③: パラメータを調整する
SelfDestruct ノードを選択すると、インスペクタに以下の項目が出ます。
enable_time_limit: 生成から一定時間後に消したい場合は ON(デフォルト: ON)time_to_live: 弾丸の寿命(秒)。例:1.5秒enable_collision_destruct: 衝突時に消したい場合は ON(デフォルト: ON)one_shot: 最初のトリガーで即削除するなら ON(普通の弾丸は ON でOK)print_debug_log: デバッグ用ログが欲しければ ON
例えば「撃ってから 1 秒で自動消滅、何かに当たったら即消滅」という弾丸なら、
enable_time_limit = truetime_to_live = 1.0enable_collision_destruct = trueone_shot = true
と設定しておけばOKです。
手順④: 他のシーンにもどんどん再利用する
SelfDestruct は「親を消すだけ」のコンポーネントなので、弾丸以外にもガンガン使い回せます。
例1: 敵の死亡エフェクト
DeathEffect (Node2D) ├── AnimatedSprite2D ├── AudioStreamPlayer2D └── SelfDestruct (Node)
enable_time_limit = truetime_to_live = 0.6(アニメーションの長さに合わせる)enable_collision_destruct = false(当たり判定は不要)
敵が死んだときにこのシーンをインスタンス化するだけで、「勝手に消えるエフェクト」が完成です。
例2: 一時的な動く床(一定時間で消える)
TempPlatform (StaticBody2D) ├── Sprite2D ├── CollisionShape2D └── SelfDestruct (Node)
enable_time_limit = truetime_to_live = 3.0(3秒で足場が消える)enable_collision_destruct = false(衝突では消えない)
「踏んでから数秒で消える足場」も、プラットフォーム側は何も知らなくてOK。寿命は全部コンポーネント任せです。
メリットと応用
メリット1: 弾丸スクリプトが「移動だけ」になる
SelfDestruct を導入すると、弾丸側はこんなシンプルなコードで済みます。
extends Area2D
@export var speed: float = 600.0
var direction: Vector2 = Vector2.RIGHT
func _physics_process(delta: float) -> void:
position += direction * speed * delta
寿命・衝突時の破棄は全部コンポーネントに任せるので、弾丸の種類が増えても「移動の違い」だけに集中できます。継承ツリーを分けなくても、コンポーネントの組み合わせで差分を表現できるのがポイントですね。
メリット2: シーン構造がフラットで見通しが良い
Godot 標準の「ノードに直接スクリプトを貼る」スタイルだと、
- 弾丸のスクリプト内に「移動」「寿命」「衝突処理」「エフェクト生成」などが混在
- どこに何のロジックが書いてあるか探しづらい
といった状態になりがちです。SelfDestruct をはじめとしたコンポーネントを使うと、
Bullet (Area2D) ├── Sprite2D ├── CollisionShape2D ├── MoveForward (Component) └── SelfDestruct (Component)
のように、「見た目」「当たり判定」「移動」「寿命」がノードレベルで分離されます。継承より合成の考え方で、シーン構造そのものがドキュメントになるイメージですね。
メリット3: レベルデザイナーもパラメータをいじりやすい
寿命や衝突条件が @export で露出しているので、「この弾だけ 0.3 秒で消したい」「このエフェクトは長めに 1.5 秒残したい」といった調整を、コードを書かずにインスペクタから設定できます。
同じ弾丸シーンを複製して、SelfDestruct の time_to_live だけ変えれば、あっという間に「短命弾」「長命弾」などのバリエーションが作れます。
改造案: 衝突相手によって自爆するかをフィルタする
「敵に当たったら消えるけど、味方に当たっても消えない」「壁に当たったときだけ消す」といったフィルタリングをしたい場合は、_trigger_self_destruct の前に条件を挟むのが簡単です。
例えば、「特定のグループに属する相手と衝突したときだけ自爆」する機能を足してみます。
@export var collision_target_group: StringName = &"":
## ここにグループ名を入れると、そのグループの相手と衝突したときだけ自爆します。
## 空文字列のときは無条件で自爆します。
func _on_body_or_area_entered(other: Node) -> void:
if collision_target_group != &"":
if not other.is_in_group(collision_target_group):
return # 対象外なので自爆しない
_trigger_self_destruct("collision")
これで、例えば collision_target_group = "Enemy" としておけば、「敵に当たったときだけ消えるプレイヤー弾」が簡単に作れます。逆に、"Player" を指定すれば「プレイヤーに当たったときだけ消える敵弾」もすぐですね。
SelfDestruct コンポーネントはとてもシンプルですが、「寿命」「破棄」の責務を一箇所に集約できるので、プロジェクトが大きくなるほど恩恵が出てきます。継承より合成の第一歩として、ぜひプロジェクトの標準コンポーネントにしてしまいましょう。
