「とりあえず Update に全部書いて動いたからOK!」——最初のうちはそれでも何とかなりますが、敵AI、プレイヤー操作、エフェクト制御…と機能が増えてくると、1つのスクリプトがあっという間に数百行の God クラスになってしまいます。

特に「敵キャラの移動」「プレイヤーへの追尾」「攻撃方向の制御」などを1つにまとめてしまうと、後から挙動を変えたいときに修正箇所が分散し、デバッグも困難になりますよね。

そこで今回は、「常にプレイヤーの方へ盾(無敵判定)を向けながら、じりじり近づいてくる敵」という、よくあるシチュエーションを題材にして、1つの責務に絞ったコンポーネントとして実装してみます。

このコンポーネントは「追尾+向き制御+盾の位置合わせ」だけに責務を限定し、攻撃判定やHP管理などは別コンポーネントに任せる前提で設計します。こうしておくと、「盾持ちの敵」プレハブを量産・調整しやすくなり、レベルデザインもかなり楽になります

【Unity】プレイヤーを追い詰める盾持ちAI!「ShieldBearer」コンポーネント

フルコード(ShieldBearer.cs)


using UnityEngine;

/// <summary>
//  常にプレイヤーの方向に盾を向けながら、じりじりと近づく敵キャラ用コンポーネント。
//  - 2D / 3D どちらでも利用可能(「水平面での追尾」を想定)
//  - 盾オブジェクトをプレイヤー方向に回転&位置調整
//  - 一定距離までは前進、それ以上近い場合は停止(調整可能)
//  攻撃判定やダメージ処理は別コンポーネントに任せることを想定しています。
// </summary>
[DisallowMultipleComponent]
public class ShieldBearer : MonoBehaviour
{
    [Header("ターゲット設定")]
    [SerializeField]
    private Transform target; // 通常はプレイヤーの Transform を指定

    [Header("移動設定")]
    [SerializeField]
    [Tooltip("ターゲットへ近づく移動速度(Units / 秒)")]
    private float moveSpeed = 2f;

    [SerializeField]
    [Tooltip("この距離より近づいたら前進をやめる")]
    private float stopDistance = 1.5f;

    [SerializeField]
    [Tooltip("回転の追従速度(大きいほど素早く向きが変わる)")]
    private float rotationSpeed = 10f;

    [Header("盾オブジェクト")]
    [SerializeField]
    [Tooltip("盾の Transform。敵キャラの子オブジェクトなどを指定")]
    private Transform shieldTransform;

    [SerializeField]
    [Tooltip("盾をどのくらい前方にオフセットさせるか(ローカル空間)")]
    private float shieldForwardOffset = 0.8f;

    [SerializeField]
    [Tooltip("盾をどのくらい上方向にオフセットさせるか(ローカル空間)")]
    private float shieldUpOffset = 0.5f;

    [Header("追尾挙動オプション")]
    [SerializeField]
    [Tooltip("Y軸の高さ差を無視して水平面上だけで追尾するか(3D向け)")]
    private bool ignoreVerticalAxis = true;

    [SerializeField]
    [Tooltip("2DゲームでZ軸回転を使って向きを変える場合は true(TopDown / 横スクロールなど)")]
    private bool use2DMode = false;

    // 内部用:計算に使う一時変数
    private Vector3 _directionToTarget;
    private Vector3 _flatDirection;

    private void Reset()
    {
        // コンポーネント追加時に、よく使う設定を自動で推測しておくと便利
        // 1. シーン内の "Player" タグを持つオブジェクトを探してターゲット候補にする
        GameObject player = GameObject.FindGameObjectWithTag("Player");
        if (player != null)
        {
            target = player.transform;
        }

        // 2. 子オブジェクトから "Shield" という名前を持つものを盾として自動設定
        if (shieldTransform == null)
        {
            Transform found = transform.Find("Shield");
            if (found != null)
            {
                shieldTransform = found;
            }
        }
    }

