Godot 4のコンポーネント指向開発シリーズ、今回はアクションゲームのギミックに不可欠な**「PathMover (パス移動)」**です。

Godotには標準で PathFollow2D というノードがありますが、これは「Pathの子ノードにしなければならない」という制約があります。

今回のコンポーネントは、**「オブジェクト側がパスを参照して動く」**という逆のアプローチをとることで、敵や床を自由に配置しつつ、指定したルートを巡回させることができます。


このコンポーネントは、シーン上に配置された Path2D ノードを参照し、そのラインに沿って親ノードを強制的に移動させます。

1. コンポーネントのコード (Full Code)

以下のコードをコピーして、PathMover.gd という名前で保存してください。

class_name PathMover
extends Node

## 親ノードを指定した Path2D に沿って移動させるコンポーネント
## 動く床(AnimatableBody2D)や巡回する敵(CharacterBody2D/Area2D)に使えます。

# --- 設定パラメータ ---
@export_group("Path Settings")
@export var path_node: NodePath   ## 追従する Path2D ノードへのパス
@export var speed: float = 100.0  ## 移動速度
@export var loop: bool = true     ## ループするか(falseなら終点で止まる)
@export var rotate: bool = false  ## 進行方向を向くか
@export var ping_pong: bool = false ## 往復移動するか(Loopがtrueの場合のみ有効)

# --- 内部変数 ---
var _parent: Node2D
var _path: Path2D
var _curve: Curve2D
var _current_distance: float = 0.0
var _direction: int = 1 # 1: 前進, -1: 後退 (PingPong用)

func _ready() -> void:
	_parent = get_parent() as Node2D
	if not _parent:
		push_error("PathMover: 親が Node2D ではありません。")
		set_physics_process(false)
		return

	# Path2Dノードの取得
	if path_node:
		_path = get_node_or_null(path_node) as Path2D
	
	if not _path:
		push_warning("PathMover: Path2D が指定されていないか、見つかりません。")
		set_physics_process(false)
		return
		
	_curve = _path.curve
	
	# 初期位置をセット
	_update_position(0.0)

func _physics_process(delta: float) -> void:
	# 移動距離を加算
	_current_distance += speed * delta * _direction
	
	var max_len = _curve.get_baked_length()
	
	# --- ループ / 往復処理 ---
	if loop:
		if ping_pong:
			# 往復モード: 端に着いたら方向反転
			if _current_distance >= max_len:
				_current_distance = max_len
				_direction = -1
			elif _current_distance <= 0:
				_current_distance = 0
				_direction = 1
		else:
			# 通常ループ: 端を超えたら0に戻す
			if _current_distance > max_len:
				_current_distance -= max_len
			elif _current_distance < 0:
				_current_distance += max_len
	else:
		# ループなし: 端で止める
		_current_distance = clamp(_current_distance, 0, max_len)

	# 座標更新
	_update_position(delta)

func _update_position(_delta: float) -> void:
	# カーブ上の座標(ローカル)を取得
	var path_local_pos = _curve.sample_baked(_current_distance)
	
	# Path2Dのグローバル座標系に変換して親に適用
	# (Path2D自体が動いていても追従できるようになる)
	var target_global_pos = _path.to_global(path_local_pos)
	_parent.global_position = target_global_pos
	
	# 進行方向を向く処理
	if rotate:
		# 少し先の座標を取得して角度を計算
		var look_offset = 1.0 if _direction > 0 else -1.0
		var next_pos = _path.to_global(_curve.sample_baked(_current_distance + look_offset))
		_parent.look_at(next_pos)

2. 使い方チュートリアル

今回は例として**「動く床(Moving Platform)」**を作ってみます。

Godot 4でプレイヤーを乗せて動く床を作るには、AnimatableBody2D を使うのが正解です。

手順①:動くルート(Path2D)を描く

  1. メインシーンの適当な場所に Path2D ノードを追加します。
  2. エディタ上部のツールバーに出る「点の追加」ツールを使って、床が動くルートをカチカチとクリックして描きます。*
  3. 必要ならインスペクターで Curve2D を開き、微調整します。

手順②:動く床(親)を作る

  1. 新しいシーン、またはメインシーン内に AnimatableBody2D を追加します。
    • 注: CharacterBody2DStaticBody2D でも動きますが、プレイヤーを乗せて運ぶなら AnimatableBody2D が最適です。
  2. 床の見た目(Sprite2D)と当たり判定(CollisionShape2D)を設定します。

手順③:コンポーネントのアタッチと接続

  1. 床(AnimatableBody2D)の子ノードに PathMover(Node)を追加し、スクリプトをアタッチします。
  2. PathMover のインスペクターにある Path Node プロパティの「割り当て(Assign)」ボタンを押し、手順①で作った Path2D を選択します。

シーン構成図:

World
 ├── Path2D (ルート)
 └── MovingPlatform (AnimatableBody2D)
      ├── Sprite2D (床の画像)
      ├── CollisionShape2D
      └── PathMover (Node)  <-- Path Nodeプロパティで上のPath2Dを指定

手順④:設定と実行

インスペクターで動き方を調整します。

  • Speed: 移動速度。
  • Ping Pong: ON にすると、「行って戻って」を繰り返す一般的な動く床になります。
  • Loop: ON にしておかないと、片道で止まってしまいます。

実行すると、床がパスに沿って動き出し、プレイヤーが乗ると一緒に運ばれるはずです!


3. このコンポーネントのメリット

配置が圧倒的に楽になる

Godot標準のやり方(PathFollow2D を使う方法)だと、シーンツリー構造が以下のようになりがちです。

Path2D
 └── PathFollow2D
      └── RemoteTransform2D
           └── 実際に動かしたい床 (別の場所にある)

これだと、「床」と「パス」が親子関係に縛られ、レベルデザインの修正が面倒になります。

今回の PathMover コンポーネント方式なら、

「パスはパスで置いておく」「床は床で置いておく」

という独立した配置が可能になり、インスペクターでリンクさせるだけで済むため、管理が非常にスッキリします。

複数の敵で同じパスを使い回せる

1つの Path2D(例:巡回ルートA)に対して、複数の敵キャラクターの PathMover をリンクさせれば、**「1本のルート上を行列で歩く敵グループ」**が簡単に作れます。

それぞれの敵の _current_distance の初期値をずらす改造を加えれば、等間隔に配置することも可能です。

# (改造案) 初期位置をランダムにする
func _ready():
    # ... (省略)
    _current_distance = randf_range(0, _curve.get_baked_length())