【Cocos Creator】アタッチするだけ!PathMover (パス移動)の実装方法【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】PathMover の実装:アタッチするだけで「Path2D に沿った移動(動く床など)」を実現する汎用スクリプト

このガイドでは、任意のノードを Path2D コンポーネントで定義したパスに沿って自動移動させる汎用コンポーネント PathMover を実装します。
動く床・巡回する敵・往復するエレベーターなど、ノードにアタッチして Path2D を指定するだけで使えるように設計します。


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

1. 機能要件の整理

  • 任意のノードを Path2D に沿って移動させる。
  • Path2D コンポーネントを持つノード(パスノード)をインスペクタで指定できる。
  • パス上の移動は以下のように制御できる:
    • 移動速度(一定速度)
    • 移動方向(順方向/逆方向)
    • ループ方式(ループ/往復/一回のみ)
    • 開始位置(パス上の 0〜1 の正規化位置)
    • 開始時に待機時間を入れる
    • 往復時の折り返し時に待機時間を入れる
  • Unity などでよくある「パスに沿った巡回」を、Cocos Creator 3.8 の Path2D で簡単に再現する。
  • 他のカスタムスクリプトには一切依存しない(完全独立)。

2. 外部依存をなくす設計アプローチ

  • 移動対象ノードは このコンポーネントがアタッチされているノード自身とする。
  • パス情報は インスペクタで指定された Path2D を持つノードから取得する。
  • Path2D の標準 API を使い、getNormalizedLengthPosition などでパス上の座標を取得する。
  • パスノードが設定されていない、または Path2D コンポーネントが見つからない場合は エラーログを出して自動移動を停止する。
  • すべての挙動は インスペクタの @property から調整できるようにする。

3. インスペクタで設定可能なプロパティ設計

以下のようなプロパティを用意します(名前は英語、説明はツールチップで日本語)。

  • @property(Node) pathNode
    • 役割: Path2D コンポーネントを持つノードを指定する。
    • 説明: ここで指定したノードから Path2D を取得し、そのパスに沿って移動する。
  • @property speed: number
    • 役割: パスに沿った移動速度(単位: 単位長 / 秒)。
    • 説明: 正の値のみを想定。0 以下にすると停止扱いになる。
    • 例: 100 〜 400 くらいの値を想定。
  • @property startPosition: number
    • 役割: パス上の開始位置(0〜1 の正規化値)。
    • 説明: 0 でパスの始点、1 で終点。0.5 なら中間からスタート。
  • @property moveDirection: MoveDirection(enum)
    • 役割: パスを辿る向き。
    • 値:
      • Forward: Path2D の正方向(0 → 1)
      • Backward: 逆方向(1 → 0)
  • @property loopType: LoopType(enum)
    • 役割: パス端に到達したときの挙動。
    • 値:
      • Loop: 端に着いたら反対側にワープしてループ。
      • PingPong: 端で折り返して往復。
      • Once: 端に着いたら停止。
  • @property startDelay: number
    • 役割: 移動開始前の待機時間(秒)。
    • 説明: ゲーム開始直後に少し待ってから動かしたい場合に使用。
  • @property turnBackDelay: number
    • 役割: PingPong 時に、端で折り返す前の待機時間(秒)。
    • 説明: エレベーターが端で少し停止してから戻るような演出に使う。
  • @property playOnStart: boolean
    • 役割: start() 時に自動で移動を開始するか。
    • 説明: false にした場合、this.play() を別スクリプトから呼ぶ想定だが、本記事では「自動開始」を主用途とする。
  • @property useLocalPosition: boolean
    • 役割: パス座標をローカル座標として適用するか、ワールド座標として適用するか。
    • 説明:
      • true: pathNode のローカル空間上のパスを、pathNode の子として動かすようなイメージ。
      • false: ワールド座標で動かす。pathNode と離れた場所でも同じ軌跡を描く。

TypeScriptコードの実装

以下が完成した PathMover.ts の実装です。


import { _decorator, Component, Node, Vec3, warn, error } from 'cc';
import { Path2D } from 'cc';

const { ccclass, property } = _decorator;

/**
 * パス上の移動方向
 */
enum MoveDirection {
    Forward = 0,
    Backward = 1,
}

/**
 * 端に到達したときのループ方式
 */
enum LoopType {
    Loop = 0,
    PingPong = 1,
    Once = 2,
}

@ccclass('PathMover')
export class PathMover extends Component {

    @property({
        type: Node,
        tooltip: '移動に使用する Path2D コンポーネントを持つノードを指定します。',
    })
    public pathNode: Node | null = null;