    private void Update()
    {
        if (target == null)
        {
            // ターゲットがいない場合は何もしない
            return;
        }

        // 1. ターゲットへの方向ベクトルを計算
        _directionToTarget = target.position - transform.position;

        // 2. 必要なら高さ差を無視(3D向け)
        if (ignoreVerticalAxis)
        {
            _flatDirection = new Vector3(_directionToTarget.x, 0f, _directionToTarget.z);
        }
        else
        {
            _flatDirection = _directionToTarget;
        }

        // ターゲットと同じ位置にいる(ほぼゼロ距離)の場合は向き計算をスキップ
        if (_flatDirection.sqrMagnitude < 0.0001f)
        {
            return;
        }

        // 3. 方向ベクトルを正規化
        Vector3 moveDir = _flatDirection.normalized;

        // 4. 一定距離以上離れている場合のみ前進する
        float distance = _flatDirection.magnitude;
        if (distance > stopDistance)
        {
            MoveTowardsTarget(moveDir);
        }

        // 5. プレイヤー方向を向く(本体の向き)
        RotateTowardsTarget(moveDir);

        // 6. 盾オブジェクトをプレイヤー方向に向けて位置を調整
        UpdateShieldTransform(moveDir);
    }

    /// <summary>
    /// ターゲット方向へじりじり移動させる処理
    /// </summary>
    /// <param name="moveDir">正規化されたターゲット方向</param>
    private void MoveTowardsTarget(Vector3 moveDir)
    {
        // Rigidbody を使わず Transform でシンプルに移動
        // 物理挙動と組み合わせたい場合は Rigidbody.MovePosition に差し替えてもOK
        Vector3 delta = moveDir * moveSpeed * Time.deltaTime;
        transform.position += delta;
    }

    /// <summary>
    /// ターゲット方向へ回転させる処理
    /// </summary>
    /// <param name="moveDir">正規化されたターゲット方向</param>
    private void RotateTowardsTarget(Vector3 moveDir)
    {
        if (use2DMode)
        {
            // --- 2Dモード(Z軸回転) ---
            // 2Dでは「右向き」を前方とするケースが多いので、必要に応じて調整してください
            float angle = Mathf.Atan2(moveDir.y, moveDir.x) * Mathf.Rad2Deg;

            // 現在の回転から目標角度へスムーズに補間
            Quaternion targetRot = Quaternion.AngleAxis(angle, Vector3.forward);
            transform.rotation = Quaternion.Lerp(transform.rotation, targetRot, rotationSpeed * Time.deltaTime);
        }
        else
        {
            // --- 3Dモード(Y軸回転) ---
            // 水平面上でターゲットの方向を向く
            Quaternion targetRot = Quaternion.LookRotation(moveDir, Vector3.up);
            transform.rotation = Quaternion.Slerp(transform.rotation, targetRot, rotationSpeed * Time.deltaTime);
        }
    }

    /// <summary>
    /// 盾の位置と回転をプレイヤー方向に合わせる処理
    /// </summary>
    /// <param name="moveDir">正規化されたターゲット方向</param>
    private void UpdateShieldTransform(Vector3 moveDir)
    {
        if (shieldTransform == null)
        {
            return;
        }

        if (use2DMode)
        {
            // 2D:盾も本体と同じ角度を向かせる
            float angle = Mathf.Atan2(moveDir.y, moveDir.x) * Mathf.Rad2Deg;
            Quaternion shieldRot = Quaternion.AngleAxis(angle, Vector3.forward);
            shieldTransform.rotation = shieldRot;

            // ローカル空間でのオフセットを計算(右方向を前とみなす)
            Vector3 localOffset = Vector3.right * shieldForwardOffset + Vector3.up * shieldUpOffset;
            shieldTransform.position = transform.position + shieldRot * localOffset;
        }
        else
        {
            // 3D:盾をターゲット方向に向ける
            Quaternion shieldRot = Quaternion.LookRotation(moveDir, Vector3.up);
            shieldTransform.rotation = shieldRot;

            // ローカル空間でのオフセット(前+上)を回転に応じて適用
            Vector3 localOffset = Vector3.forward * shieldForwardOffset + Vector3.up * shieldUpOffset;
            shieldTransform.position = transform.position + shieldRot * localOffset;
        }
    }

    /// <summary>
    /// 外部からターゲットを差し替えるための公開メソッド。
    /// 例:プレイヤーが複数いるゲームで、最も近いプレイヤーをターゲットにするなど。
    /// </summary>
    /// <param name="newTarget">新しいターゲット Transform</param>
    public void SetTarget(Transform newTarget)
    {
        target = newTarget;
    }
}

