Unityを触り始めたころは、つい「とりあえず全部Updateに書く」という実装をしがちですよね。プレイヤーの入力処理、カメラ追従、敵AI、エフェクト制御…すべて1つの巨大スクリプトに詰め込んでしまうと、次のような問題が出てきます。

  • どの処理がどのオブジェクトの責務なのか分かりづらい
  • 少し仕様変更するだけで、あちこちのコードを直す必要が出てくる
  • プレハブ単位で機能を差し替え・再利用しにくい

敵AIも同じで、「プレイヤーを追いかける」「攻撃する」「パトロールする」などを1つのクラスに書き始めると、あっという間にGodクラス化してしまいます。

そこでこの記事では、「ターゲットへ向かって移動する」だけに責務を絞ったコンポーネント、「TargetFollower」を作っていきます。敵AI用の基本パーツとして、プレイヤーや特定のノードを追尾させる動きだけを切り出しておくことで、シンプルで再利用しやすい構成にしていきましょう。

【Unity】シンプル追尾でAIを分割!「TargetFollower」コンポーネント

コンポーネントの概要

TargetFollower は、指定した Transform(プレイヤーなど)に向かって、このコンポーネントがアタッチされたオブジェクト(親)を移動させるためのコンポーネントです。

  • ターゲットの方向へ向きを変える(任意でオン/オフ)
  • ターゲットとの距離が一定以下になったら停止する
  • 追尾のオン/オフをスクリプトやインスペクタから切り替え可能
  • 物理挙動(Rigidbody)にも対応できるよう、オプションで移動方法を切り替え

「追う」という行動だけを1コンポーネントに閉じ込めておくことで、敵AIの状態管理や攻撃ロジックは別コンポーネントに分離しやすくなります。


フルコード:TargetFollower.cs


using UnityEngine;

/// <summary>
/// 指定したターゲットに向かって、このオブジェクトを移動させるコンポーネント。
/// 敵AIの「追尾」部分だけを担当させることを想定。
/// </summary>
[DisallowMultipleComponent]
[RequireComponent(typeof(Transform))]
public class TargetFollower : MonoBehaviour
{
    // ===== 設定項目(インスペクタで調整) =====

    [Header("ターゲット設定")]
    [Tooltip("追尾対象となるTransform(プレイヤーなど)")]
    [SerializeField] private Transform target;

    [Header("移動設定")]
    [Tooltip("1秒あたりの移動速度")]
    [SerializeField] private float moveSpeed = 3.0f;

    [Tooltip("この距離以下まで近づいたら移動を止める")]
    [SerializeField] private float stopDistance = 1.0f;

    [Tooltip("ターゲットがこの距離より遠い場合のみ追尾する(0以下で常に追尾)")]
    [SerializeField] private float startChaseDistance = 0.0f;

    [Header("回転設定")]
    [Tooltip("ターゲットの方向へ回転させるかどうか")]
    [SerializeField] private bool rotateTowardsTarget = true;

    [Tooltip("回転速度(度/秒)。0以下で即座に向きを合わせる")]
    [SerializeField] private float rotationSpeed = 360f;

    [Header("移動モード")]
    [Tooltip("Transform.Translate/MoveTowards で動かすか、Rigidbody で動かすか")]
    [SerializeField] private MovementMode movementMode = MovementMode.Transform;

    [Tooltip("Rigidbody を使う場合はここに参照を入れる(未設定なら自動取得を試みる)")]
    [SerializeField] private Rigidbody rb;

    [Header("動作フラグ")]
    [Tooltip("追尾を有効にするかどうか(スクリプトからも切り替え可能)")]
    [SerializeField] private bool followEnabled = true;

    /// <summary>
    /// どの方法で移動するか
    /// </summary>
    private enum MovementMode
    {
        Transform,
        Rigidbody
    }

    // ===== プロパティ(外部から制御しやすくする) =====

    /// <summary>
    /// 現在のターゲット
    /// </summary>
    public Transform Target
    {
        get => target;
        set => target = value;
    }

    /// <summary>
    /// 追尾の有効/無効
    /// </summary>
    public bool FollowEnabled
    {
        get => followEnabled;
        set => followEnabled = value;
    }

    // ===== Unity イベント =====

