Unityを触り始めた頃って、つい Update() にプレイヤー操作も敵AIもエフェクト制御も全部書いてしまいがちですよね。動きはするけど、少し仕様変更が入るだけで「どこを触ればいいんだ…?」となるパターンです。

特に敵AIは、移動・索敵・攻撃・アニメーション・HP管理などがごちゃ混ぜになりやすく、あっという間にGodクラス化してしまいます。

そこでこの記事では、「特攻してくる敵」というよくあるギミックを、1つの責務に絞ったコンポーネントとして切り出してみます。
今回作る 「Kamikaze」コンポーネント は、

  • ターゲットを発見したら移動速度を上げて突進する
  • ターゲットに接触した瞬間に自爆ダメージを与え、自分は消える

という「特攻AI」だけに責務を限定したコンポーネントです。
ダメージの適用は IDamageable インターフェースに委譲することで、プレイヤーでも敵でもオブジェクトでも、同じ仕組みでダメージを受けられるようにしていきます。

【Unity】見つけたら即特攻!「Kamikaze」コンポーネント

フルコード


using UnityEngine;

#region ダメージインターフェース

/// <summary>ダメージを受けられるオブジェクト用インターフェース</summary>
public interface IDamageable
{
    /// <summary>ダメージを受けたときに呼ばれる</summary>
    /// <param name="amount">ダメージ量</param>
    /// <param name="source">ダメージの発生源(攻撃者など)</param>
    void TakeDamage(float amount, GameObject source);
}

#endregion

/// <summary>
/// 特攻AIコンポーネント。
/// - ターゲットが一定距離内に入るとスピードアップして突進
/// - ターゲットに接触した瞬間に自爆ダメージを与えて自壊
/// </summary>
[RequireComponent(typeof(Rigidbody))]
public class Kamikaze : MonoBehaviour
{
    [Header("ターゲット設定")]
    [SerializeField]
    private Transform target; // 追いかける対象(例: プレイヤー)

    [SerializeField]
    private string targetTag = "Player"; 
    // target が未設定のとき、このタグを持つオブジェクトを自動探索

    [Header("移動設定")]
    [SerializeField]
    private float normalSpeed = 2f;   // 通常時の移動速度

    [SerializeField]
    private float chargeSpeed = 6f;   // 特攻時の移動速度

    [SerializeField]
    private float detectionRange = 8f; // ターゲット発見距離

    [SerializeField]
    private float stopDistance = 0.5f; // これ以下なら移動をやめる距離

    [Header("自爆設定")]
    [SerializeField]
    private float explosionDamage = 30f; // 与えるダメージ

    [SerializeField]
    private float explosionRadius = 0f;
    // 0 の場合は「接触した相手のみにダメージ」
    // > 0 の場合は「周囲に範囲ダメージ」

    [SerializeField]
    private LayerMask explosionLayerMask = ~0;
    // 範囲ダメージの対象レイヤー(デフォルトは全部)

    [SerializeField]
    private bool destroyOnExplode = true; // 自爆後に自分を破壊するか

    [Header("挙動オプション")]
    [SerializeField]
    private bool faceTarget = true; // 移動方向を向くかどうか

    [SerializeField]
    private float rotationSpeed = 10f; // 向きを変えるスピード

    [SerializeField]
    private float lifeTime = 0f;
    // 0 の場合は無制限。>0 の場合は寿命で自爆(ダメージは与えない)

    [Header("デバッグ")]
    [SerializeField]
    private bool drawGizmos = true;

    private Rigidbody _rigidbody;
    private bool _isCharging;
    private float _spawnTime;

    private void Awake()
    {
        _rigidbody = GetComponent<Rigidbody>();

        // 物理挙動を制御しやすくするため、回転を固定しておくと扱いやすい
        _rigidbody.constraints = RigidbodyConstraints.FreezeRotationX |
                                 RigidbodyConstraints.FreezeRotationZ;
    }

    private void Start()
    {
        _spawnTime = Time.time;

        // ターゲットが未設定なら、タグから自動検索
        if (target == null && !string.IsNullOrEmpty(targetTag))
        {
            GameObject found = GameObject.FindGameObjectWithTag(targetTag);
            if (found != null)
            {
                target = found.transform;
            }
        }
    }

