Unityでプレイヤーや敵キャラを動かし始めると、つい Update() に「移動」「入力」「カメラ追従」「エフェクト制御」など、あらゆる処理を詰め込んでしまいがちですね。最初は動くので満足できますが、

  • 処理が増えるたびに Update() が肥大化して読みづらい
  • 敵の種類ごとに挙動を変えたくなったとき、共通化が難しい
  • プレハブを複製しても、ちょっとした違いのために巨大スクリプトをコピー&改造し始めてしまう

といった問題が一気に噴き出してきます。

そこで今回は、「陣形移動」という 1 つの責務だけに絞ったコンポーネントを用意して、
「隊列を組んで動く敵」や「仲間キャラが隊列でついてくる」挙動を、シンプルに再利用できる形にしてみましょう。

【Unity】隊列AIをコンポーネント化!「FormationMove」コンポーネント

FormationMove コンポーネントは、

  • リーダー(先頭のキャラ)を 1 体決める
  • フォロワー(追従するキャラ)は、リーダーの後ろに「陣形」を保って追従する
  • 陣形は「横一列」「三角形(V字)」「カスタムオフセット」から選べる

という役割だけを持たせた、小さなコンポーネントです。
動きそのもの(ナビメッシュで移動するのか、Rigidbody で滑らせるのか等)は別コンポーネントに任せて、
ここでは「どこに向かうべきか(ターゲット位置)」だけを計算するように分離します。

フルコード:FormationMove.cs


using UnityEngine;

/// <summary>陣形のタイプ</summary>
public enum FormationType
{
    Line,       // 横一列
    Triangle,   // V字・三角形
    Custom      // 任意のオフセット
}

/// <summary>
/// 陣形移動コンポーネント。
/// リーダーの Transform とインデックスをもとに、
/// 「このフォロワーはどこにいるべきか」の目標位置を計算します。
/// 実際の移動(補間やNavMeshAgentなど)は別コンポーネントに任せる想定です。
/// </summary>
[DisallowMultipleComponent]
public class FormationMove : MonoBehaviour
{
    [Header("基本設定")]
    [SerializeField]
    private Transform leader; // 追従対象となるリーダー

    [SerializeField]
    private FormationType formationType = FormationType.Line;

    [Tooltip("このフォロワーが隊列の何番目か(0,1,2...)。0をリーダーとみなす構成でもOK。")]
    [SerializeField]
    private int formationIndex = 1;

    [Header("共通オフセット設定")]
    [Tooltip("リーダーからどれくらい後ろに下がるか(Z方向の距離)。")]
    [SerializeField]
    private float baseBackDistance = 2.0f;

    [Tooltip("隊列の横方向の間隔。")]
    [SerializeField]
    private float horizontalSpacing = 1.5f;

    [Header("三角形(V字)設定")]
    [Tooltip("縦方向(前後)の間隔。大きいほど縦に長い三角形になります。")]
    [SerializeField]
    private float triangleRowSpacing = 1.5f;

    [Header("カスタム陣形設定")]
    [Tooltip("Custom のときだけ使用。リーダーからのローカルオフセット。")]
    [SerializeField]
    private Vector3 customOffset = new Vector3(0, 0, -2f);

    [Header("追従挙動")]
    [Tooltip("目標位置への追従速度。")]
    [SerializeField]
    private float followSpeed = 5f;

    [Tooltip("Y軸だけを合わせたい場合は true。(例:2Dゲーム、地面の高さは別で制御など)")]
    [SerializeField]
    private bool ignoreY = false;

    // 現在の目標位置(デバッグや他コンポーネントから参照したいとき用)
    public Vector3 CurrentTargetPosition { get; private set; }

    private void Reset()
    {
        // コンポーネントをアタッチしたとき、自動で leader を設定しようとする
        if (leader == null)
        {
            // 親オブジェクトをリーダー候補とみなす
            if (transform.parent != null)
            {
                leader = transform.parent;
            }
        }

        // 自分のインデックスを仮に子の順番から推定
        if (transform.parent != null)
        {
            formationIndex = transform.GetSiblingIndex();
        }
        else
        {
            formationIndex = 1;
        }
    }

