Unityを触り始めた頃によくあるパターンとして、こんなコードを書いてしまいがちですよね。


// なんでもかんでも1つのUpdateに書いてしまう例(よくない例)
public class GameManager : MonoBehaviour
{
    void Update()
    {
        // プレイヤーの入力処理
        // 敵のAI処理
        // スコア管理
        // エフェクトの寿命管理 ← ついでにここで Destroy してしまう
        // 弾丸の寿命管理       ← ついでにここで Destroy してしまう
        // ...などなど
    }
}

最初はこれでも動きますが、ゲームが少し複雑になるとすぐに破綻します。

  • 弾丸・パーティクル・一時的なオブジェクトの寿命管理がバラバラ
  • 「どこで Destroy されているのか」が追いづらい
  • Prefab を差し替えたときに、寿命ロジックを毎回書き直す必要が出てくる

そこで登場するのが、今回のコンポーネント 「LifetimeTimer」 です。
オブジェクトの寿命だけを責務とする小さなコンポーネントに分離することで、

  • パーティクルや弾丸に「寿命」を簡単に付与
  • Prefab 単位で寿命を設定して再利用
  • 巨大な Update 関数から寿命ロジックを切り離す

といったメリットが得られます。

【Unity】「消えるタイミング」はコンポーネントに丸投げ!「LifetimeTimer」コンポーネント

ここでは「生成から X 秒後に親を queue_free() する」という Godot 的な発想を、Unity 流に落とし込んだ実装を紹介します。
Unity では queue_free() の代わりに Destroy() を使いますが、やりたいことはほぼ同じです。

フルコード:LifetimeTimer.cs


using UnityEngine;

namespace Lifetime
{
    /// <summary>
    /// 一定時間経過後に、自分自身または親の GameObject を破棄するコンポーネント。
    /// パーティクル、弾丸、一時的なエフェクトなどにアタッチして使います。
    /// </summary>
    public class LifetimeTimer : MonoBehaviour
    {
        // --- 設定項目(インスペクターから編集可能) ---

        [Header("寿命設定")]

        [Tooltip("生成から何秒後に削除するか")]
        [SerializeField] private float lifetimeSeconds = 2.0f;

        [Tooltip("true の場合は親の GameObject を削除し、false の場合はこのコンポーネントが付いている GameObject を削除します")]
        [SerializeField] private bool destroyParent = false;

        [Tooltip("Time.timeScale の影響を受けない(ポーズ中もカウントする)なら true")]
        [SerializeField] private bool useUnscaledTime = false;

        [Header("オプション")]

        [Tooltip("削除前に OnBeforeDestroy イベントを発火するかどうか")]
        [SerializeField] private bool invokeEventBeforeDestroy = false;

        // 削除直前に呼ばれるイベント(任意で他コンポーネントが購読可能)
        public System.Action OnBeforeDestroy;

        // 内部用:開始時間を記録
        private float _startTime;

        private void Awake()
        {
            // コンポーネントが有効化された瞬間の時間を記録
            _startTime = GetCurrentTime();
        }

        private void OnEnable()
        {
            // 再利用(オブジェクトプール)される場合も考慮して、Enable ごとに開始時間をリセット
            _startTime = GetCurrentTime();
        }

        private void Update()
        {
            // 経過時間を計算
            float elapsed = GetCurrentTime() - _startTime;

            // 寿命を超えたら削除
            if (elapsed >= lifetimeSeconds)
            {
                HandleDestroy();
            }
        }

        /// <summary>
        /// 現在時間を取得するヘルパー。
        /// useUnscaledTime の設定に応じて Time.time または Time.unscaledTime を返す。
        /// </summary>
        private float GetCurrentTime()
        {
            return useUnscaledTime ? Time.unscaledTime : Time.time;
        }

        /// <summary>
        /// 寿命をリセットして、再度カウントし直したいときに呼び出します。
        /// オブジェクトプールなどで再利用する場合に便利です。
        /// </summary>
        public void ResetLifetime()
        {
            _startTime = GetCurrentTime();
        }

