【Cocos Creator 3.8】PatrolPath の実装:Path2D 上を自動巡回し、端で折り返す汎用 AI 移動スクリプト
このガイドでは、Path2D に設定したポイントを順番に移動し、端に到達したら折り返して往復巡回する AI を、ノードにアタッチするだけで使える汎用コンポーネントとして実装します。
敵キャラや NPC、動くギミック(リフト・足場・トラップ)など、「決められたルートを行ったり来たりさせたい」場面でそのまま使い回せるよう、外部スクリプトへの依存ゼロで設計します。
コンポーネントの設計方針
機能要件の整理
- Path2D コンポーネントに設定されたポイント群を利用する。
- ポイントを 0 → 1 → 2 → … → N → … → 2 → 1 → 0 のように往復(折り返し)で移動する。
- 一定速度で移動し、ターゲットポイントに十分近づいたら次のポイントへ切り替える。
- ゲームのフレームレートに依存しないように
deltaTimeを考慮した移動を行う。 - 必要であれば、移動方向に合わせてノードの向きを変える(例:左右反転や回転)。
- 完全に独立したコンポーネントとして設計し、他のカスタムスクリプトには一切依存しない。
外部依存をなくすためのアプローチ
- 巡回ルートの情報は Path2D コンポーネントから取得する。
- Path2D は 同じノードにアタッチされている前提とし、
onLoadでgetComponent(Path2D)を試みる。 - Path2D が見つからない場合は エラーログを出し、移動処理を停止する(防御的実装)。
- 移動に Rigidbody などの物理は使わず、Transform(position)を書き換えるだけで動かすため、他コンポーネントへの依存を減らす。
@property でインスペクタから設定可能にする項目
次のようなプロパティを用意し、エディタから挙動を調整できるようにします。
- enabledOnStart (boolean)
– デフォルト:true
– 役割: ゲーム開始時(start)から自動で巡回を開始するかどうか。 - moveSpeed (number)
– デフォルト:100(単位: 単位/秒)
– 役割: 巡回移動の速度。値を大きくすると速く移動する。 - arrivalThreshold (number)
– デフォルト:5
– 役割: ターゲットポイントにどれくらい近づいたら「到達」とみなすかの距離(ピクセル)。
– 小さすぎるとポイントを通り過ぎてカクつくことがあるため、数ピクセル程度を推奨。 - loopType (Enum)
– デフォルト:PingPong
– 値:PingPong: 端まで行ったら折り返す(往復)。Loop: 端まで行ったら最初のポイントに戻る(ループ)。
- startIndex (number)
– デフォルト:0
– 役割: 巡回を開始する Path2D のポイントインデックス。
– ルート上の途中から動かしたい場合に使用。 - snapToPathOnStart (boolean)
– デフォルト:true
– 役割:start時にノード位置をstartIndexのポイント座標へスナップさせるかどうか。 - alignToDirection (boolean)
– デフォルト:false
– 役割: 移動方向に合わせてノードの回転を変えるかどうか。
– 2D 横スクロールなどで左右反転だけしたい場合は、ここをfalseにして、別の Sprite のフリップ制御と組み合わせても良い。 - rotationOffset (number)
– デフォルト:0(度)
– 役割:alignToDirectionが有効なとき、方向ベクトルから求めた角度に加算するオフセット角度。
– キャラの向き(アート)と数学的な 0 度方向が違う場合に補正するために使う。 - pauseAtPoint (number)
– デフォルト:0(秒)
– 役割: 各ポイントに到達したときに一時停止する時間。
– 例:0.5にすると、各ポイントで 0.5 秒止まってから次のポイントへ移動。
これらのプロパティにより、エディタ上で数値をいじるだけで挙動を細かく調整できます。
TypeScriptコードの実装
ここから、実際の PatrolPath.ts の実装コードを示します。
import { _decorator, Component, Node, Vec3, Vec2, math, Enum, Path2D, warn, error } from 'cc';
const { ccclass, property } = _decorator;
enum PatrolLoopType {
PingPong = 0,
Loop = 1,
}
@ccclass('PatrolPath')
export class PatrolPath extends Component {
@property({
tooltip: 'ゲーム開始時に自動で巡回を開始するかどうか。',
})
public enabledOnStart: boolean = true;
@property({
tooltip: '移動速度(単位/秒)。値を大きくすると速く移動します。',
min: 0,
})
public moveSpeed: number = 100;
@property({
tooltip: 'ターゲットポイントにどれだけ近づいたら「到達」とみなすかの距離(ピクセル)。',
min: 0.1,
})
public arrivalThreshold: number = 5;
@property({
type: Enum(PatrolLoopType),
tooltip: '巡回のループ方法。\nPingPong: 端まで行ったら折り返す。\nLoop: 最後のポイントの次は最初のポイントへ戻る。',
})
public loopType: PatrolLoopType = PatrolLoopType.PingPong;
@property({
tooltip: '巡回を開始する Path2D のポイントインデックス。\n範囲外の場合は 0 にクランプされます。',
min: 0,
})
public startIndex: number = 0;
@property({
tooltip: 'start 時にノードの位置を startIndex のポイントにスナップさせるかどうか。',
})
public snapToPathOnStart: boolean = true;
@property({
tooltip: '移動方向にノードの回転を合わせるかどうか。',
})
public alignToDirection: boolean = false;
@property({
tooltip: 'alignToDirection が有効なとき、方向ベクトルから求めた角度に加算するオフセット(度)。',
})
public rotationOffset: number = 0;
@property({
tooltip: '各ポイントに到達したときに一時停止する時間(秒)。\n0 の場合は停止しません。',
min: 0,
})
public pauseAtPoint: number = 0;
// 内部状態
private _path2D: Path2D | null = null;
private _points: Vec2[] = [];
private _currentIndex: number = 0;
private _direction: number = 1; // 1: 前進, -1: 後退
private _isMoving: boolean = false;
private _pauseTimer: number = 0;
onLoad() {
// 同じノードにアタッチされた Path2D を取得
this._path2D = this.getComponent(Path2D);
if (!this._path2D) {
error('[PatrolPath] Path2D コンポーネントが見つかりません。このノードに Path2D を追加してください。');
return;
}
// Path2D からポイントを取得
const points = this._path2D.points;
if (!points || points.length < 2) {
error('[PatrolPath] Path2D のポイントが 2 個以上必要です。Path2D の points を設定してください。');
return;
}
// Vec2 配列として保持
this._points = points.map(p => new Vec2(p.x, p.y));
// startIndex のクランプ
this._currentIndex = math.clamp(this.startIndex, 0, this._points.length - 1);
// 初期方向は前進
this._direction = 1;
}
start() {
if (!this._path2D || this._points.length < 2) {
// onLoad でエラー済み
return;
}
// 開始時の位置をルート上にスナップ
if (this.snapToPathOnStart) {
const startPoint = this._points[this._currentIndex];
const worldPos = this.node.getWorldPosition();
this.node.setWorldPosition(startPoint.x, startPoint.y, worldPos.z);
}
this._isMoving = this.enabledOnStart;
}
update(deltaTime: number) {
if (!this._isMoving || !this._path2D || this._points.length < 2) {
return;
}
// ポイント間の一時停止処理
if (this._pauseTimer > 0) {
this._pauseTimer -= deltaTime;
if (this._pauseTimer <= 0) {
this._pauseTimer = 0;
} else {
// まだ停止中
return;
}
}
const targetPoint = this._points[this._currentIndex];
// 現在位置(ワールド座標)
const currentWorldPos = this.node.getWorldPosition();
const currentPos2D = new Vec2(currentWorldPos.x, currentWorldPos.y);
// ターゲットへのベクトル
const toTarget = new Vec2(targetPoint.x - currentPos2D.x, targetPoint.y - currentPos2D.y);
const distance = toTarget.length();
if (distance <= this.arrivalThreshold) {
// ターゲットに到達したので次のポイントへ
this._advanceIndex();
// 必要なら一時停止
if (this.pauseAtPoint > 0) {
this._pauseTimer = this.pauseAtPoint;
}
return;
}
// 正規化して方向ベクトルを得る
const dir = toTarget.normalize();
// deltaTime を考慮した移動量
const moveDist = this.moveSpeed * deltaTime;
const moveVec = new Vec3(dir.x * moveDist, dir.y * moveDist, 0);
// 新しい位置を設定
const newWorldPos = new Vec3(
currentWorldPos.x + moveVec.x,
currentWorldPos.y + moveVec.y,
currentWorldPos.z
);
this.node.setWorldPosition(newWorldPos);
// 向きを移動方向に合わせる
if (this.alignToDirection && distance > 0.001) {
// atan2(y, x) でラジアン、rad2deg で度に変換
const angleRad = Math.atan2(dir.y, dir.x);
const angleDeg = angleRad * 180 / Math.PI;
const finalAngle = angleDeg + this.rotationOffset;
const euler = this.node.eulerAngles;
this.node.setRotationFromEuler(euler.x, euler.y, finalAngle);
}
}
/**
* 現在のインデックスを進め、必要に応じて折り返しやループを処理する。
*/
private _advanceIndex() {
const lastIndex = this._points.length - 1;
if (this.loopType === PatrolLoopType.Loop) {
// 常に前進し、最後の次は 0 に戻る
this._currentIndex += 1;
if (this._currentIndex > lastIndex) {
this._currentIndex = 0;
}
return;
}
// PingPong(往復)の場合
if (this._direction > 0) {
// 前進中
if (this._currentIndex >= lastIndex) {
// 端に到達したので折り返し
this._direction = -1;
this._currentIndex = lastIndex - 1; // 1 つ戻る
} else {
this._currentIndex += 1;
}
} else {
// 後退中
if (this._currentIndex <= 0) {
// 端に到達したので折り返し
this._direction = 1;
this._currentIndex = 1; // 1 つ進む
} else {
this._currentIndex -= 1;
}
}
// 万が一の防御的クランプ
this._currentIndex = math.clamp(this._currentIndex, 0, lastIndex);
}
/**
* 外部から巡回を開始するためのメソッド。
*/
public startPatrol() {
if (!this._path2D || this._points.length < 2) {
warn('[PatrolPath] Path2D が正しく設定されていないため、startPatrol は無視されました。');
return;
}
this._isMoving = true;
}
/**
* 外部から巡回を停止するためのメソッド。
*/
public stopPatrol() {
this._isMoving = false;
}
/**
* 現在のターゲットポイントインデックスを取得します。
*/
public getCurrentPointIndex(): number {
return this._currentIndex;
}
}
コードのポイント解説
onLoad
– 同じノードからPath2Dコンポーネントを取得。
– 見つからない、またはポイント数が 2 未満の場合はerrorを出して以降の処理を無効化。
– Path2D のpointsをVec2配列として内部にコピーし、startIndexをクランプして初期インデックスを決定。start
–snapToPathOnStartがtrueの場合、startIndexのポイントにノードのワールド座標をスナップ。
–enabledOnStartに応じて_isMovingを設定し、自動巡回のオン/オフを制御。update(deltaTime)
–_isMovingがtrueで、Path2D が正しく設定されている場合のみ処理。
–pauseAtPointが設定されていれば、ポイント到達後の一時停止を_pauseTimerで管理。
– 現在位置とターゲットポイントの差分から方向ベクトルを算出し、moveSpeed * deltaTimeだけ移動。
–alignToDirectionが有効なら、移動方向から角度を計算し、ノードの Z 回転を更新。_advanceIndex()
–loopTypeに応じて、インデックスの進め方を制御。
–Loop: 常に前進し、最後の次は 0 に戻る。
–PingPong: 端に到達したら_directionを反転して往復する。startPatrol()/stopPatrol()
– 他のスクリプトやアニメーションイベントから巡回の開始・停止を制御できるようにした補助メソッド。
– ただし、このコンポーネントはそれ自体で完結しており、これらを必ず使う必要はない。
使用手順と動作確認
ここからは、Cocos Creator 3.8.7 のエディタ上で、実際に PatrolPath コンポーネントを使って巡回 AI を動かす手順を説明します。
1. スクリプトファイルの作成
- Assets パネルで右クリックします。
Create → TypeScriptを選択します。- ファイル名を
PatrolPath.tsに変更します。 - 作成された
PatrolPath.tsをダブルクリックして開き、中身をすべて削除して、上記の TypeScript コードを丸ごと貼り付けて保存します。
2. 巡回させるノードの作成
例として、2D のスプライトを巡回させます。
- Hierarchy パネルで右クリック →
Create → 2D Object → Spriteを選択し、Enemyなど分かりやすい名前にします。 - Sprite の画像を設定しておきます(任意)。
3. Path2D コンポーネントの追加とポイント設定
- Hierarchy で先ほど作成した
Enemyノードを選択します。 - Inspector の
Add Componentボタンをクリックします。 Geometry → Path2Dを選択して追加します。- Path2D の
pointsプロパティに、巡回させたい座標を設定します。- 例:
- Element 0:
(x: 0, y: 0) - Element 1:
(x: 300, y: 0) - Element 2:
(x: 300, y: 200) - Element 3:
(x: 0, y: 200)
- Element 0:
- このように設定すると、四角形の頂点を巡回するルートになります。
- 例:
注意: points が 1 個以下だとエラーになり巡回は行われません。必ず 2 個以上設定してください。
4. PatrolPath コンポーネントをアタッチ
- 引き続き
Enemyノードを選択した状態で、Inspector のAdd Componentをクリックします。 Customカテゴリ内にあるPatrolPathを選択して追加します。
Inspector に PatrolPath の各種プロパティが表示されます。
5. プロパティの設定例
まずはシンプルな往復移動から試してみます。
Enabled On Start:trueMove Speed:100Arrival Threshold:5Loop Type:PingPongStart Index:0Snap To Path On Start:trueAlign To Direction:false(まずはオフで挙動確認)Rotation Offset:0Pause At Point:0
この設定では、ゲーム開始と同時に Path2D の 0 番ポイントからスタートし、ポイント 0 → 1 → 2 → 3 → 2 → 1 → 0 … と往復巡回します。
6. 再生して動作確認
- エディタ右上の
▶(Play)ボタンを押してゲームを実行します。 - シーンビューまたは Game ビューで、
Enemyノードが Path2D で指定したルートに沿って移動することを確認します。
想定と違う動きをした場合は、以下をチェックしてください。
- Path2D が同じノードに付いているか(他のノードに付けていないか)。
- Path2D の points が 2 個以上あるか。
- エディタの Console に
[PatrolPath]のエラーログが出ていないか。
7. 応用設定例
例1: ループ移動(ぐるぐる回る)
Loop Type:Loop
この設定にすると、0 → 1 → 2 → 3 → 0 → 1 → … のように、最後のポイントから最初のポイントに戻るループ移動になります。
例2: 各ポイントで一瞬止まる巡回
Pause At Point:0.5
これにより、各ポイントに到達したときに 0.5 秒停止してから次のポイントへ移動するようになります。見張りをしている敵などに使えます。
例3: 移動方向に合わせてキャラを回転させる
Align To Direction:trueRotation Offset: キャラの絵に合わせて調整(例: キャラが右向きで描かれているなら0、上向きで描かれているなら-90など)。
これで、Path2D のルートに沿ってキャラが「進行方向を向きながら」移動するようになります。
例4: 途中のポイントからスタート
Start Index:2Snap To Path On Start:true
この設定では、ゲーム開始時に ポイント 2 の位置にワープし、そこから巡回を開始します。シーン内に同じルートを使う敵を複数配置する場合、それぞれ違う Start Index を割り当てると、バラけた位置から動き始めるので単調さを減らせます。
8. スクリプトから巡回の開始・停止を制御したい場合(任意)
本コンポーネントは単体で完結しているため、他スクリプトは不要ですが、必要であれば以下のように制御できます。
// 例: 同じノードにアタッチされたスクリプトから制御する場合
import { _decorator, Component } from 'cc';
import { PatrolPath } from './PatrolPath';
const { ccclass } = _decorator;
@ccclass('ExampleController')
export class ExampleController extends Component {
private patrol: PatrolPath | null = null;
start() {
this.patrol = this.getComponent(PatrolPath);
if (this.patrol) {
this.patrol.stopPatrol(); // 最初は止めておく
// 条件が整ったら
this.patrol.startPatrol();
}
}
}
もちろん、このような制御スクリプトは任意であり、PatrolPath 単体で機能は完結しています。
まとめ
このガイドでは、Path2D に設定されたポイントを順番に移動し、端で折り返す巡回 AI を、完全に独立した汎用コンポーネントとして実装しました。
- Path2D を同じノードに追加し、ポイントを打つだけで巡回ルートを定義できる。
- 速度・到達判定距離・ループ方法・開始ポイント・一時停止時間・向き合わせなどを、すべてインスペクタから調整可能。
- 他のカスタムスクリプトに依存せず、この
PatrolPath.tsだけをプロジェクトに追加すればすぐに使える。 - 敵 AI、動く足場、移動するトラップ、巡回する NPC など、さまざまな用途にそのまま流用できる。
一度このような汎用コンポーネントを作っておけば、「ルートを描いて、ノードにアタッチするだけ」で巡回挙動を量産できるようになり、レベルデザインやプロトタイピングのスピードが大きく向上します。
プロジェクトごとに必要なパラメータを少し足したり、Path2D ではなく別のルート定義に差し替えたりと、このコンポーネントをベースにした拡張も行いやすいので、自分のゲームに合わせてカスタマイズしてみてください。