使い方の手順

  1. シーンにプレイヤーを用意する
    プレイヤー用の GameObject(例: Player)を作成し、Transform を持っている状態にします。
    可能であれば TagPlayer にしておくと、Reset() 時に自動でターゲットを拾ってくれます。
  2. 盾持ちの敵プレハブを作る
    • 空の GameObject を作成し、名前を ShieldEnemy などにする。
    • その子オブジェクトとして、盾用の GameObject を作成し、名前を Shield にする(コライダーや見た目の Mesh / Sprite を付ける)。
    • 親オブジェクトに ShieldBearer コンポーネントを追加する。

    子オブジェクト名を Shield にしておけば、Reset() 時に自動で shieldTransform が設定されます。

  3. インスペクターでパラメータを調整する
    • Target:プレイヤーの Transform をドラッグ&ドロップ(Tag が Player なら自動設定されているはずです)。
    • Move Speed:盾持ちがどれくらいの速度で近づいてくるか(例: 1.5 ~ 3.0)。
    • Stop Distance:この距離まで近づいたら足を止める(例: 1.2)。
    • Rotation Speed:プレイヤーの周りを回ったときに、どれくらい素早く盾を向け直すか。
    • Shield Transform:子オブジェクト Shield が自動で入っていなければ手動で指定。
    • Shield Forward Offset / Up Offset:盾をどれくらい前・上に出すか。コライダーの位置に合わせて微調整しましょう。
    • Ignore Vertical Axis:3DのトップダウンやTPSならオン、完全3Dで上下にも動く敵ならオフ。
    • Use 2D Mode:2Dゲーム(TopDown / 横スクロール)ならオン、3Dならオフ。
  4. 具体的な使用例
    • プレイヤーを追い詰める盾持ち兵(3Dアクション)
      プレイヤーの前方に常に盾を構えながらじりじり近づいてくる近接兵として使えます。
      別コンポーネントで「背後からの攻撃のみダメージが通る」などのロジックを組むと、立ち回りを要求する敵になります。
    • 動く盾付きギミック(2D横スクロール)
      ステージ上を移動する「盾付きトロッコ」などのギミックにこのコンポーネントを付けておき、プレイヤーを検知すると向きを変えて近づいてくるようにすると、シンプルなギミックが一気に「ボスっぽい」動きになります。
    • 動く床+盾(エスコート対象)
      プレイヤーを守る「護衛ドローン」として使い、ターゲットをプレイヤーにしておくと、プレイヤーの前に常に盾が出るエスコート演出が作れます。
      この場合、moveSpeed を少し高めにして、プレイヤーの移動速度に追従できるようにしておくと自然です。

メリットと応用

ShieldBearer コンポーネントは、以下のような点でプレハブ管理やレベルデザインを楽にしてくれます。

  • 責務が明確:追尾と盾の向き制御だけに責務を限定しているので、攻撃パターンやHP管理は別スクリプトに分離しやすいです。
  • プレハブの再利用性が高い:敵AIのコア部分がコンポーネント化されているため、見た目だけ変えた別の盾持ち敵プレハブを簡単に量産できます。
  • パラメータ駆動でバリエーションを作りやすいmoveSpeedstopDistance を変えるだけで、「超慎重に近づく重装兵」「ダッシュで迫る軽装兵」といったバリエーションが作れます。
  • 2D / 3D 両対応use2DMode フラグと ignoreVerticalAxis によって、同じロジックを2Dと3Dで共有できます。

さらに、「改造しやすい構造」になっているので、ゲームの仕様に合わせて簡単に拡張できます。

例として、「一定時間おきにターゲットを再探索し、最も近いプレイヤーを追う」ような改造案を示します。


/// <summary>
// シーン内の Player タグを持つオブジェクトから、最も近いものをターゲットにする例。
// Update ではなく、数秒おきに呼び出すと負荷が軽くなります。
/// </summary>
private void RetargetToNearestPlayer()
{
    GameObject[] players = GameObject.FindGameObjectsWithTag("Player");
    if (players.Length == 0)
    {
        return;
    }

    Transform nearest = null;
    float nearestSqrDist = float.MaxValue;

    foreach (GameObject p in players)
    {
        float sqrDist = (p.transform.position - transform.position).sqrMagnitude;
        if (sqrDist < nearestSqrDist)
        {
            nearestSqrDist = sqrDist;
            nearest = p.transform;
        }
    }

    if (nearest != null)
    {
        SetTarget(nearest);
    }
}

このように、ShieldBearer を小さな責務に絞ったコンポーネントとして設計しておくと、「追尾ロジックを別メソッドに切り出す」「ターゲット選択ロジックを差し替える」といった拡張がとてもやりやすくなります。
巨大な God クラスではなく、こうした小さなコンポーネントを組み合わせて、読みやすく保守しやすいプロジェクトを目指していきましょう。