        /// <summary>
        /// 即座に対象オブジェクトを削除します。
        /// 外部スクリプトから強制的に寿命を終わらせたいときに使えます。
        /// </summary>
        public void ForceExpire()
        {
            HandleDestroy();
        }

        /// <summary>
        /// 実際の削除処理をまとめたメソッド。
        /// </summary>
        private void HandleDestroy()
        {
            // すでに Destroy 済みのケースを避けるため、null チェック
            if (this == null) return;

            // 削除前イベントを発火(オプション)
            if (invokeEventBeforeDestroy && OnBeforeDestroy != null)
            {
                OnBeforeDestroy.Invoke();
            }

            // 削除対象の GameObject を決定
            GameObject target = gameObject;

            if (destroyParent && transform.parent != null)
            {
                target = transform.parent.gameObject;
            }

            // 寿命タイマー自身が無効化されてから Destroy が呼ばれる可能性もあるので、
            // Destroy を呼んだあとにこのコンポーネントの Update が走らないように一応無効化しておく
            enabled = false;

            // Unity の Destroy で GameObject を破棄
            Destroy(target);
        }

#if UNITY_EDITOR
        private void OnValidate()
        {
            // インスペクターで値を編集したときに、最低値を保証しておく
            if (lifetimeSeconds < 0f)
            {
                lifetimeSeconds = 0f;
            }
        }
#endif
    }
}

使い方の手順

ここからは、具体的な使用例を交えながら手順を見ていきましょう。

① スクリプトをプロジェクトに追加する

  1. Unity の Project ウィンドウで Scripts フォルダなどを作成します。
  2. LifetimeTimer.cs という名前で上記コードを保存します。
  3. コンパイルが終わると、インスペクターからアタッチできるようになります。

② 弾丸プレハブに寿命を付ける例

典型的な例として「弾丸」のプレハブに寿命を持たせてみます。

  1. Hierarchy で弾丸プレハブ(例:Bullet)を開きます。
  2. 弾丸のルート GameObject(例:Bullet)を選択します。
  3. Add Component ボタンから LifetimeTimer を追加します。
  4. インスペクターで以下のように設定します:
    • Lifetime Seconds:1.5(1.5秒後に消える弾丸)
    • Destroy Parentfalse(弾丸オブジェクト自体を削除)
    • Use Unscaled Timefalse(ゲームのポーズ中は止めたい場合)

これで、弾丸は生成された地点から 1.5 秒後に自動的に Destroy() されます。
弾が壁に当たった瞬間にも消したい場合は、別のコンポーネント(衝突検知)で Destroy(gameObject) を呼べば OK です。
「寿命で消える」「当たったら消える」をそれぞれ別コンポーネントに分けられるのがポイントですね。

③ パーティクルエフェクトの親ごと削除する例

よくあるパターンとして、「爆発エフェクトの GameObject の中に、複数のパーティクルシステムやライトがぶら下がっている」構造があります。

  1. Hierarchy で ExplosionEffect という空の GameObject を作成します。
  2. その子として ParticleSystemLight などを配置します。
  3. 子オブジェクトのどれか(例:メインのパーティクル)に LifetimeTimer をアタッチします。
  4. インスペクターで以下のように設定します:
    • Lifetime Seconds:2.0(2秒後に爆発全体を削除)
    • Destroy Parenttrue(親の ExplosionEffect を削除)
    • Use Unscaled Timetrue(ポーズ中も爆発は消えてほしい場合)

これで、子オブジェクトに付けた LifetimeTimer が、2秒後に親の ExplosionEffect をまるごと削除してくれます。
「どの子に付けても親ごと消せる」ので、パーティクル構成を変えても寿命ロジックはそのまま流用できます。

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

弾丸やエフェクト以外にも、「一定時間だけ存在するギミック」にも応用できます。

  • 数秒後に消える足場(動く床)
  • 一定時間だけ開いている扉
  • 時間経過で消えるバフアイテムのエフェクト

