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人パーティを作るイメージです。
-
① Health コンポーネントを味方キャラに付ける
- プレイヤー用のプレハブ(例:
Player)を選択 Add Component > Healthを追加Max Healthを 100 など好きな値に設定- 同様に、味方戦士や回復役にも
Healthをアタッチ
- プレイヤー用のプレハブ(例:
-
② 味方キャラに共通のタグを付ける
- Unityメニューから
Tags & Layersを開き、Allyというタグを作成 - プレイヤー、味方戦士、回復役のすべての GameObject に
Allyタグを設定 - HealerAI の
allyTagをAllyにしておくと、このタグを持つオブジェクトを味方として扱います
- Unityメニューから
-
③ 回復役キャラに HealerAI をアタッチする
- 回復役用のプレハブ(例:
PriestやHealer)を選択 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(回復役らしく、前衛より少し遅めに)
- 回復役用のプレハブ(例:
-
④ ダメージを与える仕組みを用意して動作確認
例として、敵の攻撃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は回復だけ」「このボスは攻撃+自己回復」など、
HealthとHealerAIを組み合わせるだけで、多様なキャラを作れます。 - レベルデザイン時にパラメータ調整がしやすい
Search RadiusやHeal Cooldownをいじるだけで、
「やたら優秀な回復役」「のんびりした回復役」など、ゲームバランスを簡単に調整できます。 - 責務が明確なのでデバッグしやすい
「回復しないバグ」が出たとき、見るべきスクリプトはHealerAIとHealthだけです。
巨大な 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を組み立てていきましょう。