    private void FixedUpdate()
    {
        // 寿命が設定されている場合、時間切れで自爆(ダメージなし)
        if (lifeTime > 0f && Time.time - _spawnTime >= lifeTime)
        {
            SelfDestructOnly();
            return;
        }

        if (target == null)
        {
            // ターゲットがいない場合は何もしない
            _rigidbody.velocity = Vector3.zero;
            return;
        }

        // ターゲットとの距離
        Vector3 toTarget = target.position - transform.position;
        float distance = toTarget.magnitude;

        // 発見距離以内に入ったら特攻モードへ
        _isCharging = distance <= detectionRange;

        // 近づきすぎたら停止
        if (distance <= stopDistance)
        {
            _rigidbody.velocity = Vector3.zero;
            return;
        }

        // 正規化した方向ベクトル
        Vector3 direction = toTarget.normalized;

        // 現在の速度
        float currentSpeed = _isCharging ? chargeSpeed : normalSpeed;

        // 平面上(X-Z平面)だけで移動したい場合は、Y成分をゼロにする
        direction.y = 0f;

        // Rigidbody に速度を直接設定
        _rigidbody.velocity = direction * currentSpeed;

        // ターゲットの方向を向く
        if (faceTarget && direction.sqrMagnitude > 0.0001f)
        {
            Quaternion targetRotation = Quaternion.LookRotation(direction, Vector3.up);
            transform.rotation = Quaternion.Slerp(
                transform.rotation,
                targetRotation,
                rotationSpeed * Time.fixedDeltaTime
            );
        }
    }

    /// <summary>
    /// 何かに接触したときに呼ばれる(当たり判定は Collider の設定に依存)
    /// </summary>
    /// <param name="other">接触した Collider</param>
    private void OnCollisionEnter(Collision other)
    {
        // 自爆処理
        Explode(other.collider);
    }

    /// <summary>
    /// トリガーで当たった場合にも対応したい場合はこちらを使用
    /// (必要なければ削除してもOK)
    /// </summary>
    /// <param name="other">接触した Collider</param>
    private void OnTriggerEnter(Collider other)
    {
        Explode(other);
    }

    /// <summary>
    /// 自爆処理本体。接触相手、もしくは範囲内のオブジェクトにダメージを与える。
    /// </summary>
    /// <param name="hitCollider">最初に接触したコライダー</param>
    private void Explode(Collider hitCollider)
    {
        // すでに破壊されていたら何もしない
        if (!gameObject.activeInHierarchy) return;

        // 範囲ダメージが無効(radius == 0)の場合:
        // 接触した相手だけにダメージ
        if (Mathf.Approximately(explosionRadius, 0f))
        {
            TryApplyDamage(hitCollider.gameObject);
        }
        else
        {
            // 範囲ダメージが有効な場合:OverlapSphereで周囲を探索
            Collider[] hits = Physics.OverlapSphere(
                transform.position,
                explosionRadius,
                explosionLayerMask,
                QueryTriggerInteraction.Ignore
            );

            foreach (Collider col in hits)
            {
                TryApplyDamage(col.gameObject);
            }
        }

        // 自分自身を破壊
        if (destroyOnExplode)
        {
            Destroy(gameObject);
        }
    }

    /// <summary>
    /// ダメージを適用できるオブジェクトならダメージを与える。
    /// </summary>
    /// <param name="targetObject">ダメージを与えたいオブジェクト</param>
    private void TryApplyDamage(GameObject targetObject)
    {
        if (targetObject == null) return;

        // IDamageable を実装しているコンポーネントを探す
        IDamageable damageable = targetObject.GetComponent<IDamageable>();
        if (damageable != null)
        {
            damageable.TakeDamage(explosionDamage, gameObject);
        }
    }

    /// <summary>
    /// ダメージを与えずに自分だけ消える自爆(寿命切れなどに使用)
    /// </summary>
    private void SelfDestructOnly()
    {
        if (destroyOnExplode)
        {
            Destroy(gameObject);
        }
    }

    #region デバッグ描画

    private void OnDrawGizmosSelected()
    {
        if (!drawGizmos) return;

        // 発見範囲
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(transform.position, detectionRange);

        // 範囲ダメージ
        if (explosionRadius > 0f)
        {
            Gizmos.color = new Color(1f, 0.3f, 0f, 0.8f); // オレンジ
            Gizmos.DrawWireSphere(transform.position, explosionRadius);
        }

        // 停止距離
        Gizmos.color = Color.cyan;
        Gizmos.DrawWireSphere(transform.position, stopDistance);
    }

    #endregion
}

