Unityを触り始めると、ついプレイヤーや敵のAIロジックを全部ひとつの Update() に書いてしまいがちですよね。
「移動」「攻撃」「回復」「UI更新」などをひとまとめにした巨大スクリプトは、最初は動いても、あとから仕様変更が入った瞬間に地獄になります。

そこでこの記事では、「回復役のAI」だけをきれいに切り出したコンポーネント HealerAI を作ってみます。
「HPの減った味方を探して、回復魔法をかけに向かう」という役割だけに責任を絞った、小さなコンポーネントです。

【Unity】味方を自動でケアする回復サポーター!「HealerAI」コンポーネント

今回の HealerAI は、ざっくり言うとこんなことをします。

  • シーン内から「味方」だけを探す(タグやレイヤーでフィルタ)
  • その中から「HPが減っている味方」を見つける
  • 一番ヤバそう(HP割合が低い)な味方を優先してターゲットにする
  • ターゲットに近づき、一定距離まで来たら回復魔法を発動する
  • クールダウン時間を管理して、連射しすぎないようにする

移動や回復の処理も「コンポーネントとして分離しやすい」形で書いているので、他のAIにも流用しやすい構成になっています。


フルコード:HealerAI.cs


using UnityEngine;
using System.Collections.Generic;

/// <summary>
/**
 * HealerAI
 * - HPの減った味方を探し、近づいて回復するシンプルなAIコンポーネント
 * - 「回復役」という 1 つの責務に絞ったコンポーネントです
 */
/// </summary>
[RequireComponent(typeof(CharacterController))]
public class HealerAI : MonoBehaviour
{
    // -----------------------------
    // 検索系設定
    // -----------------------------

    [Header("味方探索設定")]
    [SerializeField] private string allyTag = "Ally"; 
    // 味方に付けるタグ名。Player / Ally など好きな名前に変更可

    [SerializeField] private float searchRadius = 15f;
    // この半径内の味方だけを回復対象として探す

    [SerializeField] private float searchInterval = 0.5f;
    // 何秒おきに味方を再探索するか(毎フレームやると重くなるので間引き)

    // -----------------------------
    // 回復系設定
    // -----------------------------

    [Header("回復設定")]
    [SerializeField] private float healAmount = 30f;
    // 1回の回復で回復するHP量

    [SerializeField] private float healRange = 2f;
    // この距離まで近づいたら回復魔法を発動

    [SerializeField] private float healCooldown = 3f;
    // 1回回復したあと、次に回復できるまでのクールダウン時間

    [SerializeField] private float minHealthRatioToHeal = 0.99f;
    // HPがこの割合未満の味方だけを回復対象にする(1.0 = 100%)

    // -----------------------------
    // 移動系設定
    // -----------------------------

    [Header("移動設定")]
    [SerializeField] private float moveSpeed = 3f;
    // ターゲットに向かう移動速度

    [SerializeField] private float stoppingDistance = 1f;
    // これ以上近づかない距離(回復レンジより少し短くしておくと自然)

    [SerializeField] private float gravity = -9.81f;
    // CharacterController 用の簡易重力

    // -----------------------------
    // 内部状態
    // -----------------------------

    private CharacterController controller;
    private Health targetHealth;      // 現在回復しようとしている味方の Health
    private float searchTimer = 0f;   // 味方探索用タイマー
    private float healCooldownTimer = 0f; // 回復クールダウン用タイマー
    private Vector3 velocity;         // 縦方向の速度(重力用)

    private void Awake()
    {
        controller = GetComponent<CharacterController>();
    }

    private void Update()
    {
        // 時間の更新
        searchTimer += Time.deltaTime;
        healCooldownTimer -= Time.deltaTime;

        // 定期的にターゲットを探し直す
        if (searchTimer >= searchInterval)
        {
            searchTimer = 0f;
            UpdateTarget();
        }

        // ターゲットがいなければ何もしない
        if (targetHealth == null)
        {
            ApplyGravityOnly();
            return;
        }

        // ターゲットに向かって移動
        MoveTowardsTarget();

        // 一定距離内なら回復を試みる
        TryHealTarget();
    }

