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: 1Ally3: 2
- spacing: 48〜64 あたりから調整
- follow_distance: 32〜64 あたりから調整
- move_speed: 8〜12 くらいにすると自然な追従感になります
手順④: プレイヤーを動かすだけで隊列がついてくる
- プレイヤーの移動は、いつも通り
CharacterBody2Dのvelocityを使って実装して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) # 隊列から離脱
# あとは自分の好きなロジックで突撃させる
という感じで、簡単に「隊列から離脱→独自行動」という流れを作れます。
このように、小さなコンポーネントとして分割しておくと、「継承ツリーをいじらずに、振る舞いを足したり止めたりできる」のが大きな強みですね。