    @property({
        tooltip: 'パスに沿った移動速度(単位: パスの長さ/秒)。0 以下で移動しません。',
    })
    public speed: number = 200;

    @property({
        tooltip: 'パス上の開始位置(0〜1)。0: 始点, 1: 終点。',
        min: 0,
        max: 1,
        step: 0.01,
        slide: true,
    })
    public startPosition: number = 0;

    @property({
        type: MoveDirection,
        tooltip: 'パスを辿る向き。Forward: 0→1, Backward: 1→0。',
    })
    public moveDirection: MoveDirection = MoveDirection.Forward;

    @property({
        type: LoopType,
        tooltip: 'パス端に到達したときの挙動。Loop: ループ, PingPong: 往復, Once: 一度だけで停止。',
    })
    public loopType: LoopType = LoopType.Loop;

    @property({
        tooltip: '移動開始前の待機時間(秒)。',
        min: 0,
    })
    public startDelay: number = 0;

    @property({
        tooltip: 'PingPong 時に端で折り返す前の待機時間(秒)。Loop, Once では無視されます。',
        min: 0,
    })
    public turnBackDelay: number = 0;

    @property({
        tooltip: 'true の場合、start() 時に自動で移動を開始します。',
    })
    public playOnStart: boolean = true;

    @property({
        tooltip: 'true: パス座標をローカル座標として適用 / false: ワールド座標として適用。',
    })
    public useLocalPosition: boolean = false;

    // 内部状態
    private _path2D: Path2D | null = null;
    private _pathLength: number = 0;
    private _normalizedPos: number = 0; // 0〜1
    private _isPlaying: boolean = false;
    private _currentDirection: number = 1; // 1 or -1
    private _delayTimer: number = 0; // 開始/折り返し待機用

    onLoad() {
        // Path2D の取得を試みる
        this._resolvePath2D();

        // 初期方向を設定
        this._currentDirection = (this.moveDirection === MoveDirection.Forward) ? 1 : -1;

        // 開始位置をクランプ
        this._normalizedPos = this._clamp01(this.startPosition);

        // 初期位置に配置
        this._updateNodePositionByPath();
    }

    start() {
        if (this.playOnStart) {
            this.play();
        }
    }

    update(deltaTime: number) {
        if (!this._isPlaying) {
            return;
        }

        if (!this._path2D || this._pathLength <= 0) {
            return;
        }

        // ディレイ中であればタイマーを減らす
        if (this._delayTimer > 0) {
            this._delayTimer -= deltaTime;
            return;
        }

        if (this.speed <= 0) {
            return;
        }

        // 正規化位置の更新
        const deltaNormalized = (this.speed * deltaTime) / this._pathLength;
        this._normalizedPos += deltaNormalized * this._currentDirection;

        // 端の処理
        this._handleBoundary();

        // 実際の位置を更新
        this._updateNodePositionByPath();
    }

    /**
     * 移動を開始 / 再開します。
     */
    public play() {
        if (!this._path2D) {
            this._resolvePath2D();
        }

        if (!this._path2D) {
            error('[PathMover] Path2D が見つからないため、再生できません。pathNode を確認してください。');
            return;
        }

        this._isPlaying = true;
        this._delayTimer = this.startDelay;
    }

    /**
     * 移動を一時停止します。
     */
    public pause() {
        this._isPlaying = false;
    }

    /**
     * パスの先頭に戻して停止します。
     */
    public stopAndReset() {
        this._isPlaying = false;
        this._normalizedPos = this._clamp01(this.startPosition);
        this._currentDirection = (this.moveDirection === MoveDirection.Forward) ? 1 : -1;
        this._delayTimer = 0;
        this._updateNodePositionByPath();
    }

    /**
     * Path2D コンポーネントを解決して内部に保持します。
     */
    private _resolvePath2D() {
        if (!this.pathNode) {
            warn('[PathMover] pathNode が設定されていません。インスペクタで Path2D を持つノードを指定してください。');
            this._path2D = null;
            this._pathLength = 0;
            return;
        }

        const path2D = this.pathNode.getComponent(Path2D);
        if (!path2D) {
            error('[PathMover] pathNode に Path2D コンポーネントが見つかりません。パスノードに Path2D を追加してください。');
            this._path2D = null;
            this._pathLength = 0;
            return;
        }

        this._path2D = path2D;
        this._pathLength = path2D.getLength();
        if (this._pathLength <= 0) {
            warn('[PathMover] Path2D の長さが 0 です。パスに十分なポイントが設定されているか確認してください。');
        }
    }