たとえば「3秒後に消える足場」を作る場合:

  1. 足場プレハブ(例:TemporaryPlatform)を用意します。
  2. ルート GameObject に LifetimeTimer をアタッチします。
  3. Lifetime Seconds3.0 に設定。
  4. Destroy Parentfalse(ルートに付けているので親は不要)。

これで、どのシーンに配置しても「3秒で消える床」として機能します。
「動き」や「当たり判定」は別コンポーネントに任せ、寿命だけを LifetimeTimer に任せる構成にすると、とても見通しが良くなります。

メリットと応用

LifetimeTimer を導入するメリットを整理してみましょう。

1. プレハブ単位で寿命を完結できる

寿命ロジックをプレハブに内包できるので、

  • 弾丸 A は 0.5 秒で消える
  • 弾丸 B は 2 秒で消える
  • 爆発エフェクトは 1.2 秒で消える

といった違いを、Prefab のインスペクターだけで完結させられます。
発射側のスクリプト(プレイヤーや敵のシューティングロジック)は「どの弾を撃つか」だけを気にすればよく、「何秒で消えるか」は弾丸側の責務にできます。

2. レベルデザインが楽になる

ステージにオブジェクトをポンポン配置していくだけで、

  • この足場は 3 秒で消える
  • このギミックは 10 秒で消える
  • このエフェクトは 1 秒で消える

といった「時間的な演出」を、スクリプトを書き足さずにインスペクターの数値調整だけで実現できます。
レベルデザイナーや非プログラマでも、寿命をいじるだけでテンポ感を調整できるのはかなり大きな利点です。

3. Godクラスを避け、小さな責務に分割できる

寿命管理を GameManagerBulletController のような巨大クラスに押し込めるのではなく、「寿命専用コンポーネント」として切り出すことで:

  • コードの見通しが良くなる
  • テストやデバッグがしやすくなる
  • 別プロジェクトに持ち運びもしやすくなる

といったメリットが得られます。
「寿命は LifetimeTimer に任せる」というルールをチーム内で共有しておくと、プロジェクト全体がかなりスッキリします。

4. 改造案:削除前にフェードアウトさせる

最後に、LifetimeTimer をちょっと改造・拡張してみるアイデアを紹介します。
例えば「削除される直前にスプライトをフェードアウトさせたい」場合、OnBeforeDestroy を利用して別コンポーネントからこういった処理を追加できます。


using UnityEngine;

[RequireComponent(typeof(SpriteRenderer))]
[RequireComponent(typeof(Lifetime.LifetimeTimer))]
public class FadeOutBeforeDestroy : MonoBehaviour
{
    [SerializeField] private float fadeDuration = 0.3f;

    private SpriteRenderer _renderer;
    private Lifetime.LifetimeTimer _lifetime;

    private void Awake()
    {
        _renderer = GetComponent<SpriteRenderer>();
        _lifetime = GetComponent<Lifetime.LifetimeTimer>();

        // 削除前イベントにフェードアウト処理を登録
        _lifetime.OnBeforeDestroy += StartFadeOut;
    }

    private void StartFadeOut()
    {
        // 簡易的なフェードアウト(本格的にやるなら Coroutine などに分離してもOK)
        Color c = _renderer.color;
        c.a = 0f;
        _renderer.color = c;

        // ここでは説明用に即座に透明化していますが、
        // 実際には Coroutine で徐々に透明にするなどの発展もできます。
    }
}

このように、寿命そのものは LifetimeTimer に任せつつ、「削除直前の演出」は別コンポーネントで自由にカスタマイズできる構成にしておくと、プロジェクトがどんどん拡張しやすくなります。

小さなコンポーネントに責務を分割していくと、結果的に「Update に全部書いてしまう」スタイルから自然と卒業できます。
まずはパーティクルや弾丸から、この LifetimeTimer を導入してみてください。