    /// <summary>
    /// 回復対象となる味方を探し、もっともHP割合が低いものをターゲットにする
    /// </summary>
    private void UpdateTarget()
    {
        // シーン内の味方候補をすべて取得
        GameObject[] allies = GameObject.FindGameObjectsWithTag(allyTag);

        Health bestCandidate = null;
        float lowestHealthRatio = minHealthRatioToHeal; 
        // この値よりHP割合が低い味方だけを候補にする

        Vector3 myPos = transform.position;

        foreach (GameObject ally in allies)
        {
            // 自分自身は除外
            if (ally == this.gameObject)
                continue;

            // 距離チェック(遠すぎる味方はスキップ)
            float sqrDist = (ally.transform.position - myPos).sqrMagnitude;
            if (sqrDist > searchRadius * searchRadius)
                continue;

            // Health コンポーネントを持っていないオブジェクトはスキップ
            Health health = ally.GetComponent<Health>();
            if (health == null)
                continue;

            // すでに満タンに近い味方は回復不要
            float ratio = health.CurrentHealth / Mathf.Max(health.MaxHealth, 0.0001f);
            if (ratio >= minHealthRatioToHeal)
                continue;

            // 最もHP割合が低い味方を選ぶ
            if (ratio < lowestHealthRatio)
            {
                lowestHealthRatio = ratio;
                bestCandidate = health;
            }
        }

        targetHealth = bestCandidate;
    }

    /// <summary>
    /// ターゲットに向かって移動する
    /// </summary>
    private void MoveTowardsTarget()
    {
        if (targetHealth == null)
            return;

        Vector3 targetPos = targetHealth.transform.position;
        Vector3 myPos = transform.position;

        // 水平面(XZ平面)だけで距離を測る
        Vector3 toTarget = targetPos - myPos;
        toTarget.y = 0f;

        float distance = toTarget.magnitude;

        // 一定距離以内なら移動しない
        if (distance <= stoppingDistance)
        {
            ApplyGravityOnly();
            return;
        }

        // 方向を正規化して移動ベクトルを作る
        Vector3 direction = toTarget.normalized;
        Vector3 horizontalVelocity = direction * moveSpeed;

        // 重力を適用
        if (controller.isGrounded)
        {
            velocity.y = -1f; // 地面にくっつけておく程度の小さな値
        }
        else
        {
            velocity.y += gravity * Time.deltaTime;
        }

        Vector3 finalVelocity = horizontalVelocity + new Vector3(0f, velocity.y, 0f);

        controller.Move(finalVelocity * Time.deltaTime);

        // ターゲットの方向を向く(Y軸だけ回転)
        if (direction.sqrMagnitude > 0.001f)
        {
            Quaternion lookRot = Quaternion.LookRotation(direction, Vector3.up);
            transform.rotation = Quaternion.Slerp(transform.rotation, lookRot, 10f * Time.deltaTime);
        }
    }

    /// <summary>
    /// 重力だけを適用して落下させる
    /// </summary>
    private void ApplyGravityOnly()
    {
        if (controller.isGrounded)
        {
            velocity.y = -1f;
        }
        else
        {
            velocity.y += gravity * Time.deltaTime;
        }

        controller.Move(velocity * Time.deltaTime);
    }

    /// <summary>
    /// ターゲットが回復可能な距離にいれば回復する
    /// </summary>
    private void TryHealTarget()
    {
        if (targetHealth == null)
            return;

        // すでに死んでいる、またはオブジェクトが無効ならターゲット解除
        if (!targetHealth.gameObject.activeInHierarchy || targetHealth.IsDead)
        {
            targetHealth = null;
            return;
        }

        // まだクールダウン中の場合は何もしない
        if (healCooldownTimer > 0f)
            return;

        // HPが十分にあるならターゲット解除
        float ratio = targetHealth.CurrentHealth / Mathf.Max(targetHealth.MaxHealth, 0.0001f);
        if (ratio >= minHealthRatioToHeal)
        {
            targetHealth = null;
            return;
        }

        // 距離チェック
        float distance = Vector3.Distance(transform.position, targetHealth.transform.position);
        if (distance > healRange)
            return;

        // 実際の回復処理
        PerformHeal(targetHealth);

        // クールダウン開始
        healCooldownTimer = healCooldown;
    }

