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
}
使い方の手順
-
① プレイヤー側に「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をアタッチし、TagをPlayerにしておきます。 -
② 特攻敵用のプレハブを作る
Unityエディタで空の GameObject を作成し、名前をKamikazeEnemyなどに変更します。
そのオブジェクトに以下を追加します。- Rigidbody(Use Gravity ON, Constraints で X/Z 回転を Freeze 推奨)
- Collider(SphereCollider など。
Is Triggerは好みで) - Kamikaze コンポーネント(上のスクリプト)
Kamikazeのインスペクタで、Targetを空にする(→targetTagで自動取得)Target TagをPlayerに設定Normal Speed,Charge Speed,Detection Rangeなどを好みで調整- 自爆で巻き込みダメージを出したい場合は
Explosion Radiusを 2〜3 などに設定
作成したらプレハブ化しておきましょう。
-
③ シーンに配置して動作確認
シーンにプレイヤー(PlayerHealth付き)を配置し、特攻敵プレハブを複数並べてみましょう。
再生すると、- プレイヤーが発見範囲に入るまでゆっくり移動(または停止)
- 範囲に入った瞬間、特攻速度 で一気に接近
- 接触した瞬間にプレイヤーにダメージを与え、自分は消滅
という挙動になります。
Gizmos を ON にしておくと、発見範囲や爆発範囲がシーンビューに表示されるので、レベルデザイン時に便利です。 -
④ 応用例:動く床やオブジェクトにもダメージを与える
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が減ると特攻モードに入るボス」などを作る場合も、Kamikaze の chargeSpeed や detectionRange を外部から切り替えるだけで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);
}
のように呼び出せば、「ボスの咆哮と同時に周囲の雑魚が一斉特攻」みたいな派手な演出も簡単に実装できます。
こうやって小さな責務のコンポーネントを積み重ねていくと、プロジェクト全体がかなりスッキリしていくので、ぜひ自分のゲーム用にアレンジしてみてください。
