Godotでパーティクルや弾丸、エフェクトを作っていると、こんなコードを書きがちですよね。

  • 弾丸シーンの _process()_physics_process() で「生存時間」を手動でカウントする
  • 親ノード側で await get_tree().create_timer() を使って寿命を管理する
  • シーンごとに「寿命用の変数」と「タイマー処理」をコピペする

動くには動くんですが、弾丸、エフェクト、ヒットスパーク、落下する石…などなど、寿命付きオブジェクトが増えるたびに同じようなコードを量産することになります。さらに、寿命ロジックを変えたいときは、全部のシーンを開いて修正…ちょっと地獄ですね。

そこで、「寿命」という責務だけを切り出したコンポーネント LifetimeTimer を用意しておき、必要なノードにペタッと貼るだけで「X秒後に自動で消える」ようにしてしまいましょう。継承ではなく 合成(Composition) で寿命を付与するイメージです。

【Godot 4】寿命管理はコンポーネントに丸投げ!「LifetimeTimer」コンポーネント

今回の LifetimeTimer はとてもシンプルです:

  • 生成されてから指定秒数が経過したら
  • 親ノード(通常は自分がアタッチされているオブジェクト)を queue_free() する

パーティクル、弾丸、エフェクト、使い捨てUIポップアップなど、「一定時間で消えるもの」全般に使えます。


GDScript フルコード


extends Node
class_name LifetimeTimer
## LifetimeTimer
## 親ノードに「寿命」を付与するコンポーネント。
## シーンに追加しておくだけで、生成から X 秒後に親を queue_free() します。
##
## 想定用途:
## - 弾丸やミサイルなどの飛び道具
## - 爆発エフェクト、ヒットエフェクト
## - 一時的なUI(ダメージ表示など)
## - 一定時間だけ存在するギミック

@export_range(0.0, 600.0, 0.1, "or_greater")
var lifetime_seconds: float = 3.0:
	set(value):
		lifetime_seconds = max(value, 0.0)
		if Engine.is_editor_hint():
			# エディタ上で値を変えたときに、簡易プレビュー用のログ
			print("LifetimeTimer: lifetime_seconds set to ", lifetime_seconds)

## true のとき、シーンが ready になった瞬間からカウントを開始します。
## false にしておくと、自分で start() を呼ぶまで親は消えません。
@export
var auto_start: bool = true

## true のとき、親ノードではなく、自分自身 (LifetimeTimer) を queue_free() します。
## 「親は残して、コンポーネントだけ寿命を持たせたい」特殊ケース向け。
@export
var free_self_instead_of_parent: bool = false

## 寿命が尽きたときに発火するシグナル。
## 外部で「消える直前に何かしたい」ときに接続できます。
signal timeout

## 内部フラグ:すでにカウントダウンが始まっているかどうか
var _is_running: bool = false

func _ready() -> void:
	# 親ノードがいないと誰を消すのか分からないので、警告を出しておきます。
	if not free_self_instead_of_parent and get_parent() == null:
		push_warning("LifetimeTimer: No parent found. Nothing will be freed.")
	
	if auto_start:
		start()

## 寿命カウントダウンを開始します。
## すでに開始している場合は何もしません。
func start() -> void:
	if _is_running:
		return
	_is_running = true
	
	# 0秒なら即座に削除してしまう
	if is_zero_approx(lifetime_seconds):
		_on_timeout_immediate()
		return
	
	# 非同期でタイマーを待機してから削除処理へ
	_run_timer()

## 寿命カウントダウンをリセットして再スタートします。
## 弾丸が何かに当たったときに寿命を延長したり、再利用時に使えます。
func restart(new_lifetime_seconds: float = -1.0) -> void:
	if new_lifetime_seconds >= 0.0:
		lifetime_seconds = new_lifetime_seconds
	_is_running = false
	start()

## 内部コルーチン: 指定秒数待ってから削除処理を呼ぶ
func _run_timer() -> void:
	# Timerノードを使わず、シンプルに SceneTreeTimer を利用
	var tree := get_tree()
	if tree == null:
		push_warning("LifetimeTimer: No SceneTree available. Cannot start timer.")
		return
	
	var timer := tree.create_timer(lifetime_seconds)
	await timer.timeout
	
	_on_timeout_immediate()

## 即時に削除処理を行う関数。
## 外部から「今すぐ寿命を終わらせる」ために呼んでもOKです。
func force_timeout() -> void:
	_on_timeout_immediate()

## 寿命が尽きたときの共通処理。
func _on_timeout_immediate() -> void:
	if not is_inside_tree():
		# すでにツリーから外れているなら何もしない
		return
	
	emit_signal("timeout")
	
	if free_self_instead_of_parent:
		# コンポーネント自身を削除
		queue_free()
	else:
		var parent := get_parent()
		if parent:
			parent.queue_free()
		else:
			# 親がいない場合は自分だけ消す
			push_warning("LifetimeTimer: No parent to free. Freeing self instead.")
			queue_free()

使い方の手順

ここでは代表的な3パターンで使い方を見ていきます。

