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 つ用意します。- Rigidbody で動かしてもいいですし、NavMeshAgent や CharacterController でもOKです。
- このリーダーは
FormationMoveを付けなくても構いません(付けてもいいですが、index=0 などにしておくと分かりやすいです)。
-
フォロワーのプレハブを作る
追従させたいキャラ(仲間や雑魚敵など)のプレハブに、FormationMoveコンポーネントを追加します。
インスペクターで以下を設定しましょう。- Leader:リーダーとなるオブジェクトの Transform をドラッグ&ドロップ
- Formation Type:
Line(横一列)か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
-
プレハブをシーンに複数配置する
敵の集団や仲間キャラを複数体シーンに置き、
それぞれのFormationMoveのFormation Indexを変えて隊列を作ります。
プレハブ自体は同じでも、インスペクターのパラメータだけで隊列の形が変えられるのがポイントですね。 -
動作確認:リーダーを動かしてみる
リーダーに移動用スクリプト(例: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クラスを避けつつ、陣形移動を気持ちよくコンポーネント指向で組み立てていきましょう。
