Unityを触り始めた頃って、つい何でもかんでも Update() に書きたくなりますよね。プレイヤーの入力処理、敵AI、エフェクト制御、スコア更新…全部を1つのスクリプトに押し込んでしまうと、最初は動いても、あとから「どこを直せばいいのか」「どこがバグっているのか」が一気に分かりにくくなります。

とくに「爆弾」や「弾丸」のような、一時的に存在して複雑な挙動をするオブジェクトは、つい巨大なGodクラスに押し込まれがちです。

そこで今回は、「吸着爆弾」という1つの役割にだけフォーカスしたコンポーネント StickyBomb を作っていきましょう。役割はシンプルです。

  • 何か(敵など)に当たったら、そのオブジェクトの子ノードになる(くっつく)
  • 数秒後に爆発してダメージを与え、自分自身は破壊される

このロジックを1つのコンポーネントに閉じ込めておくことで、プレイヤー・敵・ギミックなど、どこからでも簡単に「吸着爆弾」を使い回せるようになります。

【Unity】敵にペタッと貼り付く爆弾!「StickyBomb」コンポーネント

フルコード


using System.Collections;
using UnityEngine;

/// <summary>爆発ダメージを受け取れる対象のインターフェース</summary>
public interface IExplosionDamageReceiver
{
    /// <summary>爆発によるダメージを受け取る</summary>
    /// <param name="damage">ダメージ量</param>
    /// <param name="hitPoint">爆心地のワールド座標</param>
    void ReceiveExplosionDamage(float damage, Vector3 hitPoint);
}

/// <summary>
/// 吸着爆弾コンポーネント。
/// - 何かに衝突したら、そのオブジェクトにくっつく(子ノード化)
/// - 一定時間後に爆発し、周囲にダメージを与えて自壊する
/// </summary>
[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(Collider))]
public class StickyBomb : MonoBehaviour
{
    [Header("基本設定")]
    [SerializeField] private float fuseTime = 3.0f;      // 起爆までの時間(秒)
    [SerializeField] private float explosionRadius = 5f; // 爆発半径
    [SerializeField] private float explosionDamage = 50f;// ダメージ量

    [Header("見た目・演出")]
    [SerializeField] private GameObject explosionEffectPrefab; // 爆発エフェクト(任意)

    [Header("物理挙動")]
    [SerializeField] private bool stickOnFirstHitOnly = true;  // 最初に当たったものにだけ吸着するか
    [SerializeField] private bool disablePhysicsAfterStick = true; // 吸着後に物理挙動を止めるか

    private Rigidbody _rigidbody;
    private Collider _collider;
    private bool _hasStuck = false;        // すでに何かにくっついたか
    private bool _hasExploded = false;     // すでに爆発したか
    private Coroutine _fuseCoroutine;      // 起爆カウントダウン用コルーチン

    private void Awake()
    {
        // 必須コンポーネントのキャッシュ
        _rigidbody = GetComponent<Rigidbody>();
        _collider = GetComponent<Collider>();

        // 爆弾自身のコライダーは「trigger ではない」ことを想定
        // (物理的に衝突して貼り付くため)
        _collider.isTrigger = false;
    }

    private void OnEnable()
    {
        // 有効化されたタイミングでヒューズ(導火線)を開始
        _fuseCoroutine = StartCoroutine(FuseTimer());
    }

    private void OnDisable()
    {
        // 無効化時はコルーチンを止めておく
        if (_fuseCoroutine != null)
        {
            StopCoroutine(_fuseCoroutine);
            _fuseCoroutine = null;
        }
    }

    /// <summary>
    /// 起爆までのカウントダウンを行うコルーチン
    /// </summary>
    private IEnumerator FuseTimer()
    {
        // 設定時間だけ待機
        yield return new WaitForSeconds(fuseTime);

        // 時間が来たら爆発
        Explode();
    }

    /// <summary>
    /// 物理衝突時に呼ばれる(非Trigger)
    /// </summary>
    /// <param name="collision">衝突情報</param>
    private void OnCollisionEnter(Collision collision)
    {
        // すでにくっついていて、かつ「最初に当たったものだけに吸着」設定なら何もしない
        if (_hasStuck && stickOnFirstHitOnly)
        {
            return;
        }

        StickToTarget(collision.transform, collision.GetContact(0).point, collision.GetContact(0).normal);
    }

    /// <summary>
    /// 対象に吸着して子オブジェクトになる処理
    /// </summary>
    /// <param name="target">くっつく相手のTransform</param>
    /// <param name="hitPoint">衝突地点のワールド座標</param>
    /// <param name="hitNormal">衝突面の法線ベクトル</param>
    private void StickToTarget(Transform target, Vector3 hitPoint, Vector3 hitNormal)
    {
        _hasStuck = true;

        // 衝突位置に爆弾を移動
        transform.position = hitPoint;

        // 衝突面の法線方向を「上」か「前」に向けることで、それっぽい向きに貼り付ける
        // ここでは forward を法線に合わせる
        if (hitNormal != Vector3.zero)
        {
            transform.rotation = Quaternion.LookRotation(hitNormal);
        }

        // 親子関係を設定(吸着)
        transform.SetParent(target);

        if (disablePhysicsAfterStick)
        {
            // 吸着後は物理挙動を止めて、対象にぴったり追従させる
            _rigidbody.isKinematic = true;
            _rigidbody.velocity = Vector3.zero;
            _rigidbody.angularVelocity = Vector3.zero;
        }
    }