例1:弾丸(Bullet)の寿命管理に使う

  1. 弾丸用シーン(例:Bullet.tscn)を作成し、ルートを Area2DCharacterBody2D にします。
  2. ルートノードの子として LifetimeTimer を追加します(Node として追加し、スクリプトに LifetimeTimer.gd をアタッチ)。
  3. インスペクタで lifetime_seconds を 1.0〜2.0 秒程度に設定します。
  4. あとはプレイヤーから弾をインスタンスして飛ばすだけで、指定秒数後に弾のシーンごと自動で消えます。

シーン構成例:

Bullet (Area2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── LifetimeTimer (Node)

この構成なら、弾の「見た目」「当たり判定」「寿命」が完全に分離されていて、寿命ロジックを他のシーンにもそのまま流用できます。

例2:爆発エフェクトの自動削除に使う

  1. Explosion.tscn を作り、ルートを Node2DGPUParticles2D にします。
  2. 子として LifetimeTimer を追加し、アニメーションやパーティクルの長さに合わせて lifetime_seconds を設定します(例:0.8秒)。
  3. 敵を倒したときなどに Explosion をインスタンスして再生するだけで、後始末は LifetimeTimer がやってくれます。

シーン構成例:

Explosion (Node2D)
 ├── GPUParticles2D
 ├── AudioStreamPlayer2D
 └── LifetimeTimer (Node)

これで「消し忘れたエフェクトがシーンに残り続けて重くなる」といった事故を防げます。

例3:動く床や一時的なギミックに使う

例えば、一定時間だけ出現する足場を作りたい場合:

  1. TemporaryPlatform.tscn を作り、ルートを StaticBody2D にします。
  2. 子として Sprite2DCollisionShape2D、そして LifetimeTimer を追加。
  3. lifetime_seconds に 5.0 などを設定すれば、5秒後に足場が消えます。

シーン構成例:

TemporaryPlatform (StaticBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── LifetimeTimer (Node)

このように、寿命を持たせたいものにどんどん LifetimeTimer をアタッチしていくだけで、「時間で消えるギミック」を量産できます。親の型は Node であればなんでもOKなのもポイントですね。


メリットと応用

1. シーン構造がシンプルで再利用しやすい

寿命ロジックを各シーンにベタ書きするのではなく、LifetimeTimer コンポーネントに閉じ込めておくことで、次のようなメリットがあります。

  • 弾丸、エフェクト、ギミックなど、どのシーンでも同じコンポーネントを使い回せる
  • 寿命の仕様変更(例:0秒の扱い、削除前イベントなど)を1箇所直すだけで全シーンに反映
  • 親ノードのスクリプトから「寿命カウンタ」などの余計な状態が消え、責務が明確になる

まさに「継承より合成」の恩恵ですね。弾丸クラスに寿命ロジックを継承で押し込むのではなく、「寿命」という機能をコンポーネントとして後から付け足すスタイルです。

2. レベルデザインが直感的になる

レベルデザイナや自分の未来の自分がシーンを開いたとき、「あ、このノードには寿命があるんだな」と一目で分かります。LifetimeTimerlifetime_seconds をインスペクタでちょっといじるだけで、ゲーム内のテンポを調整できるのも嬉しいところです。

3. timeout シグナルで「消える直前アクション」も簡単

timeout シグナルに接続することで、例えば「消える直前に点滅させる」「SEを鳴らす」「スコアを加算する」などの処理を追加できます。


func _on_lifetime_timer_timeout() -> void:
	# ここに消える直前の処理を書く
	$Sprite2D.modulate = Color.RED

このように、単なる「時間で消える」だけでなく、「消える前の演出」をまとめて扱えるのもコンポーネント化の良いところですね。


改造案:寿命が近づいたら点滅させる

例えば「残り1秒になったら点滅を始める」ような拡張をしたい場合、LifetimeTimer を少し改造して、こんな関数を追加するのもアリです。


## 残り warn_before 秒になったら、親の Modulate を点滅させる簡易演出
func start_with_warning(warn_before: float = 1.0, blink_interval: float = 0.2) -> void:
	if lifetime_seconds <= warn_before:
		# 寿命が短すぎる場合は普通に start だけ
		start()
		return
	
	_is_running = true
	var tree := get_tree()
	if tree == null:
		push_warning("LifetimeTimer: No SceneTree available. Cannot start timer.")
		return
	
	# 「警告開始まで」と「最後の warn_before 秒」とでタイマーを分ける
	var first_timer := tree.create_timer(lifetime_seconds - warn_before)
	await first_timer.timeout
	
	# ここから点滅開始
	var parent := get_parent()
	if parent and parent.has_method("set_modulate"):
		var elapsed := 0.0
		while elapsed < warn_before and is_inside_tree():
			parent.modulate.a = 0.3 if parent.modulate.a > 0.5 else 1.0
			var t := tree.create_timer(blink_interval)
			await t.timeout
			elapsed += blink_interval
	
	_on_timeout_immediate()

このように、寿命まわりの挙動はすべて LifetimeTimer に集約しておくと、ゲーム全体の「時間で消えるもの」の挙動を一元管理できて気持ちいいです。ぜひ自分のプロジェクト用にカスタマイズしながら育てていきましょう。