    /// <summary>
    /// 回復処理そのものを行う(エフェクトやSEを追加するならここ)
    /// </summary>
    private void PerformHeal(Health target)
    {
        if (target == null)
            return;

        target.Heal(healAmount);

        // ここにエフェクトやSEを追加すると見た目がリッチになります
        // 例:
        // Instantiate(healEffectPrefab, target.transform.position, Quaternion.identity);
        // audioSource.PlayOneShot(healClip);
    }
}

/// <summary>
/**
 * シンプルな Health コンポーネント
 * - HPの管理だけに責務を絞ったコンポーネントです
 * - ダメージや回復はこのクラスを通して行います
 */
/// </summary>
public class Health : MonoBehaviour
{
    [Header("HP設定")]
    [SerializeField] private float maxHealth = 100f;

    [SerializeField] private bool destroyOnDeath = false;
    // true の場合、HPが0になったら GameObject を破棄する

    public float MaxHealth => maxHealth;
    public float CurrentHealth { get; private set; }
    public bool IsDead => CurrentHealth <= 0f;

    private void Awake()
    {
        CurrentHealth = maxHealth;
    }

    /// <summary>
    /// ダメージを受ける
    /// </summary>
    public void TakeDamage(float amount)
    {
        if (IsDead)
            return;

        CurrentHealth -= amount;
        if (CurrentHealth <= 0f)
        {
            CurrentHealth = 0f;
            OnDeath();
        }
    }

    /// <summary>
    /// 回復する
    /// </summary>
    public void Heal(float amount)
    {
        if (IsDead)
            return;

        CurrentHealth += amount;
        if (CurrentHealth > maxHealth)
        {
            CurrentHealth = maxHealth;
        }
    }

    /// <summary>
    /// 死亡時の処理
    /// </summary>
    private void OnDeath()
    {
        // ここに死亡アニメーションやエフェクトを入れると良いです
        if (destroyOnDeath)
        {
            Destroy(gameObject);
        }
        else
        {
            // とりあえず非アクティブにする例
            gameObject.SetActive(false);
        }
    }
}

使い方の手順

ここでは、RPG風のパーティを想定した具体例で説明します。
「プレイヤー(前衛)」「味方戦士」「回復役(HealerAI)」の3人パーティを作るイメージです。

  1. ① Health コンポーネントを味方キャラに付ける
    • プレイヤー用のプレハブ(例: Player)を選択
    • Add Component > Health を追加
    • Max Health を 100 など好きな値に設定
    • 同様に、味方戦士や回復役にも Health をアタッチ
  2. ② 味方キャラに共通のタグを付ける
    • Unityメニューから Tags & Layers を開き、Ally というタグを作成
    • プレイヤー、味方戦士、回復役のすべての GameObject に Ally タグを設定
    • HealerAI の allyTagAlly にしておくと、このタグを持つオブジェクトを味方として扱います
  3. ③ 回復役キャラに HealerAI をアタッチする
    • 回復役用のプレハブ(例: PriestHealer)を選択
    • Add Component > Character Controller を追加(自動で RequireComponent されますが、先に付けておくと楽です)
    • Add Component > HealerAI を追加
    • インスペクターで以下の値を調整
      • Search Radius: 15 〜 20 くらい(回復役が味方を認識できる範囲)
      • Heal Amount: 20 〜 40(1回の回復量)
      • Heal Range: 2(これくらい近づいたら回復)
      • Heal Cooldown: 3(3秒に1回くらいの頻度で回復)
      • Move Speed: 3(回復役らしく、前衛より少し遅めに)
  4. ④ ダメージを与える仕組みを用意して動作確認
    例として、敵の攻撃AIやトラップが Health.TakeDamage() を呼ぶようにしておきます。
    • 敵の攻撃スクリプトから:
      
      private void OnTriggerEnter(Collider other)
      {
          // 味方タグに当たったらダメージ
          if (other.CompareTag("Ally"))
          {
              Health health = other.GetComponent<Health>();
              if (health != null)
              {
                  health.TakeDamage(20f);
              }
          }
      }
      
    • ゲームを再生し、プレイヤーや味方戦士がダメージを受けると、
      • HealerAI が HPの減った味方を自動で探す
      • 一番危険な味方に向かって走っていく
      • 一定距離まで近づくと回復してくれる