    /// <summary>
    /// 爆発処理本体
    /// </summary>
    private void Explode()
    {
        if (_hasExploded)
        {
            return;
        }
        _hasExploded = true;

        Vector3 center = transform.position;

        // 爆発エフェクトを生成(任意)
        if (explosionEffectPrefab != null)
        {
            Instantiate(explosionEffectPrefab, center, Quaternion.identity);
        }

        // 爆発半径内にある全ての Collider を取得
        Collider[] hitColliders = Physics.OverlapSphere(center, explosionRadius);

        foreach (Collider hit in hitColliders)
        {
            // 自分自身は無視
            if (hit.attachedRigidbody != null && hit.attachedRigidbody.gameObject == gameObject)
            {
                continue;
            }

            // IExplosionDamageReceiver を探してダメージを通知
            // 自身に付いていなければ、親オブジェクトにも探しにいく
            IExplosionDamageReceiver damageReceiver =
                hit.GetComponentInParent<IExplosionDamageReceiver>();

            if (damageReceiver != null)
            {
                damageReceiver.ReceiveExplosionDamage(explosionDamage, center);
            }

            // 物理オブジェクトなら、簡易的な爆風力を加える
            Rigidbody rb = hit.attachedRigidbody;
            if (rb != null)
            {
                // 爆心地から外向きに力を与える
                Vector3 direction = (rb.worldCenterOfMass - center).normalized;
                float distance = Vector3.Distance(rb.worldCenterOfMass, center);
                float force = Mathf.Lerp(0f, explosionDamage, 1f - (distance / explosionRadius));

                rb.AddForce(direction * force, ForceMode.Impulse);
            }
        }

        // 爆弾本体を破壊
        Destroy(gameObject);
    }

    /// <summary>
    /// デバッグ用にシーンビューで爆発範囲を可視化
    /// </summary>
    private void OnDrawGizmosSelected()
    {
        Gizmos.color = new Color(1f, 0.4f, 0f, 0.4f);
        Gizmos.DrawWireSphere(transform.position, explosionRadius);
    }

    #region 公開API

    /// <summary>
    /// 外部から強制的に起爆させたい場合に呼ぶ
    /// 例:リモコン爆弾、チェーン爆発など
    /// </summary>
    public void ForceExplode()
    {
        // すでに爆発済みなら何もしない
        if (_hasExploded)
        {
            return;
        }

        // 起爆カウントダウンを止めて即爆発
        if (_fuseCoroutine != null)
        {
            StopCoroutine(_fuseCoroutine);
            _fuseCoroutine = null;
        }

        Explode();
    }

    #endregion
}

使い方の手順

ここからは、具体的な使い方をステップ形式で見ていきましょう。例として「プレイヤーが投げる吸着爆弾」「敵に貼り付いて爆発する」を想定します。

手順① StickyBomb プレハブを作る

  1. Hierarchy で右クリック > 3D Object > Sphere などで爆弾用オブジェクトを作成します。
  2. オブジェクト名を StickyBomb に変更します。
  3. 以下のコンポーネントを追加・設定します。
    • Rigidbody(Use Gravity: ON / Constraints はお好みで)
    • Collider(SphereCollider など。Is Trigger: OFF)
    • 上記コードの StickyBomb スクリプト
  4. Project ビューにドラッグして Prefab 化します。
  5. 爆発エフェクトを持っている場合は、explosionEffectPrefab に割り当てます(なくても動作します)。

手順② 敵に「ダメージを受けられる」コンポーネントを付ける

爆発のダメージは IExplosionDamageReceiver で受け取る設計にしているので、敵側にこのインターフェースを実装したコンポーネントを付けます。


using UnityEngine;

/// <summary>シンプルな敵のヘルス管理</summary>
public class SimpleEnemyHealth : MonoBehaviour, IExplosionDamageReceiver
{
    [SerializeField] private float maxHealth = 100f;
    private float _currentHealth;

    private void Awake()
    {
        _currentHealth = maxHealth;
    }

    public void ReceiveExplosionDamage(float damage, Vector3 hitPoint)
    {
        _currentHealth -= damage;
        Debug.Log($"{name} は爆発ダメージ {damage} を受けた! 残りHP: {_currentHealth}");

        if (_currentHealth <= 0f)
        {
            Die();
        }
    }

    private void Die()
    {
        Debug.Log($"{name} は倒れた!");
        // TODO: 死亡アニメーションやスコア加算など
        Destroy(gameObject);
    }
}