    private void Update()
    {
        if (leader == null)
        {
            // リーダーがいない場合は何もしない
            return;
        }

        // 陣形ごとのオフセットを計算
        Vector3 localOffset = CalculateLocalOffset();

        // リーダーのローカル座標系からワールド座標へ変換
        Vector3 targetPosition = leader.TransformPoint(localOffset);

        if (ignoreY)
        {
            // 高さは現在位置を維持
            targetPosition.y = transform.position.y;
        }

        CurrentTargetPosition = targetPosition;

        // 目標位置へスムーズに追従(ここでは単純な Lerp を使用)
        transform.position = Vector3.Lerp(
            transform.position,
            targetPosition,
            followSpeed * Time.deltaTime
        );
    }

    /// <summary>
    /// 陣形タイプに応じて、リーダーからのローカルオフセットを計算する。
    /// </summary>
    private Vector3 CalculateLocalOffset()
    {
        switch (formationType)
        {
            case FormationType.Line:
                return CalculateLineOffset();
            case FormationType.Triangle:
                return CalculateTriangleOffset();
            case FormationType.Custom:
                return customOffset;
            default:
                return Vector3.zero;
        }
    }

    /// <summary>
    /// 横一列のオフセットを計算。
    /// リーダーの真後ろを基準に、左右に並べていきます。
    /// </summary>
    private Vector3 CalculateLineOffset()
    {
        // 例: index=1 が左、2 が右、3 が左、4 が右... のようにジグザグに配置
        int side = (formationIndex % 2 == 0) ? 1 : -1; // 偶数なら右、奇数なら左
        int rank = (formationIndex + 1) / 2;          // 何列目か(0 はリーダー想定)

        float x = side * horizontalSpacing * rank;
        float z = -baseBackDistance; // リーダーの後ろ

        return new Vector3(x, 0f, z);
    }

    /// <summary>
    /// 三角形(V字)のオフセットを計算。
    /// リーダーの後ろに、左右対称の三角形になるように配置します。
    /// </summary>
    private Vector3 CalculateTriangleOffset()
    {
        if (formationIndex <= 0)
        {
            // 0 をリーダーとみなす構成なら、0番はリーダー位置と同じにしてもよい
            return new Vector3(0f, 0f, -baseBackDistance);
        }

        // 行(row)と列(column)を決める
        // 例:
        // row 1: index 1-2
        // row 2: index 3-5
        // row 3: index 6-9 ... のように増やしていくイメージ
        int row = 1;
        int countInRow = 2;
        int remaining = formationIndex;

        while (remaining > countInRow)
        {
            remaining -= countInRow;
            row++;
            countInRow++;
        }

        // row 行目の中で、左右に並べる
        // row=1 なら 2 体、row=2 なら 3 体... のような三角形
        float totalWidth = (countInRow - 1) * horizontalSpacing;
        float startX = -totalWidth * 0.5f;

        float x = startX + horizontalSpacing * (remaining - 1);
        float z = -baseBackDistance - triangleRowSpacing * (row - 1);

        return new Vector3(x, 0f, z);
    }

    /// <summary>
    /// ランタイム中にリーダーを変更したい場合用のメソッド。
    /// </summary>
    public void SetLeader(Transform newLeader)
    {
        leader = newLeader;
    }

    /// <summary>
    /// ランタイム中に陣形タイプを変更したい場合用のメソッド。
    /// </summary>
    public void SetFormationType(FormationType newType)
    {
        formationType = newType;
    }

    /// <summary>
    /// ランタイム中にインデックスを変更したい場合用のメソッド。
    /// </summary>
    public void SetFormationIndex(int newIndex)
    {
        formationIndex = Mathf.Max(0, newIndex);
    }
}