    /**
     * 0〜1 にクランプします。
     */
    private _clamp01(v: number): number {
        if (v < 0) return 0;
        if (v > 1) return 1;
        return v;
    }

    /**
     * パスの端に達したときの処理。
     */
    private _handleBoundary() {
        if (!this._path2D) {
            return;
        }

        if (this.loopType === LoopType.Loop) {
            // 0〜1 をループ
            if (this._normalizedPos > 1) {
                this._normalizedPos -= 1;
            } else if (this._normalizedPos < 0) {
                this._normalizedPos += 1;
            }
        } else if (this.loopType === LoopType.PingPong) {
            // 端で折り返し
            if (this._normalizedPos > 1) {
                this._normalizedPos = 1;
                this._currentDirection *= -1;
                if (this.turnBackDelay > 0) {
                    this._delayTimer = this.turnBackDelay;
                }
            } else if (this._normalizedPos < 0) {
                this._normalizedPos = 0;
                this._currentDirection *= -1;
                if (this.turnBackDelay > 0) {
                    this._delayTimer = this.turnBackDelay;
                }
            }
        } else if (this.loopType === LoopType.Once) {
            // 一度だけで停止
            if (this._normalizedPos >= 1) {
                this._normalizedPos = 1;
                this._isPlaying = false;
            } else if (this._normalizedPos <= 0) {
                this._normalizedPos = 0;
                this._isPlaying = false;
            }
        }
    }

    /**
     * 現在の _normalizedPos に基づいてノード位置を更新します。
     */
    private _updateNodePositionByPath() {
        if (!this._path2D || this._pathLength <= 0) {
            return;
        }

        const pos = this._path2D.getPositionAt(this._normalizedPos);
        if (!pos) {
            return;
        }

        if (this.useLocalPosition) {
            // ローカル座標で適用
            this.node.setPosition(pos);
        } else {
            // ワールド座標で適用
            // Path2D が返す座標は pathNode のローカル座標系なので、
            // pathNode のワールド位置に変換してから適用
            const worldPos = new Vec3();
            this.pathNode!.getWorldMatrix().transformPoint(worldPos, pos);
            this.node.worldPosition = worldPos;
        }
    }
}

主要な処理の解説

  • onLoad()
    • _resolvePath2D()pathNode から Path2D を取得。
    • moveDirection から _currentDirection(1 or -1)を決定。
    • startPosition を 0〜1 にクランプして _normalizedPos に設定。
    • _updateNodePositionByPath() で初期位置に配置。
  • start()
    • playOnStart が true の場合、自動的に play() を呼び出して移動開始。
  • update(deltaTime)
    • _isPlaying が false、または Path2D が無効な場合は何もしない。
    • _delayTimer が 0 より大きい場合はディレイ中としてタイマーを減らし、0 になるまで移動しない。
    • speeddeltaTime、パス長から deltaNormalized を計算し、_currentDirection(1 or -1)を掛けて _normalizedPos を更新。
    • _handleBoundary() で 0〜1 の範囲外に出たときの処理(ループ/往復/停止)を行う。
    • 最後に _updateNodePositionByPath() で実際の座標を更新。
  • _updateNodePositionByPath()
    • Path2D.getPositionAt(normalizedPos) でパス上の位置を取得。
    • useLocalPosition が true の場合は、そのまま this.node.setPosition() に適用。
    • false の場合は、pathNode のワールド行列を使ってワールド座標に変換し、this.node.worldPosition に適用。

使用手順と動作確認

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

  1. エディタ左下の Assets パネルで、スクリプトを置きたいフォルダ(例: assets/scripts)を選択します。
  2. フォルダ上で右クリック → CreateTypeScript を選択します。
  3. 新しく作成されたスクリプトに PathMover.ts という名前を付けます。
  4. ダブルクリックしてエディタ(VS Code など)で開き、先ほどの TypeScript コード全文 を貼り付けて保存します。

2. パス用ノード(Path2D)の作成

まずは Path2D コンポーネントを持つノードを作成します。

  1. Hierarchy パネルで右クリック → CreateEmpty Node を選択し、PathNode など分かりやすい名前に変更します。
  2. PathNode を選択した状態で、右側の Inspector パネル下部にある Add Component ボタンをクリックします。
  3. 検索欄に Path2D と入力し、表示された Path2D コンポーネントを追加します。
  4. Path2D の編集モードに入り、以下のようにパスを作成します(基本的な例):
    • Scene ビュー上部の Edit Path などのボタン(Path2D の編集)を押す。
    • Scene ビュー内でクリックしてポイントを追加し、直線やカーブを作る。
    • 例えば、X 方向に 0 → 300 → 600 の 3 点を置いて、水平に移動するパスを作る。
  5. パスの編集が終わったら、編集モードを終了します。