    private void Reset()
    {
        // コンポーネント追加時に Rigidbody があれば拾っておく
        if (rb == null)
        {
            rb = GetComponent();
        }
    }

    private void Awake()
    {
        // Rigidbody モードなのに Rigidbody が設定されていない場合、自動取得を試みる
        if (movementMode == MovementMode.Rigidbody && rb == null)
        {
            rb = GetComponent();
            if (rb == null)
            {
                Debug.LogWarning(
                    $"[TargetFollower] Rigidbodyモードですが、Rigidbodyが見つかりません。" +
                    $" ゲームオブジェクト: {gameObject.name}",
                    this
                );
            }
        }
    }

    private void Update()
    {
        if (!followEnabled) return;     // 無効なら何もしない
        if (target == null) return;     // ターゲットがいなければ何もしない

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

        // 追尾開始距離が設定されている場合、その距離より遠いときだけ追尾
        if (startChaseDistance > 0f && distance <= startChaseDistance)
        {
            return;
        }

        // 停止距離以内なら移動しない
        if (distance <= stopDistance)
        {
            // 近づきすぎ防止のため、速度をゼロにしておく(Rigidbodyモードのみ)
            if (movementMode == MovementMode.Rigidbody && rb != null)
            {
                rb.velocity = Vector3.zero;
            }
            // 回転だけは行うかどうかは好みだが、ここでは行わないことにする
            return;
        }

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

        // 回転処理
        if (rotateTowardsTarget)
        {
            RotateTowards(direction);
        }

        // 移動処理
        MoveTowards(direction);
    }

    // ===== 内部処理 =====

    /// <summary>
    /// 指定された方向へオブジェクトを移動させる
    /// </summary>
    private void MoveTowards(Vector3 direction)
    {
        float delta = moveSpeed * Time.deltaTime;

        switch (movementMode)
        {
            case MovementMode.Transform:
                // Transform を直接動かすシンプルな方法
                transform.position += direction * delta;
                break;

            case MovementMode.Rigidbody:
                if (rb == null)
                {
                    // Rigidbody がない場合は Transform で代用
                    transform.position += direction * delta;
                    return;
                }

                // Rigidbody の velocity を使って移動させる
                // (地面判定などをする場合はここを拡張するとよい)
                Vector3 newVelocity = direction * moveSpeed;
                newVelocity.y = rb.velocity.y; // Y方向の速度は維持(重力など)
                rb.velocity = newVelocity;
                break;
        }
    }

    /// <summary>
    /// 指定された方向へ向くように回転させる
    /// </summary>
    private void RotateTowards(Vector3 direction)
    {
        if (direction.sqrMagnitude <= 0.0001f) return;

        // Y軸だけを回転させたい場合(2D横スクロールや3Dキャラ用)
        Vector3 lookDirection = direction;
        lookDirection.y = 0f; // 水平面上での回転に限定

        if (lookDirection.sqrMagnitude <= 0.0001f) return;

        Quaternion targetRotation = Quaternion.LookRotation(lookDirection, Vector3.up);

        if (rotationSpeed <= 0f)
        {
            // 即座に向きを合わせる
            transform.rotation = targetRotation;
        }
        else
        {
            // なめらかに回転させる
            transform.rotation = Quaternion.RotateTowards(
                transform.rotation,
                targetRotation,
                rotationSpeed * Time.deltaTime
            );
        }
    }

    // ===== デバッグ用の可視化 =====

    private void OnDrawGizmosSelected()
    {
        // 停止距離と追尾開始距離をシーンビューに可視化する
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(transform.position, stopDistance);

        if (startChaseDistance > 0f)
        {
            Gizmos.color = Color.cyan;
            Gizmos.DrawWireSphere(transform.position, startChaseDistance);
        }

        // ターゲットへのライン
        if (target != null)
        {
            Gizmos.color = Color.red;
            Gizmos.DrawLine(transform.position, target.position);
        }
    }
}

使い方の手順

ここでは、敵キャラクターがプレイヤーを追いかけるケースを例に、基本的な使い方を説明します。

手順①:スクリプトをプロジェクトに追加

  1. TargetFollower.cs という名前で、上記のコードをそのまま保存します。
  2. Unityエディタに戻り、コンパイルが通るのを待ちます。

