Unityを触り始めた頃は、つい「とりあえず全部Updateに書けば動くし楽!」となりがちですよね。
プレイヤーの移動、敵AI、UI更新、当たり判定の処理などを1つの巨大スクリプトに押し込んでしまうと、次のような問題が出てきます。
- ちょっと仕様を変えたいだけなのに、どこを触ればいいか分からない
- 敵の挙動を増やすたびに、既存コードを壊してしまう
- Prefab単位での再利用が難しく、コピペ地獄になる
敵AI周りで特にありがちなのが、「敵がダメージを受けたら仲間を呼ぶ」「一定距離にプレイヤーが入ったらリンクして一斉に襲ってくる」といった「アグロ(敵対)共有」の処理を、1つの巨大なEnemyControllerにベタ書きしてしまうパターンです。
この記事では、その「仲間呼び」ロジックを AggroLink という専用コンポーネントに切り出して、
「ダメージを受けたら、周囲の同じグループの敵を一斉にアクティブ化する」
という処理を、シンプルかつ再利用しやすい形で実装していきます。
【Unity】敵同士で一斉リンク!「AggroLink」コンポーネント
ここでは次のような前提の「小さな責務」に分けた構成をとります。
- AggroLink:
ダメージを受けたときに、周囲の仲間を「アクティブ状態」にするコンポーネント - EnemyAggroState:
敵が「待機中か、プレイヤー追跡中か」の状態を持つ小さなコンポーネント - EnemyHealth:
敵のHP管理とダメージ受付を行い、「ダメージを受けた」イベントを発火するコンポーネント
すべてこの記事内で完結しているので、コピペしてそのまま動かせます。
フルコード:AggroLink + 補助コンポーネント
using System;
using UnityEngine;
namespace Sample.Enemy
{
/// <summary>
/// 敵のアグロ(プレイヤー追跡状態)を管理するシンプルなコンポーネント。
/// ここでは「アクティブかどうか」だけを持たせています。
/// 実際の移動AIは別コンポーネントに分けることを想定しています。
/// </summary>
public class EnemyAggroState : MonoBehaviour
{
[SerializeField]
private bool isAggro; // 現在アグロ状態かどうか
/// <summary>
/// 外部からアグロ状態を読み取るためのプロパティ
/// </summary>
public bool IsAggro => isAggro;
/// <summary>
/// アグロ状態を設定する。
/// ここでアグロ開始時の演出などを追加してもよいです。
/// </summary>
public void SetAggro(bool value)
{
if (isAggro == value) return;
isAggro = value;
// デバッグ用ログ(必要なければ削除してOK)
if (isAggro)
{
Debug.Log($"[{name}] Aggro ON", this);
}
else
{
Debug.Log($"[{name}] Aggro OFF", this);
}
}
/// <summary>
/// 外部から「アグロ開始」を明示的に呼び出したいとき用のショートカット。
/// </summary>
public void ActivateAggro()
{
SetAggro(true);
}
}
/// <summary>
/// 敵のHPとダメージ受付を管理するコンポーネント。
/// ダメージを受けたタイミングをイベントで通知します。
/// AggroLink はこのイベントをフックして仲間呼びを行います。
/// </summary>
[RequireComponent(typeof(Collider))]
public class EnemyHealth : MonoBehaviour
{
[SerializeField]
private int maxHealth = 100;
[SerializeField]
private int currentHealth;
/// <summary>
/// ダメージを受けたときに発火するイベント。
/// (被ダメージ量, ダメージの発生源) を通知します。
/// </summary>
public event Action<int, GameObject> OnDamaged;
/// <summary>
/// HPが0以下になったときに発火するイベント。
/// </summary>
public event Action OnDied;
private void Awake()
{
// ゲーム開始時に現在HPを最大値で初期化
currentHealth = maxHealth;
}
/// <summary>
/// 外部からダメージを与えるときに呼ぶ関数。
/// 弾やプレイヤー攻撃スクリプトから呼び出す想定です。
/// </summary>
/// <param name="damage">与えるダメージ量</param>
/// <param name="attacker">攻撃者(プレイヤーなど)のGameObject</param>
public void ApplyDamage(int damage, GameObject attacker = null)
{
if (currentHealth <= 0) return;
currentHealth -= damage;
if (currentHealth < 0)
{
currentHealth = 0;
}
// ダメージイベントを発火
OnDamaged?.Invoke(damage, attacker);
// 死亡判定
if (currentHealth == 0)
{
OnDied?.Invoke();
HandleDeath();
}
}
/// <summary>
/// デモ用の簡易死亡処理。
/// 実際のゲームではアニメーション再生やドロップ処理などに差し替えてください。
/// </summary>
private void HandleDeath()
{
Debug.Log($"[{name}] Died", this);
// とりあえず消すだけ
Destroy(gameObject);
}
}
/// <summary>
/// ダメージを受けたときに、周囲の同じグループの敵をアクティブ(アグロ状態)にするコンポーネント。
///
/// - EnemyHealth の OnDamaged イベントを購読して発火トリガーとします。
/// - 同じ groupId を持つ EnemyAggroState を、一定半径内から探して一斉にアクティブ化します。
///
/// 「仲間呼び」ロジックだけをこのクラスに閉じ込めることで、
/// 他のAIや移動処理と疎結合に保てます。
/// </summary>
[RequireComponent(typeof(EnemyHealth))]
[DisallowMultipleComponent]
public class AggroLink : MonoBehaviour
{
[Header("グループ設定")]
[SerializeField]
[Tooltip("同じグループIDを持つ敵同士が仲間呼びの対象になります。")]
private string groupId = "DefaultGroup";
[Header("仲間呼びの範囲")]
[SerializeField]
[Tooltip("仲間呼びを行う半径(ワールド座標)。")]
private float linkRadius = 10f;
[SerializeField]
[Tooltip("仲間呼びの中心を、敵の現在位置ではなく任意のTransformにしたい場合に指定します。")]
private Transform customCenter;
[Header("自分もアグロ化するか")]
[SerializeField]
[Tooltip("ダメージを受けた本人もアグロ状態にするかどうか。")]
private bool includeSelf = true;
[Header("一度だけ仲間呼びするか")]
[SerializeField]
[Tooltip("true の場合、一度仲間呼びしたらこのコンポーネントは無効化されます。")]
private bool triggerOnlyOnce = true;
[Header("デバッグ表示")]
[SerializeField]
[Tooltip("Sceneビューに仲間呼びの範囲をワイヤーフレームで表示します。")]
private bool drawGizmos = true;
[SerializeField]
[Tooltip("Gizmosで描画する色。")]
private Color gizmoColor = new Color(1f, 0.5f, 0f, 0.3f);
// キャッシュ
private EnemyHealth _health;
private EnemyAggroState _myAggroState;
private bool _hasTriggered; // すでに仲間呼びを発動したかどうか
private void Awake()
{
_health = GetComponent<EnemyHealth>();
// 自分自身もアグロ対象にする場合に限り、AggroState を探しておく
if (includeSelf)
{
_myAggroState = GetComponent<EnemyAggroState>();
}
}
private void OnEnable()
{
// ダメージイベントを購読
if (_health != null)
{
_health.OnDamaged += HandleDamaged;
}
}
private void OnDisable()
{
// イベント購読解除(メモリリークや多重登録を防ぐ)
if (_health != null)
{
_health.OnDamaged -= HandleDamaged;
}
}
/// <summary>
/// ダメージを受けたときに呼ばれるコールバック。
/// ここから仲間呼びを行います。
/// </summary>
private void HandleDamaged(int damage, GameObject attacker)
{
// すでに一度トリガー済みで、triggerOnlyOnce が有効なら何もしない
if (triggerOnlyOnce && _hasTriggered)
{
return;
}
_hasTriggered = true;
// 自分もアグロ状態にする設定なら、ここでONにする
if (includeSelf && _myAggroState != null)
{
_myAggroState.ActivateAggro();
}
// 実際の仲間呼び処理
ActivateNearbyAllies();
// 一度だけトリガーする設定なら、自身を無効化して二度と発動しないようにする
if (triggerOnlyOnce)
{
enabled = false;
}
}
/// <summary>
/// 周囲の同じグループの敵を検索し、アグロ状態に切り替える。
/// </summary>
private void ActivateNearbyAllies()
{
// 仲間呼びの中心位置を決定(customCenter が指定されていればそちらを優先)
Vector3 center = customCenter != null ? customCenter.position : transform.position;
// Physics.OverlapSphere で一定半径内のコライダーを取得
Collider[] hits = Physics.OverlapSphere(center, linkRadius);
int activatedCount = 0;
foreach (var hit in hits)
{
// 自分自身のコライダーを拾うこともあるので、GameObjectレベルで比較して除外
if (hit.attachedRigidbody != null && hit.attachedRigidbody.gameObject == gameObject)
{
continue;
}
if (hit.gameObject == gameObject) continue;
// 同じグループIDを持つ AggroLink を探す
AggroLink otherLink = hit.GetComponentInParent<AggroLink>();
if (otherLink == null) continue;
// グループIDが一致しないならスキップ
if (!string.Equals(otherLink.groupId, groupId, StringComparison.Ordinal))
{
continue;
}
// アグロ状態コンポーネントを探す
EnemyAggroState aggroState = hit.GetComponentInParent<EnemyAggroState>();
if (aggroState == null) continue;
// すでにアグロ状態ならスキップ(無駄な再設定を避ける)
if (aggroState.IsAggro) continue;
// アグロON!
aggroState.ActivateAggro();
activatedCount++;
}
Debug.Log($"[{name}] AggroLink activated {activatedCount} allies in group '{groupId}'", this);
}
/// <summary>
/// Sceneビューに仲間呼びの範囲を表示するためのGizmos描画。
/// デバッグ時に範囲を調整しやすくなります。
/// </summary>
private void OnDrawGizmosSelected()
{
if (!drawGizmos) return;
Gizmos.color = gizmoColor;
Vector3 center = customCenter != null ? customCenter.position : transform.position;
Gizmos.DrawWireSphere(center, linkRadius);
}
}
}
使い方の手順
ここでは「プレイヤーに殴られた敵が、周囲の敵を巻き込んで一斉に追跡モードになる」ケースを例にします。
-
敵Prefabに必要なコンポーネントを付ける
1つの敵Prefabに対して、少なくとも次の3つをアタッチします。Collider(BoxCollider / CapsuleCollider など、Is Trigger でも可)EnemyHealth(HP管理)EnemyAggroState(アグロ状態管理)AggroLink(仲間呼び)
敵の見た目用に
MeshRendererやAnimatorを付けていても問題ありません。
プレイヤーの攻撃スクリプトからはEnemyHealth.ApplyDamage()を呼ぶだけでOKです。 -
AggroLink のパラメータを設定する
敵Prefabを選択し、Inspector でAggroLinkの項目を調整します。- Group Id(groupId)
例:RoomA,GoblinCampなど。
同じIDを持つ敵同士が仲間呼びの対象になります。
部屋ごとにグループを分ければ、「この部屋の敵だけ連動して襲ってくる」といった調整ができます。 - Link Radius(linkRadius)
仲間呼びの半径。例えば8 ~ 15くらいにしておくと、
1体を殴ると近くの敵だけ反応する、という自然な挙動になります。 - Custom Center(customCenter)
通常は空欄でOKです(敵の位置が中心になります)。
「部屋の中央から半径を測りたい」など特別なケースで、Emptyオブジェクトを指定します。 - Include Self(includeSelf)
ONにすると、ダメージを受けた本人もアグロ状態になります(普通はON推奨)。
「本人は隠れていて、仲間だけ襲ってくる」など変わったギミックを作りたい場合はOFFにしてもOKです。 - Trigger Only Once(triggerOnlyOnce)
ONにすると、1回仲間呼びしたらそれ以降は発動しません。
「1回バレたら全員でずっと追いかけてくる」タイプの敵に向いています。
OFFにすると、殴られるたびに周囲の敵を再チェックします。
- Group Id(groupId)
-
プレイヤーの攻撃からダメージを与える
例えば近接攻撃の当たり判定で、次のようなスクリプトをプレイヤー側に付けておきます。using UnityEngine; using Sample.Enemy; // EnemyHealth / AggroLink の名前空間 public class SimpleMeleeAttack : MonoBehaviour { [SerializeField] private int damage = 10; [SerializeField] private float attackRange = 2f; [SerializeField] private LayerMask enemyLayer; public void PerformAttack() { // プレイヤーの前方にRayを飛ばして、敵に当たったらダメージを与える簡易攻撃例 Vector3 origin = transform.position + Vector3.up * 1f; Vector3 direction = transform.forward; if (Physics.Raycast(origin, direction, out RaycastHit hit, attackRange, enemyLayer)) { var health = hit.collider.GetComponentInParent<EnemyHealth>(); if (health != null) { health.ApplyDamage(damage, gameObject); } } } }この
ApplyDamageが呼ばれたタイミングでEnemyHealth.OnDamagedが発火し、
それをAggroLinkが受け取って仲間呼びを行います。 -
敵のアグロ状態をAIや移動に反映する
EnemyAggroStateのIsAggroを、移動AIコンポーネントから参照します。
例えば「プレイヤーを追いかけるだけの超簡易AI」はこんな感じです。using UnityEngine; using Sample.Enemy; [RequireComponent(typeof(EnemyAggroState))] public class SimpleChaseAI : MonoBehaviour { [SerializeField] private Transform target; // 追いかける対象(プレイヤー) [SerializeField] private float moveSpeed = 3f; private EnemyAggroState _aggroState; private void Awake() { _aggroState = GetComponent<EnemyAggroState>(); } private void Update() { if (!_aggroState.IsAggro) return; if (target == null) return; Vector3 direction = (target.position - transform.position); direction.y = 0f; // 水平移動に限定 if (direction.sqrMagnitude < 0.01f) return; direction.Normalize(); transform.position += direction * (moveSpeed * Time.deltaTime); transform.rotation = Quaternion.LookRotation(direction); } }これで、1体が殴られる → AggroLink が発動 → 周囲の敵の
IsAggroが true になる → 全員が追跡開始 という流れが実現できます。
メリットと応用
AggroLink コンポーネントを導入することで、次のようなメリットがあります。
- 敵AIスクリプトの責務を分離できる
ダメージ処理(EnemyHealth)、アグロ状態(EnemyAggroState)、仲間呼び(AggroLink)、移動AI(SimpleChaseAI)を分けておくことで、
「仲間呼びの仕様だけを変えたい」「アグロ状態のON/OFFだけを別のイベントに繋ぎたい」といった変更がやりやすくなります。 - Prefab単位での再利用がしやすい
同じ敵Prefabを別のステージに配置するときでも、
groupIdとlinkRadiusを変えるだけで、
「この部屋では4体同時に襲ってくる」「この通路では1体ずつしか反応しない」など、レベルデザインに応じた調整が簡単です。 - レベルデザインの試行錯誤が早くなる
Sceneビューで Gizmos によって仲間呼び範囲が可視化されるため、
「プレイヤーがここを通ると、この敵とこの敵がリンクしてくるな」というのが一目で分かります。
デザイナーやレベル設計担当が、コードを触らずにパラメータだけで調整できるのも大きな利点ですね。
改造案:視線が通っている仲間だけを呼ぶ
もう一歩踏み込んで、「壁の向こうにいる敵はリンクしない」ようにしたい場合は、
ActivateNearbyAllies() の中で Raycast を使って「視線が遮られていないか」を判定するとよいです。
例えば次のようなヘルパー関数を AggroLink に追加して、
仲間呼びの前にチェックを挟む形にできます。
/// <summary>
/// center から target への視線が壁などで遮られていないかを判定する。
/// 壁用の LayerMask を用意しておくと、より正確に判定できます。
/// </summary>
private bool HasLineOfSight(Vector3 center, Transform target, LayerMask obstacleMask)
{
Vector3 origin = center + Vector3.up * 1f; // 少し持ち上げて地面との干渉を避ける
Vector3 dest = target.position + Vector3.up * 1f;
Vector3 dir = dest - origin;
float distance = dir.magnitude;
if (distance <= 0.01f) return true;
dir /= distance;
// 障害物に当たったら視線が通っていないとみなす
if (Physics.Raycast(origin, dir, distance, obstacleMask))
{
return false;
}
return true;
}
この関数を使って、「視線が通っている仲間だけ ActivateAggro() を呼ぶ」といったロジックにすれば、
よりステルス寄りのゲームデザインにも対応できます。
このように、仲間呼びのロジックを AggroLink という1つの責務に閉じ込めておくことで、
ゲームの仕様に合わせた拡張・改造がとてもやりやすくなります。
巨大なEnemyクラスに全部詰め込むのではなく、小さなコンポーネントに分割していく方針で設計していきましょう。
