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)を描く
- メインシーンの適当な場所に
Path2Dノードを追加します。 - エディタ上部のツールバーに出る「点の追加」ツールを使って、床が動くルートをカチカチとクリックして描きます。*
- 必要ならインスペクターで
Curve2Dを開き、微調整します。
手順②:動く床(親)を作る
- 新しいシーン、またはメインシーン内に
AnimatableBody2Dを追加します。- 注:
CharacterBody2DやStaticBody2Dでも動きますが、プレイヤーを乗せて運ぶならAnimatableBody2Dが最適です。
- 注:
- 床の見た目(Sprite2D)と当たり判定(CollisionShape2D)を設定します。
手順③:コンポーネントのアタッチと接続
- 床(AnimatableBody2D)の子ノードに
PathMover(Node)を追加し、スクリプトをアタッチします。 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())
