Godotで「隊列を組んで動く敵」や「仲間キャラがキレイに並んでついてくる」みたいな演出を作ろうとすると、ついこう考えがちですよね。

  • フォロワー側のシーンを EnemyFollower.gd みたいに継承して、
    「リーダーを参照して、座標を計算して…」を全部1クラスに書いてしまう
  • あるいは、リーダー側に「部下を全部管理する巨大スクリプト」を書いてしまう
  • 隊列パターン(横一列、三角形、縦列…)が増えるたびに if / match が増殖してカオスになる

これ、動き始めはシンプルなんですが、

  • 「リーダーを変えたい」「隊列を途中で組み替えたい」
  • 「一部の敵だけ別の動きをさせたい」

といった要件が入るたびに、継承ツリーや巨大スクリプトがどんどん壊れやすくなります。
そこで今回は、「陣形のロジックだけ」を独立コンポーネントとして切り出して、どんなノードにも後付けできるようにしてみましょう。

紹介するのは、リーダーの後ろに三角形や横並びの陣形を保って追従するコンポーネント、「FormationMove」です。

【Godot 4】隊列はクラス継承じゃなくてコンポーネントで!「FormationMove」コンポーネント

この FormationMove コンポーネントは、

  • リーダー(先頭のキャラ)の位置・向き
  • 自分の「隊列内インデックス」
  • 隊列パターン(横一列 / 三角形 など)

をもとに、自分の「理想位置」を計算し、そこに向かって滑らかに移動するだけの、小さな Node です。
フォロワーの本体(敵AI、プレイヤーの仲間、動く足場など)は別スクリプトにしておき、「隊列に参加させたいノードに FormationMove をアタッチするだけ」で陣形移動を実現できます。


フルコード:FormationMove.gd


extends Node
class_name FormationMove
## 陣形移動コンポーネント
## 任意の2Dノードにアタッチして、リーダーの後ろに隊列を組んで追従させる

@export_node_path("Node2D") var leader_path: NodePath
## リーダーとなる Node2D へのパス
## - 例: プレイヤー、隊長の敵、移動する足場など
## - 空のままでも動作するが、その場合は何もしない

@export var formation_index: int = 0
## 隊列内のインデックス(0,1,2,...)
## - 0: 先頭のすぐ後ろ
## - 1: その後ろ、など
## 同じ index を複数のノードで共有してもOK(わざと重ねる演出など)

enum FormationType {
	HORIZONTAL_LINE,  ## 横一列
	TRIANGLE,         ## 三角形
	COLUMN            ## 縦列(行列)
}

@export var formation_type: FormationType = FormationType.HORIZONTAL_LINE
## 陣形の種類を切り替える
## - HORIZONTAL_LINE: 横一列
## - TRIANGLE: 三角形
## - COLUMN: 縦に並ぶ

@export var spacing: float = 48.0
## 隊列メンバー同士の基本間隔(ピクセル)
## 陣形によって前後・左右のオフセットに使われる

@export var follow_distance: float = 32.0
## リーダーからどれくらい離れた位置を基準とするか(前後方向の距離)

@export var move_speed: float = 8.0
## 理想位置への追従スピード
## - 数値が大きいほど素早く追従する
## - 小さすぎると「ゴムで引っ張られてる」ような感じになる

@export var use_leader_rotation: bool = true
## リーダーの向きに応じて陣形を回転させるかどうか
## - true: リーダーの向きに合わせて隊列も回転
## - false: 常にワールド座標の向きで陣形を保つ

@export var max_distance_before_snap: float = 512.0
## リーダーからあまりにも離れすぎたとき、
## 一気にワープ(スナップ)して追いつかせる距離のしきい値

@export var debug_draw: bool = false
## デバッグ用に「理想位置」を小さな十字で描画する

var _leader: Node2D
var _target_position: Vector2

func _ready() -> void:
	# leader_path からリーダーを取得
	if leader_path != NodePath():
		_leader = get_node_or_null(leader_path) as Node2D
	else:
		_leader = null
	
	if _leader == null:
		push_warning("FormationMove: leader is not assigned or not found. Component will be idle.")

	# 親ノードが Node2D であることを前提にする
	if get_parent() is not Node2D:
		push_warning("FormationMove should be attached under a Node2D (e.g. CharacterBody2D, Node2D, etc.).")

	# 初期ターゲット位置を現在位置にしておく
	var parent_2d := get_parent() as Node2D
	if parent_2d:
		_target_position = parent_2d.global_position