この構成なら、「回復役の思考」は HealerAI に、「HP管理」は Health に完全に分離されています。
プレイヤー操作や敵AIは、また別のコンポーネントとして実装すればOKです。


メリットと応用

HealerAI をコンポーネントとして切り出しておくことで、プレハブ管理やレベルデザインがかなり楽になります。

  • プレハブの再利用性が高い
    「この敵は攻撃だけ」「このNPCは回復だけ」「このボスは攻撃+自己回復」など、
    HealthHealerAI を組み合わせるだけで、多様なキャラを作れます。
  • レベルデザイン時にパラメータ調整がしやすい
    Search RadiusHeal Cooldown をいじるだけで、
    「やたら優秀な回復役」「のんびりした回復役」など、ゲームバランスを簡単に調整できます。
  • 責務が明確なのでデバッグしやすい
    「回復しないバグ」が出たとき、見るべきスクリプトは HealerAIHealth だけです。
    巨大な God クラスを必死にスクロールする必要がありません。

応用例としては、以下のような改造が簡単にできます。

  • パーティ全体のHPを見て、「全体回復魔法」を使うようにする
  • MP(マナ)やアイテム残数を見て、回復するかどうか判断させる
  • プレイヤーがピンチのときだけ、最優先でプレイヤーを回復する

例えば、「プレイヤーだけは優先的に回復したい」という場合、以下のようなメソッドを追加して、
UpdateTarget() の中で使うと、ターゲット選択ロジックを簡単に差し替えできます。


/// <summary>
/// プレイヤーを最優先しつつ、HP割合が低い順に味方を選ぶ例
/// (playerTag のオブジェクトがいれば、それを最優先でターゲットにする)
/// </summary>
private Health FindBestTargetWithPlayerPriority(string playerTag)
{
    Health best = null;
    float lowestRatio = minHealthRatioToHeal;
    Vector3 myPos = transform.position;

    // まずプレイヤーを探す
    GameObject player = GameObject.FindGameObjectWithTag(playerTag);
    if (player != null && player.CompareTag(allyTag))
    {
        Health playerHealth = player.GetComponent<Health>();
        if (playerHealth != null)
        {
            float dist = Vector3.Distance(myPos, player.transform.position);
            float ratio = playerHealth.CurrentHealth / Mathf.Max(playerHealth.MaxHealth, 0.0001f);

            // プレイヤーが回復対象条件を満たしていれば即ターゲット
            if (dist <= searchRadius && ratio < minHealthRatioToHeal)
            {
                return playerHealth;
            }
        }
    }

    // プレイヤーが回復不要なら、通常どおりHP割合が低い味方を探す
    GameObject[] allies = GameObject.FindGameObjectsWithTag(allyTag);
    foreach (GameObject ally in allies)
    {
        if (ally == this.gameObject)
            continue;

        float sqrDist = (ally.transform.position - myPos).sqrMagnitude;
        if (sqrDist > searchRadius * searchRadius)
            continue;

        Health health = ally.GetComponent<Health>();
        if (health == null)
            continue;

        float ratio = health.CurrentHealth / Mathf.Max(health.MaxHealth, 0.0001f);
        if (ratio >= minHealthRatioToHeal)
            continue;

        if (ratio < lowestRatio)
        {
            lowestRatio = ratio;
            best = health;
        }
    }

    return best;
}

このように、「ターゲット選択」「移動」「回復処理」をそれぞれ小さなメソッドに分けておくと、
仕様変更が入っても一部だけを差し替えやすくなります。
Godクラス化を避けて、コンポーネント指向でAIを組み立てていきましょう。