使い方の手順

  1. ① プレイヤー側に「IDamageable」実装を追加する
    まずはダメージを受ける側を用意します。プレイヤーのHP管理用スクリプトなどに、IDamageable を実装しましょう。
    
    using UnityEngine;
    
    /// <summary>シンプルなプレイヤーHPコンポーネントの例</summary>
    public class PlayerHealth : MonoBehaviour, IDamageable
    {
        [SerializeField]
        private float maxHealth = 100f;
    
        [SerializeField]
        private float currentHealth;
    
        private void Awake()
        {
            currentHealth = maxHealth;
        }
    
        public void TakeDamage(float amount, GameObject source)
        {
            currentHealth -= amount;
            Debug.Log($"Player took {amount} damage from {source.name}. HP: {currentHealth}");
    
            if (currentHealth <= 0f)
            {
                Die();
            }
        }
    
        private void Die()
        {
            Debug.Log("Player Dead");
            // TODO: リスポーン処理やゲームオーバー演出など
        }
    }
    

    プレイヤーの GameObject に PlayerHealth をアタッチし、TagPlayer にしておきます。

  2. ② 特攻敵用のプレハブを作る
    Unityエディタで空の GameObject を作成し、名前を KamikazeEnemy などに変更します。
    そのオブジェクトに以下を追加します。
    • Rigidbody(Use Gravity ON, Constraints で X/Z 回転を Freeze 推奨)
    • Collider(SphereCollider など。Is Trigger は好みで)
    • Kamikaze コンポーネント(上のスクリプト)

    Kamikaze のインスペクタで、

    • Target を空にする(→ targetTag で自動取得)
    • Target TagPlayer に設定
    • Normal Speed, Charge Speed, Detection Range などを好みで調整
    • 自爆で巻き込みダメージを出したい場合は Explosion Radius を 2〜3 などに設定

    作成したらプレハブ化しておきましょう。

  3. ③ シーンに配置して動作確認
    シーンにプレイヤー(PlayerHealth 付き)を配置し、特攻敵プレハブを複数並べてみましょう。
    再生すると、
    • プレイヤーが発見範囲に入るまでゆっくり移動(または停止)
    • 範囲に入った瞬間、特攻速度 で一気に接近
    • 接触した瞬間にプレイヤーにダメージを与え、自分は消滅

    という挙動になります。
    Gizmos を ON にしておくと、発見範囲や爆発範囲がシーンビューに表示されるので、レベルデザイン時に便利です。

  4. ④ 応用例:動く床やオブジェクトにもダメージを与える
    IDamageable を実装していれば、プレイヤー以外にも同じ仕組みでダメージを与えられます。
    例えば、「ダメージを受けると壊れる橋」などを作る場合は、橋のオブジェクトにこんなコンポーネントを付けるだけです。
    
    using UnityEngine;
    
    public class BreakablePlatform : MonoBehaviour, IDamageable
    {
        [SerializeField]
        private float durability = 50f;
    
        public void TakeDamage(float amount, GameObject source)
        {
            durability -= amount;
            if (durability <= 0f)
            {
                Debug.Log($"{name} is broken by {source.name}");
                Destroy(gameObject);
            }
        }
    }
    

    これで、特攻敵がぶつかると橋が壊れる、といったギミックを簡単に追加できます。

メリットと応用

Kamikaze コンポーネントを使うメリットは、敵の「特攻行動」だけを独立した責務として切り出している点にあります。

  • 移動・索敵・自爆ダメージが 1 コンポーネントにまとまっているので、他の敵AIと組み合わせやすい
  • プレイヤーやオブジェクト側は IDamageable を実装するだけでよく、敵の実装詳細を知らなくていい
  • プレハブ化しておけば、ステージ上にポンポン置くだけで「特攻ポイント」を増やせる
  • Gizmos で発見範囲・爆発範囲が見えるので、レベルデザイン時に「ここは危険地帯」という演出をしやすい

特にプレハブ管理の観点では、

  • 「普通に追いかけてくる敵」プレハブ
  • 「特攻だけする敵」プレハブ(今回の Kamikaze)
  • 「遠距離攻撃だけする敵」プレハブ

のように、コンポーネントの組み合わせでバリエーションを作れるので、スクリプトを書き換えずにゲームデザイン側で差別化しやすくなります。

例えば、「普段は歩いているけど、HPが減ると特攻モードに入るボス」などを作る場合も、KamikazechargeSpeeddetectionRange を外部から切り替えるだけでOKです。

改造案:外部から強制的に特攻モードにする

ボス戦などで「合図と同時に一斉特攻させたい」ときのために、Kamikaze にこんなメソッドを追加しても面白いです。


    /// <summary>
    /// 外部から強制的に特攻モードを開始する。
    /// 例:ボスのフェーズ移行時に全Kamikazeを一斉突撃させる。
    /// </summary>
    public void ForceChargeTo(Transform newTarget, float? overrideChargeSpeed = null)
    {
        if (newTarget != null)
        {
            target = newTarget;
        }

        if (overrideChargeSpeed.HasValue)
        {
            chargeSpeed = overrideChargeSpeed.Value;
        }

        // 次のFixedUpdateから常に特攻扱いにするため、検知距離を広げる
        detectionRange = float.MaxValue;
        _isCharging = true;
    }

ステージ演出用のスクリプトから、


foreach (var kamikaze in FindObjectsOfType<Kamikaze>())
{
    kamikaze.ForceChargeTo(playerTransform, 10f);
}

のように呼び出せば、「ボスの咆哮と同時に周囲の雑魚が一斉特攻」みたいな派手な演出も簡単に実装できます。

こうやって小さな責務のコンポーネントを積み重ねていくと、プロジェクト全体がかなりスッキリしていくので、ぜひ自分のゲーム用にアレンジしてみてください。