func _process(delta: float) -> void:
	if _leader == null:
		return
	
	var parent_2d := get_parent() as Node2D
	if parent_2d == null:
		return
	
	# 陣形内の「理想位置」を計算
	_target_position = _calculate_formation_position()
	
	# リーダーから離れすぎたら、スナップして瞬間移動
	var dist_to_leader := parent_2d.global_position.distance_to(_leader.global_position)
	if dist_to_leader > max_distance_before_snap:
		parent_2d.global_position = _target_position
		return
	
	# 線形補間で滑らかに追従
	var t := clampf(move_speed * delta, 0.0, 1.0)
	parent_2d.global_position = parent_2d.global_position.lerp(_target_position, t)
	
	if debug_draw:
		queue_redraw()


func _calculate_formation_position() -> Vector2:
	## 陣形タイプと index に応じて、リーダーからのオフセットを計算する
	if _leader == null:
		return Vector2.ZERO
	
	# ベースとなる位置は「リーダーの後ろ」
	var base_pos := _leader.global_position
	
	# リーダーの前方向・右方向ベクトル
	# Godot 2D では、角度0が右向き、+90度で下向きなので注意
	var forward := Vector2.RIGHT.rotated(_leader.global_rotation)
	var right := Vector2.UP.rotated(_leader.global_rotation) * -1.0
	# forward: 右向き(リーダーの進行方向とみなす)
	# right:   forward に対して右側
	
	if not use_leader_rotation:
		# リーダーの向きを無視して、ワールド座標に固定した forward/right を使う
		forward = Vector2.RIGHT
		right = Vector2.DOWN * -1.0  # (= Vector2.UP)

	var offset := Vector2.ZERO
	
	match formation_type:
		FormationType.HORIZONTAL_LINE:
			# リーダーの真後ろを中心に、左右に並べる
			# index=0: 真後ろ
			# index=1: 右側
			# index=2: 左側
			# index=3: 右側2個目... というイメージでジグザグ配置
			var row := formation_index
			var side := 0
			if row == 0:
				side = 0
			else:
				side = (row % 2 == 0) ? -1 : 1
			var side_index := int(ceil(row / 2.0))
			
			offset += -forward * follow_distance
			offset += right * spacing * side * side_index
		
		FormationType.TRIANGLE:
			# 三角形の陣形
			# 例: index
			# 0: 先頭の真後ろ
			# 1,2: その後ろ左右
			# 3,4,5: さらに後ろに3人... のように層を増やす
			var layer := 0
			var index_in_layer := 0
			
			# シンプルに「0, 1-2, 3-5, 6-9,...」という層構造を計算
			var remaining := formation_index
			var count_in_layer := 1
			while remaining >= count_in_layer:
				remaining -= count_in_layer
				layer += 1
				count_in_layer += 1
			index_in_layer = remaining
			# layer: 何段目か
			# index_in_layer: その段の中での位置(0〜)
			
			# 前後方向: 層が増えるほど後ろにずれる
			offset += -forward * (follow_distance + layer * spacing)
			
			# 左右方向: 段の中で左右に均等配置する
			if count_in_layer > 1:
				var total_width := (count_in_layer - 1) * spacing
				var start_x := -total_width * 0.5
				var x := start_x + index_in_layer * spacing
				offset += right * x
		
		FormationType.COLUMN:
			# 縦一列(行列)
			# index が増えるごとに後ろに並んでいく
			offset += -forward * (follow_distance + formation_index * spacing)
	
	return base_pos + offset


func _draw() -> void:
	if not debug_draw:
		return
	var parent_2d := get_parent() as Node2D
	if parent_2d == null:
		return
	
	# ローカル座標系に変換して小さな十字を描画
	var local_target := parent_2d.to_local(_target_position)
	var size := 4.0
	draw_line(local_target + Vector2(-size, 0), local_target + Vector2(size, 0), Color.YELLOW, 1.0)
	draw_line(local_target + Vector2(0, -size), local_target + Vector2(0, size), Color.YELLOW, 1.0)


## --- 便利API: ランタイムで隊列を変えたいとき用 ---

func set_leader(leader: Node2D) -> void:
	_leader = leader
	if _leader:
		leader_path = _leader.get_path()


func set_formation_index(index: int) -> void:
	formation_index = max(index, 0)


func set_formation_type(type: FormationType) -> void:
	formation_type = type

使い方の手順

ここでは、プレイヤーの後ろを仲間キャラが三角形の陣形でついてくる例と、敵の小隊が横一列で移動する例を紹介します。

例1: プレイヤーの後ろに仲間が三角形で追従

シーン構成例:

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── PlayerController (Script)   ※任意