3. 移動させたいノードの作成

次に、実際にパスに沿って動かしたいノード(例: 動く床)を作ります。

  1. Hierarchy パネルで右クリック → Create2D ObjectSprite を選択し、名前を MovingPlatform とします。
  2. MovingPlatform を選択し、Inspector の Sprite コンポーネントで適当な画像(床画像など)を設定します。
  3. 初期位置はどこでも構いませんが、テストしやすいように PathNode の近くに配置しておくと良いです。

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

  1. Hierarchy で MovingPlatform を選択します。
  2. Inspector 下部の Add Component ボタンをクリックし、CustomPathMover を選択します。
    • Custom カテゴリに表示されない場合は、スクリプト名とクラス名(@ccclass('PathMover'))が一致しているか、コンパイルエラーが出ていないか確認してください。

5. PathMover のプロパティ設定

MovingPlatform にアタッチされた PathMover コンポーネントの各プロパティを設定します。

  • Path Node:
    • Hierarchy から PathNode をドラッグ&ドロップして、このフィールドにセットします。
  • Speed:
    • 例として 200300 あたりを入力してみてください。
    • 値を大きくすると速く移動します。
  • Start Position:
    • とりあえず 0 のままで OK(パスの始点からスタート)。
    • パスの中間から開始したい場合は 0.5 などに変更します。
  • Move Direction:
    • Forward: Path2D の定義通り(0 → 1)に移動。
    • Backward: 逆方向(1 → 0)に移動。
  • Loop Type:
    • 常にぐるぐる回したい場合は Loop
    • 行ったり来たりさせたい場合は PingPong
    • 一度だけ動かして止めたい場合は Once
  • Start Delay:
    • ゲーム開始からすぐ動かしたい場合は 0
    • 例えば 1.0 にすると、1 秒待ってから動き始めます。
  • Turn Back Delay:
    • LoopTypePingPong のときのみ有効。
    • 例えば 0.5 にすると、端に到達してから 0.5 秒止まって反対方向へ動き出します。
  • Play On Start:
    • 基本的には true のままで OK。
    • false にすると、他のスクリプトから getComponent(PathMover).play() を呼ぶまで動きません。
  • Use Local Position:
    • false(デフォルト):
      • パス座標をワールド座標に変換して適用するため、PathNode と離れた位置に置いても、同じ形の軌跡を描きます。
    • true:
      • パス座標をそのままローカル座標として適用するため、MovingPlatformPathNode の子にして使うケースなどに向いています。

6. ゲーム再生で動作確認

  1. 上部ツールバーの Play ボタン(▶)を押してゲームを再生します。
  2. Scene または Game ビューを見ながら、MovingPlatformPathNode の Path2D で定義したパスに沿って移動することを確認します。
  3. 速度や LoopType、StartPosition などを変えて再生し直し、挙動が変わることを試してみてください。

7. 動かないときのチェックポイント

  • Path Node が未設定:
    • Console に [PathMover] pathNode が設定されていません の警告が出ていないか確認。
    • Inspector で Path Node に正しく PathNode を指定してください。
  • Path2D が付いていない:
    • Console に pathNode に Path2D コンポーネントが見つかりません のエラーが出ていないか確認。
    • PathNode に Path2D コンポーネントが追加されているか確認し、必要なら再追加します。
  • パス長が 0:
    • Console に Path2D の長さが 0 です の警告が出ていないか確認。
    • Path2D に十分な数のポイントが配置されているか、パスが重なりすぎていないかを確認してください。
  • Speed が 0 以下:
    • speed が 0 になっていないか確認。
  • LoopType.Once で端に到達済み:
    • 一度端まで移動した後は _isPlaying が false になり停止します。
    • 再度動かしたい場合は stopAndReset()play() を呼ぶか、Play を押し直してシーンをリセットしてください。

まとめ

この PathMover コンポーネントを使うことで、

  • 動く床やエレベーター
  • パトロールする敵キャラ
  • 背景をぐるぐる回るオブジェクト
  • 演出用のカメラ移動(カメラノードにアタッチ)

といった「パスに沿った移動」を、スクリプト 1 つをノードにアタッチして Path2D を指定するだけで簡単に実現できます。

本コンポーネントは他のカスタムスクリプトに一切依存せず、インスペクタのプロパティだけで完結しているため、プロジェクト内のどのシーン・どのノードにもそのまま再利用できます。
パスの形状は 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をコピーしました!