使い方の手順

  1. シーンにリーダーを用意する
    例として、プレイヤーキャラや敵の隊長など、先頭になるオブジェクトを 1 つ用意します。

    • Rigidbody で動かしてもいいですし、NavMeshAgent や CharacterController でもOKです。
    • このリーダーは FormationMove を付けなくても構いません(付けてもいいですが、index=0 などにしておくと分かりやすいです)。
  2. フォロワーのプレハブを作る
    追従させたいキャラ(仲間や雑魚敵など)のプレハブに、FormationMove コンポーネントを追加します。
    インスペクターで以下を設定しましょう。

    • Leader:リーダーとなるオブジェクトの Transform をドラッグ&ドロップ
    • Formation TypeLine(横一列)か Triangle(V字)、または Custom
    • Formation Index:1, 2, 3… と番号を振る(隊列の番号)
    • Base Back Distance / Horizontal Spacing / Triangle Row Spacing:隊列の見た目を調整
    • Follow Speed:追従のスムーズさ。大きいほどキビキビ追従します。

    例:仲間キャラ 3 体をプレイヤーの後ろに三角形で追従させたい場合

    • Follower A: Formation Index = 1
    • Follower B: Formation Index = 2
    • Follower C: Formation Index = 3
    • Formation Type = Triangle
  3. プレハブをシーンに複数配置する
    敵の集団や仲間キャラを複数体シーンに置き、
    それぞれの FormationMoveFormation Index を変えて隊列を作ります。
    プレハブ自体は同じでも、インスペクターのパラメータだけで隊列の形が変えられるのがポイントですね。

  4. 動作確認:リーダーを動かしてみる
    リーダーに移動用スクリプト(例:WASD で動く簡単な PlayerMove)を付けて、再生してみましょう。
    フォロワーたちが、リーダーの後ろで指定した陣形を保ちながら追従してくれれば成功です。

    簡単なリーダー用の移動スクリプト例(テスト用):

    
    using UnityEngine;
    
    [RequireComponent(typeof(CharacterController))]
    public class SimpleLeaderMove : MonoBehaviour
    {
        [SerializeField] private float moveSpeed = 5f;
        [SerializeField] private float rotateSpeed = 180f;
    
        private CharacterController controller;
    
        private void Awake()
        {
            controller = GetComponent<CharacterController>();
        }
    
        private void Update()
        {
            float h = Input.GetAxis("Horizontal");
            float v = Input.GetAxis("Vertical");
    
            // 前後移動
            Vector3 move = transform.forward * v * moveSpeed;
            controller.SimpleMove(move);
    
            // 左右回転
            transform.Rotate(Vector3.up, h * rotateSpeed * Time.deltaTime);
        }
    }
    

    この SimpleLeaderMove をリーダーに付けておけば、
    WASD でリーダーを動かしつつ、FormationMove でフォロワーを追従させるテストができます。

メリットと応用

FormationMove をコンポーネントとして分離しておくことで、

  • プレハブの再利用性が高い
    敵でも仲間でも「隊列で動くキャラ」にしたいときは、このコンポーネントを付けるだけ。
    リーダーや移動方式が違っても、陣形ロジックは共通で使い回せます。
  • レベルデザインが楽になる
    シーン上でフォロワーをポチポチ複製して、Formation Index を 1,2,3… と振っていくだけで、
    「3 体の三角形」「5 体の横一列」「カスタム陣形のボス護衛隊」などを直感的に配置できます。
  • 責務が明確で保守しやすい
    陣形のロジックを変えたいときは、このコンポーネントだけを触ればOK。
    移動の補間方法やアニメーション制御は別コンポーネントに任せられるので、
    「巨大な Update 関数」を避けやすくなります。

応用としては、

  • 敵 AI が隊列を変形する(Line → Triangle → Line)
  • プレイヤーが隊列フォーメーションを切り替えるスキルを持つ
  • ダメージを受けたフォロワーを隊列から外し、残りのインデックスを詰める

といった演出も簡単に実現できます。

例えば、「一定時間ごとに陣形をローテーションする」ような改造をしたい場合、
以下のような補助メソッドを別コンポーネントに書いておくと、動的な隊列変化を演出できます。


using UnityEngine;

public class FormationSwitcher : MonoBehaviour
{
    [SerializeField] private FormationMove formationMove;
    [SerializeField] private float switchInterval = 5f;

    private float timer;
    private FormationType[] pattern = 
    {
        FormationType.Line,
        FormationType.Triangle,
        FormationType.Custom
    };
    private int currentIndex = 0;

    private void Update()
    {
        if (formationMove == null) return;

        timer += Time.deltaTime;
        if (timer >= switchInterval)
        {
            timer = 0f;
            currentIndex = (currentIndex + 1) % pattern.Length;
            formationMove.SetFormationType(pattern[currentIndex]);
        }
    }
}

このように、FormationMove 自体は「隊列内での位置を決めるだけ」の小さな責務にとどめておくと、
上に被せる演出コンポーネント(FormationSwitcher のようなもの)を自由に増やしていけます。
Godクラスを避けつつ、陣形移動を気持ちよくコンポーネント指向で組み立てていきましょう。