【Cocos Creator】アタッチするだけ!PatrolPath (巡回ルート)の実装方法【TypeScript】

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

【Cocos Creator 3.8】PatrolPath の実装:Path2D 上を自動巡回し、端で折り返す汎用 AI 移動スクリプト

このガイドでは、Path2D に設定したポイントを順番に移動し、端に到達したら折り返して往復巡回する AI を、ノードにアタッチするだけで使える汎用コンポーネントとして実装します。

敵キャラや NPC、動くギミック(リフト・足場・トラップ)など、「決められたルートを行ったり来たりさせたい」場面でそのまま使い回せるよう、外部スクリプトへの依存ゼロで設計します。


コンポーネントの設計方針

機能要件の整理

  • Path2D コンポーネントに設定されたポイント群を利用する。
  • ポイントを 0 → 1 → 2 → … → N → … → 2 → 1 → 0 のように往復(折り返し)で移動する。
  • 一定速度で移動し、ターゲットポイントに十分近づいたら次のポイントへ切り替える。
  • ゲームのフレームレートに依存しないように deltaTime を考慮した移動を行う。
  • 必要であれば、移動方向に合わせてノードの向きを変える(例:左右反転や回転)。
  • 完全に独立したコンポーネントとして設計し、他のカスタムスクリプトには一切依存しない。

外部依存をなくすためのアプローチ

  • 巡回ルートの情報は Path2D コンポーネントから取得する。
  • Path2D は 同じノードにアタッチされている前提とし、onLoadgetComponent(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 の pointsVec2 配列として内部にコピーし、startIndex をクランプして初期インデックスを決定。
  • start
    snapToPathOnStarttrue の場合、startIndex のポイントにノードのワールド座標をスナップ。
    enabledOnStart に応じて _isMoving を設定し、自動巡回のオン/オフを制御。
  • update(deltaTime)
    _isMovingtrue で、Path2D が正しく設定されている場合のみ処理。
    pauseAtPoint が設定されていれば、ポイント到達後の一時停止を _pauseTimer で管理。
    – 現在位置とターゲットポイントの差分から方向ベクトルを算出し、moveSpeed * deltaTime だけ移動。
    alignToDirection が有効なら、移動方向から角度を計算し、ノードの Z 回転を更新。
  • _advanceIndex()
    loopType に応じて、インデックスの進め方を制御。
    Loop: 常に前進し、最後の次は 0 に戻る。
    PingPong: 端に到達したら _direction を反転して往復する。
  • startPatrol() / stopPatrol()
    – 他のスクリプトやアニメーションイベントから巡回の開始・停止を制御できるようにした補助メソッド。
    – ただし、このコンポーネントはそれ自体で完結しており、これらを必ず使う必要はない。

使用手順と動作確認

ここからは、Cocos Creator 3.8.7 のエディタ上で、実際に PatrolPath コンポーネントを使って巡回 AI を動かす手順を説明します。

1. スクリプトファイルの作成

  1. Assets パネルで右クリックします。
  2. Create → TypeScript を選択します。
  3. ファイル名を PatrolPath.ts に変更します。
  4. 作成された PatrolPath.ts をダブルクリックして開き、中身をすべて削除して、上記の TypeScript コードを丸ごと貼り付けて保存します。

2. 巡回させるノードの作成

例として、2D のスプライトを巡回させます。

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、Enemy など分かりやすい名前にします。
  2. Sprite の画像を設定しておきます(任意)。

3. Path2D コンポーネントの追加とポイント設定

  1. Hierarchy で先ほど作成した Enemy ノードを選択します。
  2. Inspector の Add Component ボタンをクリックします。
  3. Geometry → Path2D を選択して追加します。
  4. 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)
    • このように設定すると、四角形の頂点を巡回するルートになります。

注意: points が 1 個以下だとエラーになり巡回は行われません。必ず 2 個以上設定してください。

4. PatrolPath コンポーネントをアタッチ

  1. 引き続き Enemy ノードを選択した状態で、Inspector の Add Component をクリックします。
  2. Custom カテゴリ内にある PatrolPath を選択して追加します。

Inspector に PatrolPath の各種プロパティが表示されます。

5. プロパティの設定例

まずはシンプルな往復移動から試してみます。

  • Enabled On Start: true
  • Move Speed: 100
  • Arrival Threshold: 5
  • Loop Type: PingPong
  • Start Index: 0
  • Snap To Path On Start: true
  • Align To Direction: false(まずはオフで挙動確認)
  • Rotation Offset: 0
  • Pause At Point: 0

この設定では、ゲーム開始と同時に Path2D の 0 番ポイントからスタートし、ポイント 0 → 1 → 2 → 3 → 2 → 1 → 0 … と往復巡回します。

6. 再生して動作確認

  1. エディタ右上の (Play)ボタンを押してゲームを実行します。
  2. シーンビューまたは 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: true
  • Rotation Offset: キャラの絵に合わせて調整(例: キャラが右向きで描かれているなら 0、上向きで描かれているなら -90 など)。

これで、Path2D のルートに沿ってキャラが「進行方向を向きながら」移動するようになります。

例4: 途中のポイントからスタート

  • Start Index: 2
  • Snap 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 ではなく別のルート定義に差し替えたりと、このコンポーネントをベースにした拡張も行いやすいので、自分のゲームに合わせてカスタマイズしてみてください。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!