Unityを触り始めたころによくあるのが、弾丸やエフェクトの「寿命管理」を全部 Update の中に書いてしまうパターンですね。
例えば、プレイヤーのスクリプトの Update で「弾を撃つ処理」「弾の移動」「弾の寿命タイマー」「敵に当たったら消す」などを全部まとめて書いてしまうと、すぐに数百行のGodクラスになってしまいます。
そうなると、
- 弾の種類が増えるたびに巨大スクリプトを編集しないといけない
- 別のシーンや別プロジェクトで弾だけ使い回したいのに、依存関係が多すぎて流用しづらい
- ちょっとした仕様変更(寿命を伸ばす、衝突時の挙動を変える)が地味に怖い
といった問題が出てきます。
そこでこの記事では、「弾丸の寿命管理」だけに責務を絞ったコンポーネント 「SelfDestruct」 を作っていきます。
生成から一定時間経ったら自動で消える、何かに衝突したら消える、といった処理をひとまとめにして、プレハブにポン付けできる形にしましょう。
【Unity】弾の寿命管理をコンポーネント化!「SelfDestruct」コンポーネント
フルコード(SelfDestruct.cs)
using UnityEngine;
/// <summary>
/// 一定時間後、または衝突時に「親オブジェクト」を破棄するコンポーネント。
/// 主に弾丸や一時的なエフェクト用。
///
/// - 責務は「寿命と衝突による破棄」のみ
/// - 移動処理やダメージ処理とは分離して使う
/// </summary>
[DisallowMultipleComponent]
public class SelfDestruct : MonoBehaviour
{
// ====== 設定項目 ======
[Header("寿命設定")]
[SerializeField]
[Tooltip("生成から何秒後に自動で破棄するか。0以下なら時間では破棄しない。")]
private float lifeTimeSeconds = 3f;
[SerializeField]
[Tooltip("時間経過による自動破棄を有効にするかどうか。")]
private bool useLifeTime = true;
[Header("衝突設定")]
[SerializeField]
[Tooltip("OnCollisionEnter / OnTriggerEnter で何かに当たったら破棄するか。")]
private bool destroyOnAnyCollision = true;
[SerializeField]
[Tooltip("このタグのオブジェクトに当たったときのみ破棄したい場合に指定(空文字なら無条件)。")]
private string destroyOnTag = string.Empty;
[Header("破棄対象")]
[SerializeField]
[Tooltip("破棄したいオブジェクト。未指定なら、このコンポーネントの親(transform.root)を破棄します。")]
private GameObject targetToDestroy;
[Header("デバッグ")]
[SerializeField]
[Tooltip("破棄時にDebug.Logを出すかどうか。")]
private bool logOnDestroy = false;
// 内部で使うタイマー
private float _timer = 0f;
private void Awake()
{
// 破棄対象が指定されていない場合は、ルートオブジェクトを対象にする
if (targetToDestroy == null)
{
// 「親を消去したい」という要件に合わせて、ルートを対象にします。
// 弾丸が子オブジェクトとして発射されるケースを想定。
targetToDestroy = transform.root.gameObject;
}
}
private void Update()
{
// 時間による自動破棄が無効なら何もしない
if (!useLifeTime)
{
return;
}
// 経過時間を加算
_timer += Time.deltaTime;
// lifeTimeSeconds が 0 以下なら、時間では破棄しない
if (lifeTimeSeconds > 0f && _timer >= lifeTimeSeconds)
{
DestroyTarget("LifeTime");
}
}
/// <summary>
/// 物理コリジョン(非トリガー)に衝突したときに呼ばれる
/// </summary>
/// <param name="collision">衝突情報</param>
private void OnCollisionEnter(Collision collision)
{
if (!destroyOnAnyCollision)
{
return;
}
if (!IsValidTag(collision.gameObject))
{
return;
}
DestroyTarget("CollisionEnter with " + collision.gameObject.name);
}
/// <summary>
/// トリガーコリジョンに入ったときに呼ばれる
/// (弾丸をTriggerにしているケースなど)
/// </summary>
/// <param name="other">接触したCollider</param>
private void OnTriggerEnter(Collider other)
{
if (!destroyOnAnyCollision)
{
return;
}
if (!IsValidTag(other.gameObject))
{
return;
}
DestroyTarget("TriggerEnter with " + other.gameObject.name);
}
/// <summary>
/// タグ条件を満たすかどうかを判定するヘルパー
/// </summary>
/// <param name="other">接触した相手オブジェクト</param>
/// <returns>破棄条件を満たしているか</returns>
private bool IsValidTag(GameObject other)
{
// destroyOnTag が空なら、タグ条件なし(何に当たってもOK)
if (string.IsNullOrEmpty(destroyOnTag))
{
return true;
}
// タグが一致したときのみ破棄
return other.CompareTag(destroyOnTag);
}
/// <summary>
/// 実際に破棄を実行する共通処理
/// </summary>
/// <param name="reason">ログ用の理由文字列</param>
private void DestroyTarget(string reason)
{
if (targetToDestroy == null)
{
// すでに手動でDestroyされているなど
return;
}
if (logOnDestroy)
{
Debug.Log($"[SelfDestruct] Destroy {targetToDestroy.name} ({reason})", this);
}
// 対象を破棄
Destroy(targetToDestroy);
// 自分自身も不要なので一応参照をクリア
targetToDestroy = null;
}
// ====== 公開API(他コンポーネントから制御したい場合用) ======
/// <summary>
/// 外部から明示的に破棄をトリガーしたいときに呼ぶ関数。
/// 例)ダメージ計算が終わった後に弾を消すなど。
/// </summary>
public void ForceDestroy()
{
DestroyTarget("ForceDestroy");
}
/// <summary>
/// 寿命タイマーをリセットしたい場合に呼ぶ。
/// 例)オブジェクトプールから再利用した直後に呼ぶなど。
/// </summary>
public void ResetLifeTimer()
{
_timer = 0f;
}
}
使い方の手順
-
弾丸プレハブを用意する
例として、以下のような「弾丸」オブジェクト(Prefab)を作っておきます。- Sphere などの Mesh(または Sprite)
Rigidbody(物理挙動で飛ばす場合)Collider(SphereCollider など)を追加- 物理衝突で当たり判定を取りたい:Is Trigger をオフ
- トリガー判定だけ取りたい:Is Trigger をオン
-
SelfDestruct コンポーネントをアタッチ
弾丸プレハブのルートオブジェクト(または、子オブジェクト)にSelfDestructを追加します。
デフォルト設定のままでも動きますが、必要に応じて以下を調整しましょう。Use Life Time:時間経過で自動破棄するかLife Time Seconds:何秒後に破棄するか(例:3秒)Destroy On Any Collision:衝突時に破棄するかDestroy On Tag:特定のタグに当たったときだけ破棄したい場合は、「Enemy」などのタグ名を指定Target To Destroy:破棄したいオブジェクト。未指定ならルートオブジェクトが破棄されます
「弾丸の見た目は子オブジェクト、ルートに弾丸の論理オブジェクトがある」ような構造でも、
ルートを消したい場合はTarget To Destroyを空にしておけばOKです。 -
発射処理から弾丸を生成する
プレイヤーや敵の「発射スクリプト」から、通常通りInstantiateで弾丸プレハブを生成します。
SelfDestruct は生成された瞬間から自動で寿命カウントを始めるので、発射側は特別な処理は不要です。// 例: シンプルな発射スクリプト using UnityEngine; public class SimpleShooter : MonoBehaviour { [SerializeField] private GameObject bulletPrefab; [SerializeField] private Transform firePoint; [SerializeField] private float bulletSpeed = 10f; private void Update() { if (Input.GetKeyDown(KeyCode.Space)) { // 弾丸を生成 GameObject bullet = Instantiate( bulletPrefab, firePoint.position, firePoint.rotation ); // 物理で前方に飛ばす if (bullet.TryGetComponent(out Rigidbody rb)) { rb.velocity = firePoint.forward * bulletSpeed; } } } }弾丸プレハブに
SelfDestructが付いていれば、
「一定時間後」もしくは「衝突時」に自動で親オブジェクトごと破棄されます。 -
他のオブジェクトにも流用する
弾丸以外にも、例えば以下のようなケースでそのまま使い回せます。- 一定時間だけ表示したいヒットエフェクト(爆発、斬撃エフェクトなど)
- 時間で消えるトラップ(一定時間後に消えるトゲ床など)
- 敵が撃つ弾丸、味方が撃つ弾丸、ボスの特殊弾など
どれも「寿命」と「衝突で消える」という共通のルールなので、
プレハブにSelfDestructをペタッと貼るだけで完結します。
メリットと応用
SelfDestruct コンポーネントを使うことで、以下のようなメリットがあります。
- 弾丸ロジックから「寿命管理」が分離される
弾丸の移動・ダメージ計算・エフェクト再生などは別コンポーネントに分けられるので、
それぞれの責務が小さくなり、コードが読みやすくなります。 - プレハブ単位で挙動を完結できる
「この弾は5秒で消える」「この弾は敵に当たったらだけ消える」「このエフェクトは時間だけで消える」など、
プレハブのインスペクタ上だけで設定を変えられるので、レベルデザインやパラメータ調整が楽になります。 - 再利用性が高い
どのシーン・どのプロジェクトでも「寿命付きオブジェクト」としてそのまま使い回せます。
発射側のスクリプトに手を入れる必要がないのも嬉しいポイントですね。 - オブジェクトプールとの相性も良い
プールから取り出したときにResetLifeTimer()を呼ぶだけで寿命をリセットできるので、
破棄ではなく再利用する設計にもスムーズに移行できます。
さらに、改造次第でいろいろなバリエーションが作れます。例えば、
「破棄する前に小さなエフェクトを出したい」という要望が出たとしましょう。
そんなときは、以下のような関数を追加するだけでも十分に応用できます。
[SerializeField]
[Tooltip("破棄時に生成するエフェクトプレハブ(任意)。")]
private GameObject destroyEffectPrefab;
/// <summary>
/// 破棄時にエフェクトを再生する例。
/// DestroyTarget 内でこの関数を呼び出すようにすればOK。
/// </summary>
private void SpawnDestroyEffect()
{
if (destroyEffectPrefab == null || targetToDestroy == null)
{
return;
}
// 破棄対象の位置と回転でエフェクトを生成
Instantiate(
destroyEffectPrefab,
targetToDestroy.transform.position,
targetToDestroy.transform.rotation
);
}
このように、「寿命管理」という1つの責務にフォーカスしたコンポーネントを用意しておくと、
弾丸やエフェクト、トラップなどのプレハブ設計・レベルデザインがかなりスッキリします。
巨大な Update にロジックを詰め込むのではなく、小さなコンポーネントを組み合わせていくスタイルを意識していきましょう。




