【Cocos Creator 3.8】PathFollower の実装:アタッチするだけで Path2D に沿った往復移動を実現する汎用スクリプト
このガイドでは、Cocos Creator 3.8.7 + TypeScript で、Path2D に沿ってノードを往復移動させるための汎用コンポーネント「PathFollower」を実装します。
任意のノードにアタッチし、Inspector で Path2D ノード と 移動速度や開始位置 を指定するだけで、他のスクリプトに依存せずに経路移動を実現できます。
コンポーネントの設計方針
機能要件の整理
- 指定した Path2D コンポーネント に沿ってノードを移動させる。
- 終点まで到達したら 折り返して往復 する(ループではなく「行き来」)。
- Inspector からのみ設定でき、他のカスタムスクリプトには一切依存しない。
- Path2D の曲線形状に合わせて ノードの位置を更新(必要に応じてローカル座標/ワールド座標を選べるようにする)。
- エディタ実行時に Path2D が未設定・未取得の場合は警告ログを出し、安全に動作を停止する。
Path2D について
Cocos Creator 3.8 では、2D ベクターパスを表現するために Path2D コンポーネント(cc.Path2D)を利用できます。
本コンポーネントでは、指定された Path2D に対して「0.0 ~ 1.0 の正規化パラメータ t」で位置をサンプリングし、t を時間経過で増減させることで往復移動を実現します。
実装上は「t を 0→1→0→1… と三角波のように変化させる」イメージです。
Inspector で設定可能なプロパティ設計
PathFollower に用意するプロパティとその役割は以下の通りです。
pathNode: Node | null
└ 経路として使用する Path2D コンポーネントを持つノード。ここから Path2D を取得してサンプリングします。useWorldSpace: boolean
└ true: Path2D の ワールド座標に合わせてノードのworldPositionを移動。
└ false: Path2D の ローカル座標に合わせてノードのpositionを移動(親子関係を使った相対移動にしたい場合など)。speed: number
└ 経路上を移動する速度(1秒あたりに進む正規化距離)。
例:speed = 0.2なら、約 5 秒で 0→1 に到達。startT: number
└ 経路上の開始位置(0.0 ~ 1.0)。
0 = 始点, 1 = 終点。
途中からスタートさせたい場合に使用します。playOnStart: boolean
└ true の場合、start() 時に自動で移動開始。
false の場合、初期位置にだけ配置して停止(エディタプレビュー用など)。autoAlignRotation: boolean
└ true の場合、進行方向に合わせてノードの回転(angle)を自動調整。
false の場合、回転は変更せず位置のみ移動。rotationOffset: number
└autoAlignRotationが true のときに、進行方向に対して追加で回転させるオフセット角度(度)。
例: スプライトの向きが右を向いているが、進行方向を上としたい場合などに調整。pauseAtEnds: number
└ 始点(t=0)と終点(t=1)に到達したときに 一時停止する時間(秒)。
0 の場合は停止せず即座に折り返します。
内部的には次のような状態を持ちます(Inspector には出しません)。
_path2D: Path2D | null… 実際に使用する Path2D 参照_t: number… 現在の正規化位置(0~1)_direction: number… 1 = 正方向(0→1), -1 = 逆方向(1→0)_isPlaying: boolean… 移動中かどうか_pauseTimer: number… 端点での一時停止残り時間
TypeScriptコードの実装
以下が完成した PathFollower コンポーネントの実装例です。
import { _decorator, Component, Node, Vec3, v3, warn, error, CCFloat, CCBoolean, CCInteger } from 'cc';
const { ccclass, property } = _decorator;
// Path2D は Cocos Creator の 2D パスコンポーネントです。
// プロジェクトで使用しているモジュールパスに応じて import パスを調整してください。
import { Path2D } from 'cc';
@ccclass('PathFollower')
export class PathFollower extends Component {
@property({
type: Node,
tooltip: '経路として使用する Path2D コンポーネントを持つノードを指定します。'
})
public pathNode: Node | null = null;
@property({
type: CCBoolean,
tooltip: 'true: Path2D のワールド座標に合わせて移動します。\nfalse: Path2D のローカル座標に合わせて移動します。'
})
public useWorldSpace: boolean = true;
@property({
type: CCFloat,
tooltip: '経路上の移動速度(正規化距離/秒)。\n例: 0.2 なら約 5 秒で 0→1 に到達します。'
})
public speed: number = 0.2;
@property({
type: CCFloat,
tooltip: '経路上の開始位置(0.0 ~ 1.0)。\n0 = 始点, 1 = 終点。'
})
public startT: number = 0;
@property({
type: CCBoolean,
tooltip: 'true の場合、start() 時に自動で移動を開始します。'
})
public playOnStart: boolean = true;
@property({
type: CCBoolean,
tooltip: 'true の場合、進行方向に合わせてノードの回転を自動調整します。'
})
public autoAlignRotation: boolean = false;
@property({
type: CCFloat,
tooltip: '自動回転時に進行方向に対して追加するオフセット角度(度)。'
})
public rotationOffset: number = 0;
@property({
type: CCFloat,
tooltip: '始点と終点に到達したときに一時停止する時間(秒)。\n0 の場合は停止せずに即座に折り返します。'
})
public pauseAtEnds: number = 0;
// 内部状態
private _path2D: Path2D | null = null;
private _t: number = 0;
private _direction: number = 1; // 1: 0→1, -1: 1→0
private _isPlaying: boolean = false;
private _pauseTimer: number = 0;
private _tempPos: Vec3 = v3();
private _tempForward: Vec3 = v3();
onLoad() {
// Path2D の取得を試みる(防御的実装)
if (this.pathNode) {
this._path2D = this.pathNode.getComponent(Path2D);
if (!this._path2D) {
warn('[PathFollower] 指定された pathNode に Path2D コンポーネントが見つかりません。', this.pathNode);
}
} else {
warn('[PathFollower] pathNode が設定されていません。Inspector で Path2D を持つノードを指定してください。', this.node);
}
// startT を 0~1 にクランプ
this._t = this._clamp01(this.startT);
// 初期方向は正方向
this._direction = 1;
this._pauseTimer = 0;
// Path2D が有効なら初期位置に配置
if (this._path2D) {
this._updatePositionAndRotation();
}
}
start() {
// 自動再生設定に応じて再生状態を決定
this._isPlaying = this.playOnStart && !!this._path2D;
if (!this._path2D) {
warn('[PathFollower] Path2D が見つからないため、移動は行われません。');
}
}
update(deltaTime: number) {
if (!this._path2D || !this._isPlaying) {
return;
}
// 端点での一時停止処理
if (this._pauseTimer > 0) {
this._pauseTimer -= deltaTime;
if (this._pauseTimer <= 0) {
this._pauseTimer = 0;
} else {
// まだ停止中
return;
}
}
// t を進める(往復のため direction を考慮)
const deltaT = this.speed * deltaTime * this._direction;
this._t += deltaT;
// 端に到達したら折り返し
if (this._t >= 1) {
this._t = 1;
this._direction = -1;
if (this.pauseAtEnds > 0) {
this._pauseTimer = this.pauseAtEnds;
}
} else if (this._t <= 0) {
this._t = 0;
this._direction = 1;
if (this.pauseAtEnds > 0) {
this._pauseTimer = this.pauseAtEnds;
}
}
this._updatePositionAndRotation();
}
/**
* 再生を開始します。
* Path2D が存在しない場合は警告を出して何もしません。
*/
public play() {
if (!this._path2D) {
warn('[PathFollower] Path2D が設定されていないため play() できません。', this.node);
return;
}
this._isPlaying = true;
}
/**
* 一時停止します(現在位置は保持します)。
*/
public pause() {
this._isPlaying = false;
}
/**
* 経路上の位置 t(0~1)を直接指定して移動させます。
* 自動移動の向きも t に応じて更新されます。
*/
public setPositionOnPath(t: number) {
this._t = this._clamp01(t);
// 端にいる場合の向き調整(好みに応じて変更可能)
if (this._t >= 1) {
this._direction = -1;
} else if (this._t <= 0) {
this._direction = 1;
}
this._updatePositionAndRotation();
}
/**
* 内部用: 0~1 にクランプ
*/
private _clamp01(v: number): number {
if (v < 0) return 0;
if (v > 1) return 1;
return v;
}
/**
* 現在の t に基づいて位置と回転を更新する。
*/
private _updatePositionAndRotation() {
if (!this._path2D) {
return;
}
// Path2D から位置サンプルを取得
// 実際の API 名は使用している Path2D 実装に合わせて調整してください。
// ここでは「getPointAt(t: number, out: Vec3): Vec3 | null」という想定の汎用的な書き方にしています。
const pos = this._path2D.getPointAt(this._t, this._tempPos);
if (!pos) {
warn('[PathFollower] Path2D から位置を取得できませんでした。t =', this._t);
return;
}
if (this.useWorldSpace) {
this.node.setWorldPosition(pos);
} else {
this.node.setPosition(pos);
}
if (this.autoAlignRotation) {
// 進行方向ベクトルを取得するため、少し先/少し前の点をサンプリング
const offset = 0.001; // 非常に小さいオフセット
let tAhead = this._t + offset * this._direction;
tAhead = this._clamp01(tAhead);
const forwardPos = this._path2D.getPointAt(tAhead, this._tempForward);
if (forwardPos) {
// 2D 空間での方向ベクトル (x, y) から角度を算出
const dx = forwardPos.x - pos.x;
const dy = forwardPos.y - pos.y;
const angleRad = Math.atan2(dy, dx); // ラジアン
const angleDeg = angleRad * 180 / Math.PI;
// Cocos のノードはデフォルトで右向きスプライトが 0 度なので、
// そのまま angle を設定し、オフセットを加算する。
this.node.angle = angleDeg + this.rotationOffset;
}
}
}
}
主要な処理の解説
onLoad()pathNodeからPath2Dコンポーネントを取得し、見つからなければwarnを出力。startTを 0~1 にクランプし、_tにセット。- Path2D が有効なら
_updatePositionAndRotation()で初期位置に移動。
start()playOnStartが true かつ Path2D が取得できていれば_isPlaying = true。- Path2D が無い場合は警告を出し、移動は行わない。
update(deltaTime)- Path2D が無い、または
_isPlayingが false のときは何もしない。 pauseAtEndsによる端点での一時停止を処理。_tをspeed * deltaTime * _direction分だけ進める。_tが 0 または 1 を超えたらクランプし、_directionを反転(往復)。- 最後に
_updatePositionAndRotation()で実際の座標と回転を更新。
- Path2D が無い、または
_updatePositionAndRotation()- Path2D の
getPointAt(t, out)で現在位置を取得し、useWorldSpaceに応じてsetWorldPositionまたはsetPositionを呼ぶ。 autoAlignRotationが true の場合、少し先の位置をサンプリングして進行方向ベクトルを求め、Math.atan2から角度を算出してnode.angleに設定。
- Path2D の
play() / pause()- 外部から手動で再生・一時停止を切り替えたい場合に使える補助メソッド。
使用手順と動作確認
1. PathFollower スクリプトを作成する
- Assets パネルで右クリック → Create → TypeScript を選択します。
- ファイル名を PathFollower.ts にします。
- 自動生成されたコードをすべて削除し、本記事の 「TypeScriptコードの実装」 セクションのコードをそのまま貼り付けて保存します。
2. Path2D ノードを用意する
ここでは、シンプルな 2D シーンでテストする例を示します。
- Hierarchy パネルで右クリック → Create → 2D Object → Node などを選択し、PathRoot という名前のノードを作成します。
- PathRoot を選択し、Inspector の Add Component ボタンをクリックします。
- 検索欄に Path2D と入力し、表示された Path2D コンポーネントを追加します。
- Path2D の Inspector で、パスの頂点や制御点を編集し、任意の曲線・折れ線を作成します。
(エディタ上でハンドルをドラッグして経路をデザインしてください。)
※もしコンポーネント一覧に Path2D が見つからない場合は、使用しているテンプレートやモジュール構成によって名前や場所が異なる可能性があります。その場合は、プロジェクト内の Path2D 実装の API(getPointAt のシグネチャなど)に合わせて import と呼び出し部分を調整してください。
3. 経路に沿って移動させたいノードを作成する
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、Follower という名前のスプライトノードを作成します。
- 必要に応じて Sprite の画像(Texture)を設定し、サイズやアンカーを調整します。
4. PathFollower コンポーネントをアタッチする
- Hierarchy で Follower ノードを選択します。
- Inspector の Add Component ボタンをクリック → Custom → PathFollower を選択して追加します。
- Inspector 上で以下のように設定します。
- Path Node: 先ほど作成した PathRoot ノードをドラッグ&ドロップ。
- Use World Space:
true(まずはシンプルにワールド座標でテスト)。 - Speed:
0.2(約 5 秒で往復片道)。 - Start T:
0(経路の始点からスタート)。 - Play On Start:
true(シーン再生と同時に移動開始)。 - Auto Align Rotation:
true(進行方向に自動回転)。 - Rotation Offset:
0(必要に応じて調整)。 - Pause At Ends:
0.5(端で 0.5 秒停止してから折り返す)。
5. シーンを再生して動作確認する
- 上部ツールバーの ▶(Play) ボタンを押してゲームを再生します。
- Follower ノードが、指定した Path2D の形状に沿ってスムーズに移動し、終点で折り返して往復していることを確認します。
- Pause At Ends を 0 にすると、端で止まらずに即座に折り返す挙動になります。
- Use World Space を false にすると、Path2D のローカル座標系に沿った相対移動になります。
例えば PathRoot をまとめて動かすと、Follower もそれに追従して動くようになります。
6. よくあるつまずきポイントと対処
- Path2D が見つからないという警告が出る
→ PathFollower の Path Node に、必ず Path2D コンポーネントを持つノードを指定してください。
→ 指定ノードに Path2D が付いているか Inspector で確認しましょう。 - ノードが動かない
→Play On Startが true になっているか確認。
→ Path2D の経路が極端に短い(または全て同じ座標)だと、動いているように見えないことがあります。 - 進行方向とスプライトの向きが合わない
→Auto Align Rotationを true にした上で、Rotation Offsetを 90, 180 などに変えてみてスプライトの初期向きと合わせてください。
まとめ
この PathFollower コンポーネントを使うことで、
- Path2D の形状さえ用意すれば、任意のノードを簡単に経路移動させられる
- Inspector のプロパティだけで速度・開始位置・往復挙動・一時停止・回転追従を調整できる
- 他のカスタムスクリプトやシングルトンに依存しないため、どのプロジェクトにもコピペで持ち込める
といったメリットが得られます。
応用例としては、
- 敵キャラクターのパトロール経路
- 背景オブジェクト(雲・船・列車など)の演出移動
- UI 要素を曲線に沿って動かす演出
など、2D ゲームのさまざまなシーンで再利用できます。
Path2D の設計と PathFollower のパラメータ調整だけで、移動系の演出を効率よく量産できるようになるはずです。
必要に応じて、speed をアニメーションカーブで変化させたり、pause() / play() を他のコンポーネントから呼び出してイベントに連動させるなど、プロジェクトに合わせて拡張してみてください。