この SimpleEnemyHealth を敵プレハブのルートか、適切な親オブジェクトにアタッチしておきましょう。

手順③ プレイヤーから StickyBomb を投げる

次に、プレイヤーが吸着爆弾を投げるための簡単なコンポーネント例です。プレイヤーのカメラ前方に向かって爆弾を発射するようにしています。


using UnityEngine;

/// <summary>プレイヤーから吸着爆弾を発射するサンプル</summary>
public class StickyBombLauncher : MonoBehaviour
{
    [SerializeField] private StickyBomb stickyBombPrefab; // 投げる爆弾のプレハブ
    [SerializeField] private Transform firePoint;         // 発射位置(カメラ前方など)
    [SerializeField] private float launchForce = 15f;     // 投擲の初速
    [SerializeField] private KeyCode fireKey = KeyCode.Mouse1; // 右クリックで発射

    private void Update()
    {
        if (Input.GetKeyDown(fireKey))
        {
            LaunchBomb();
        }
    }

    private void LaunchBomb()
    {
        if (stickyBombPrefab == null || firePoint == null)
        {
            Debug.LogWarning("StickyBombLauncher: プレハブまたは発射位置が設定されていません。");
            return;
        }

        // プレハブから爆弾を生成
        StickyBomb bombInstance = Instantiate(
            stickyBombPrefab,
            firePoint.position,
            firePoint.rotation
        );

        // 初速を与える
        Rigidbody rb = bombInstance.GetComponent<Rigidbody>();
        if (rb != null)
        {
            rb.velocity = firePoint.forward * launchForce;
        }
    }
}

このコンポーネントをプレイヤー(もしくはカメラ)に付けて、firePoint にはカメラの前方位置に置いた空の GameObject を割り当てておくと、右クリックで吸着爆弾を投げられるようになります。

手順④ 動く床やギミックにも貼り付ける

StickyBomb は「何に当たったか」を特に限定していないので、敵だけでなく以下のような使い方もできます。

  • 動く床にくっつけて、プレイヤーが乗ると一緒に移動する爆弾床
  • 回転するギアに貼り付く爆弾ギミック
  • ボスの特定の部位(弱点コライダー)にだけダメージを与える吸着爆弾

いずれも、対象側に IExplosionDamageReceiver を実装したコンポーネントさえ付けておけば、爆発ダメージを受け取れるようになります。

メリットと応用

StickyBomb を1つのコンポーネントとして切り出しておくメリットはたくさんあります。

  • プレハブ化して再利用しやすい
    – プレイヤー用、敵用、ギミック用など、同じ StickyBomb プレハブをシーン中のどこからでも投げられます。
  • レベルデザインが楽になる
    – レベルデザイナーは「この敵は爆弾に弱い」「このスイッチは爆弾で起動する」といったギミックを、コンポーネントの組み合わせだけで作れます。
  • 責務が明確で保守しやすい
    – 「爆弾の挙動を変えたい」と思ったときは StickyBomb だけを修正すればOK。プレイヤーや敵のスクリプトをいじる必要がありません。
  • インターフェースで依存を緩く保てる
    – ダメージ処理は IExplosionDamageReceiver に委譲しているので、敵の実装が変わっても StickyBomb 側はそのまま使い続けられます。

さらに、この記事の StickyBomb は「時間経過で自動起爆」+「外部からの強制起爆」の2パターンをサポートしているので、例えば以下のような応用ができます。

  • リモコン式:プレイヤーが任意のタイミングで ForceExplode() を呼び出して起爆
  • チェーン爆発:ある爆弾の爆発に巻き込まれた他の StickyBomb を一斉に起爆
  • パズル要素:特定のスイッチに吸着したときだけ爆発有効、などの条件付き起爆

改造案:チェーン爆発用の「爆風で起爆される」メソッドを追加

例えば、他の爆発に巻き込まれたときに少しだけディレイを入れて起爆する「チェーン爆発」を実装したい場合は、StickyBomb に次のようなメソッドを追加するのも面白いです。


    /// <summary>
    /// 他の爆発に巻き込まれたときに呼び出すと、
    /// 少し遅れて起爆するチェーン爆発用メソッド
    /// </summary>
    public void TriggerChainExplosion(float delay = 0.3f)
    {
        if (_hasExploded)
        {
            return;
        }

        // すでに通常のヒューズが動いていれば止める
        if (_fuseCoroutine != null)
        {
            StopCoroutine(_fuseCoroutine);
            _fuseCoroutine = null;
        }

        // 指定ディレイ後に起爆
        StartCoroutine(ChainExplosionRoutine(delay));
    }

    private IEnumerator ChainExplosionRoutine(float delay)
    {
        yield return new WaitForSeconds(delay);
        Explode();
    }

これを使えば、Explode() 内で「半径内にある他の StickyBomb を探して TriggerChainExplosion() を呼ぶ」といった、派手な連鎖爆発も簡単に実現できます。小さなコンポーネント単位で責務を分けておくと、こうした拡張も楽になりますね。