Ally1 (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── FormationMove (Node)

Ally2 (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── FormationMove (Node)

Ally3 (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── FormationMove (Node)

手順①: コンポーネントスクリプトを用意

  • 上記の FormationMove.gd をプロジェクト内に保存します(例: res://components/FormationMove.gd)。
  • Godot の「スクリプト」タブで開き、class_name FormationMove が効いていることを確認します。

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

  • Ally1 シーンを開き、「+」ボタンで子ノードを追加します。
  • ノードタイプは Node(もしくは Node2D)でOKです。
  • 追加した子ノードに FormationMove.gd をアタッチします。
  • Ally2, Ally3 も同様に追加します。

手順③: インスペクタでパラメータを設定

  • leader_path: プレイヤー(Player ノード)をドラッグ&ドロップで指定
  • formation_type: TRIANGLE を選択
  • formation_index:
    • Ally1: 0(先頭のすぐ後ろ)
    • Ally2: 1
    • Ally3: 2
  • spacing: 48〜64 あたりから調整
  • follow_distance: 32〜64 あたりから調整
  • move_speed: 8〜12 くらいにすると自然な追従感になります

手順④: プレイヤーを動かすだけで隊列がついてくる

  • プレイヤーの移動は、いつも通り CharacterBody2Dvelocity を使って実装してOKです。
  • FormationMove は、親ノードの global_position を直接動かすだけなので、プレイヤー側のコードに一切手を入れる必要はありません
  • 三角形の陣形で仲間がついてくるはずです。

もし「プレイヤーの向きによって三角形の向きも変わってほしい」場合は use_leader_rotation = true に、
「向きは無視して、常に画面上で固定方向の三角形にしたい」場合は false にしておきましょう。


例2: 敵の小隊を横一列で移動させる

シーン構成例:

EnemyLeader (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── EnemyAI (Script)           ※移動・攻撃ロジック

EnemyFollower1 (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── FormationMove (Node)

EnemyFollower2 (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── FormationMove (Node)

EnemyFollower3 (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── FormationMove (Node)

使い方はプレイヤーの例と同じですが、設定だけ変えます。

  • formation_type: HORIZONTAL_LINE
  • formation_index:
    • EnemyFollower1: 0(中央)
    • EnemyFollower2: 1(右)
    • EnemyFollower3: 2(左)
  • spacing: 64〜96 にすると、横一列の幅が広がって「隊列感」が出ます。

リーダー側の EnemyAI では、単に velocity を前進させるだけでOKです。
隊列の維持はすべて FormationMove に任せられるので、敵AIのロジックと隊列ロジックを完全に分離できます。


メリットと応用

FormationMove をコンポーネントとして切り出すことで、こんなメリットがあります。

  • シーン構造がシンプル
    フォロワー側は「見た目」「当たり判定」「AI」「陣形移動」がそれぞれ独立したノード・スクリプトになります。
    継承で全部まとめてしまうより、どこに何が書いてあるか一目でわかりやすいですね。
  • 使い回しが効く
    敵だけでなく、プレイヤーの仲間、動く足場、ドローン、ペットなど、「隊列に参加させたいもの全部」に同じコンポーネントをアタッチできます。
  • 隊列パターンの切り替えが簡単
    formation_type を切り替えるだけで、横一列・三角形・縦列をスイッチできます。
    途中でパターン変更したい場合も、スクリプトから set_formation_type() を呼ぶだけです。
  • リーダーの差し替えがラク
    ボス戦で「フェーズ2から別の隊長についていく」みたいな演出も、set_leader() を呼ぶだけで実現できます。

つまり、「隊列」という概念を1つの小さなコンポーネントに押し込めておくことで、
キャラクターのロジック(AI)と、隊列のロジック(FormationMove)が完全に分離され、後からいくらでも差し替え・拡張ができるようになります。


改造案:隊列から一時離脱して特攻させる

例えば「HPが減ったら隊列から離脱して自爆特攻する」みたいな演出をしたいとき、
FormationMove に「一時停止」機能を足しておくと便利です。

以下のようなメソッドを追加してみましょう。


var _enabled: bool = true

func set_enabled(enabled: bool) -> void:
	_enabled = enabled

func _process(delta: float) -> void:
	if not _enabled:
		return  # 陣形追従を一時停止
	
	if _leader == null:
		return
	
	var parent_2d := get_parent() as Node2D
	if parent_2d == null:
		return
	
	_target_position = _calculate_formation_position()
	# ...(以降は元の処理と同じ)

これで、フォロワー側のAIから


var formation := $FormationMove
formation.set_enabled(false)  # 隊列から離脱
# あとは自分の好きなロジックで突撃させる

という感じで、簡単に「隊列から離脱→独自行動」という流れを作れます。
このように、小さなコンポーネントとして分割しておくと、「継承ツリーをいじらずに、振る舞いを足したり止めたりできる」のが大きな強みですね。