手順②:敵キャラクターにコンポーネントをアタッチ

  1. ヒエラルキーで「Enemy」などの敵キャラクターの GameObject を選択します。
  2. Add Component ボタンから TargetFollower を追加します。
  3. もし敵が物理挙動で動くタイプなら、同じオブジェクトに Rigidbody を追加し、Movement ModeRigidbody に変更します。

手順③:ターゲット(プレイヤー)を指定

  1. プレイヤーの GameObject(例:Player)をヒエラルキーからドラッグし、TargetFollowerTarget スロットにドロップします。
  2. Move Speed(移動速度)、Stop Distance(どこまで近づくか)、Start Chase Distance(どれだけ離れたら追いかけ始めるか)を好みの値に調整します。
  3. 敵の向きをプレイヤーに向けたい場合は、Rotate Towards Target にチェックを入れ、Rotation Speed を設定します。

手順④:動作確認と具体的な使用例

  • プレイヤーを追いかける敵
    プレイヤーが近づくと、Start Chase Distance を超えたタイミングで敵が追いかけ始め、Stop Distance まで近づくと止まる挙動になります。
    攻撃処理は別コンポーネント(例:EnemyAttacker)に分離しておき、Stop Distance 付近で攻撃アニメーションを再生すると、きれいに責務を分けられます。
  • 巡回ポイントを追尾する敵
    ターゲットをプレイヤーではなく、「巡回ポイント用のEmptyオブジェクト」に設定すると、「今向かうべきポイント」を追尾するコンポーネントとして使えます。
    別コンポーネントで「次の巡回ポイントを TargetFollower.Target にセットする」だけで、移動ロジックはそのまま使い回せます。
  • 動く床がプレイヤーを追いかけるギミック
    動く床の GameObject に TargetFollower を付けて、プレイヤーをターゲットにするだけで、プレイヤーを追いかける床ギミックが作れます。
    Rotate Towards Target をオフにしておけば、床は回転せずに水平移動だけ行います。

メリットと応用

TargetFollower を用意しておくと、敵AIやギミックの実装がかなり楽になります。

  • プレハブ管理がシンプルになる
    「追尾する敵」「巡回する敵」「待ち伏せする敵」などを作るときも、TargetFollower を共通パーツとしてプレハブに含めておき、ターゲットやパラメータだけを変える形にできます。
    行動パターンの違いは別コンポーネントで表現できるため、プレハブのバリエーションが増えても、コードの重複は最小限で済みます。
  • レベルデザインの自由度が上がる
    レベルデザイナーは「この敵はプレイヤーを追いかける」「この敵は特定のポイントへ移動する」などを、インスペクタ上でターゲットを差し替えるだけで実現できます。
    スクリプトを書き換えずに、シーンごとに挙動を細かく調整できるのが大きなメリットです。
  • 責務が明確でテストしやすい
    「ターゲットを追尾する」という単一の責務に絞っているため、バグの切り分けがしやすく、
    「追尾が変」「攻撃タイミングがおかしい」などの問題を、それぞれ専用コンポーネント側で確認できます。

改造案:一時的に追尾を停止するメソッド

例えば、「ダメージを受けたときに少しの間だけ追尾を止める」などの演出を入れたい場合は、TargetFollower に次のようなメソッドを追加すると便利です。


/// <summary>
/// 指定した秒数だけ追尾を一時停止するコルーチン
/// (呼び出し側から StartCoroutine(TemporaryStop(1.0f)); のように使用)
/// </summary>
public System.Collections.IEnumerator TemporaryStop(float duration)
{
    // すでに止まっている場合も考慮して、元の状態を覚えておく
    bool previousState = followEnabled;
    followEnabled = false;

    float timer = 0f;
    while (timer < duration)
    {
        timer += Time.deltaTime;
        yield return null;
    }

    followEnabled = previousState;
}

これを使えば、EnemyHealthEnemyStunController など別コンポーネントから、
「スタン中は追尾しない」といった状態管理を簡単に組み込めます。

このように、「追尾する」という小さな責務のコンポーネントを用意しておくことで、敵AIやギミックの組み立てがぐっと楽になります。ぜひ自分のプロジェクトでも、機能ごとにコンポーネントを分割する設計を試してみてください。