【Cocos Creator 3.8】LadderClimber(梯子昇降)の実装:アタッチするだけで「梯子エリア内だけ上下移動+重力オフ」を実現する汎用スクリプト

2Dアクションゲームでよくある「梯子(はしご)を登る/降りる」挙動を、1つのコンポーネントをキャラクターノードにアタッチするだけで実現できるようにします。
このコンポーネントは、梯子エリアに入ったときだけ重力を無効化し、上下入力でY軸方向にのみ移動させます。
梯子エリア外では、通常どおり Rigidbody2D の重力・横移動などが働くため、既存のプレイヤー制御スクリプトとも共存しやすい設計です。


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

機能要件の整理

  • このコンポーネントはキャラクター側(登る側)にアタッチする。
  • 梯子は「Ladder」などの専用タグを持つコライダーとして用意し、Trigger(IsTrigger)で判定する。
  • キャラクターが梯子トリガーに重なっている間のみ、「梯子モード(climbing)」に入る。
  • 梯子モード中は:
    • Rigidbody2D の重力スケールを 0 にして落下しないようにする。
    • 上下入力(キーボードの Up/Down, W/S など)でY軸方向の速度のみを制御する。
    • 横方向の速度は 0 に固定(または元の速度を抑制)する。
  • 梯子トリガーから外れたら:
    • 重力スケールを元に戻す。
    • 梯子用のY方向速度制御をやめる。
  • 外部の GameManager や入力管理スクリプトには一切依存しない

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

  • Rigidbody2D コンポーネントのみ必須とし、同じノード上から getComponent で取得する。
  • 梯子判定は PhysicsSystem2D のコンタクトコールバックを使い、「梯子タグ(文字列)」で判定する。
  • 入力は Input.on で直接キーボードを監視し、このコンポーネント内だけで完結させる。
  • 元の重力スケールを記録しておき、梯子モード終了時に必ず元に戻す(防御的な実装)。

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

このコンポーネントは、以下のようなプロパティを Inspector から調整できるようにします。

  • climbSpeed: number
    • ツールチップ: 「梯子を登る/降りる速度(単位: m/s)」
    • 役割: 上下入力 1.0 に対する Rigidbody2D の Y 速度。
    • 例: 3.0 にすると、上キーで +3 m/s、下キーで -3 m/s。
  • ladderTag: string
    • ツールチップ: 「梯子コライダーに設定するタグ名」
    • 役割: このタグを持つ Collider2D の Trigger 内にいる間だけ梯子モードに入る。
    • 例: “Ladder”
  • useWASD: boolean
    • ツールチップ: 「W/S キーでも梯子の上下操作を行う」
    • 役割: true の場合、上下入力として W(上)/S(下)も受け付ける。
  • stopHorizontalOnLadder: boolean
    • ツールチップ: 「梯子中に横方向の速度を 0 にする」
    • 役割: true の場合、梯子モード中は Rigidbody2D の X 速度を 0 に固定して、完全に縦移動のみとする。
    • false にすると、他のスクリプトが付ける横移動を残すことも可能。
  • snapToLadderX: boolean
    • ツールチップ: 「梯子に入った瞬間、X 座標を梯子の中心に合わせる」
    • 役割: true の場合、梯子エリアに入ったときにキャラクターの X を梯子コライダーの中心 X にスナップして、見た目をきれいにする。
    • 2D アクションで多用される「梯子の真ん中に吸着」挙動。

※ いずれのプロパティも @property で公開されるため、プロジェクトごとに自由に調整できます。


TypeScriptコードの実装


import { _decorator, Component, Node, RigidBody2D, Vec2, Input, input, KeyCode, EventKeyboard, PhysicsSystem2D, Contact2DType, Collider2D, IPhysics2DContact, warn, director } from 'cc';
const { ccclass, property } = _decorator;

/**
 * LadderClimber
 * 
 * キャラクターノードにアタッチすると、
 * ・指定タグの「梯子」トリガー内にいる間だけ重力を 0 にし、
 * ・上下入力で Y 軸方向のみ移動させる汎用コンポーネント。
 */
@ccclass('LadderClimber')
export class LadderClimber extends Component {

    @property({
        tooltip: '梯子を登る/降りる速度(単位: m/s)。正の値で上、負の値で下方向に移動します。',
    })
    public climbSpeed: number = 3;

    @property({
        tooltip: '梯子コライダーに設定するタグ名。このタグを持つ Trigger 内にいる間だけ梯子モードになります。',
    })
    public ladderTag: string = 'Ladder';

    @property({
        tooltip: 'W/S キーでも梯子の上下操作を行うかどうか。',
    })
    public useWASD: boolean = true;

    @property({
        tooltip: 'true の場合、梯子中に Rigidbody2D の X 速度を常に 0 にして縦移動のみとします。',
    })
    public stopHorizontalOnLadder: boolean = true;

    @property({
        tooltip: 'true の場合、梯子に入った瞬間に X 座標を梯子コライダーの中心に合わせます。',
    })
    public snapToLadderX: boolean = true;

    private _rb2d: RigidBody2D | null = null;
    private _originalGravityScale: number = 1;
    private _isOnLadder: boolean = false;
    private _verticalInput: number = 0;  // -1 ~ 1

    // 現在重なっている梯子コライダー(複数重なりに備えて配列でもよいが、ここでは最後に入ったものを使用)
    private _currentLadder: Collider2D | null = null;

    onLoad() {
        // Rigidbody2D を取得
        this._rb2d = this.getComponent(RigidBody2D);
        if (!this._rb2d) {
            warn('[LadderClimber] このコンポーネントを使用するには、同じノードに RigidBody2D を追加してください。');
            return;
        }

        this._originalGravityScale = this._rb2d.gravityScale;

        // キーボード入力の監視を登録
        input.on(Input.EventType.KEY_DOWN, this._onKeyDown, this);
        input.on(Input.EventType.KEY_UP, this._onKeyUp, this);

        // 物理接触コールバックの登録
        const physics = PhysicsSystem2D.instance;
        physics.on(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this);
        physics.on(Contact2DType.END_CONTACT, this._onEndContact, this);
    }

    onDestroy() {
        // 入力イベントの解除
        input.off(Input.EventType.KEY_DOWN, this._onKeyDown, this);
        input.off(Input.EventType.KEY_UP, this._onKeyUp, this);

        // 物理接触コールバックの解除
        const physics = PhysicsSystem2D.instance;
        physics.off(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this);
        physics.off(Contact2DType.END_CONTACT, this._onEndContact, this);

        // 念のため、破棄時に重力を元に戻す
        if (this._rb2d) {
            this._rb2d.gravityScale = this._originalGravityScale;
        }
    }

    /**
     * キーが押されたときの処理
     */
    private _onKeyDown(event: EventKeyboard) {
        switch (event.keyCode) {
            case KeyCode.ARROW_UP:
                this._verticalInput = 1;
                break;
            case KeyCode.ARROW_DOWN:
                this._verticalInput = -1;
                break;
            case KeyCode.KEY_W:
                if (this.useWASD) {
                    this._verticalInput = 1;
                }
                break;
            case KeyCode.KEY_S:
                if (this.useWASD) {
                    this._verticalInput = -1;
                }
                break;
        }
    }

    /**
     * キーが離されたときの処理
     */
    private _onKeyUp(event: EventKeyboard) {
        switch (event.keyCode) {
            case KeyCode.ARROW_UP:
            case KeyCode.ARROW_DOWN:
                // 上下キーが離されたら 0 に戻す(W/S と同時押しのケースは簡易的に無視)
                this._verticalInput = 0;
                break;
            case KeyCode.KEY_W:
            case KeyCode.KEY_S:
                if (this.useWASD) {
                    this._verticalInput = 0;
                }
                break;
        }
    }

    /**
     * 2D 物理接触開始
     * 梯子タグの Trigger に入ったら梯子モードに入る。
     */
    private _onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
        // selfCollider がこのノードのコライダーでない場合は無視
        if (selfCollider.node !== this.node && otherCollider.node !== this.node) {
            return;
        }

        // どちらが「自分」かを判定
        let ladderCollider: Collider2D | null = null;
        if (selfCollider.node === this.node) {
            ladderCollider = otherCollider;
        } else {
            ladderCollider = selfCollider;
        }

        if (!ladderCollider) {
            return;
        }

        // Trigger でなければ梯子とはみなさない
        if (!ladderCollider.isTrigger) {
            return;
        }

        // タグで梯子かどうか判定
        if (ladderCollider.tag !== this.ladderTag) {
            return;
        }

        // 梯子モード開始
        this._enterLadder(ladderCollider);
    }

    /**
     * 2D 物理接触終了
     * 梯子タグの Trigger から出たら梯子モードを終了する。
     */
    private _onEndContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
        if (!this._isOnLadder) {
            return;
        }

        // selfCollider がこのノードのコライダーでない場合は無視
        if (selfCollider.node !== this.node && otherCollider.node !== this.node) {
            return;
        }

        let ladderCollider: Collider2D | null = null;
        if (selfCollider.node === this.node) {
            ladderCollider = otherCollider;
        } else {
            ladderCollider = selfCollider;
        }

        if (!ladderCollider) {
            return;
        }

        if (ladderCollider.tag !== this.ladderTag) {
            return;
        }

        // 現在の梯子から離れた場合のみ終了
        if (this._currentLadder === ladderCollider) {
            this._exitLadder();
        }
    }

    /**
     * 梯子モードに入る処理
     */
    private _enterLadder(ladderCollider: Collider2D) {
        if (!this._rb2d) {
            return;
        }

        this._isOnLadder = true;
        this._currentLadder = ladderCollider;

        // 重力を 0 にして落下しないようにする
        this._originalGravityScale = this._rb2d.gravityScale;
        this._rb2d.gravityScale = 0;

        // 現在の速度を取得して、Y 方向は一旦 0 にする
        const v = this._rb2d.linearVelocity;
        this._rb2d.linearVelocity = new Vec2(this.stopHorizontalOnLadder ? 0 : v.x, 0);

        // X 座標を梯子の中心にスナップ(オプション)
        if (this.snapToLadderX) {
            const ladderWorldPos = ladderCollider.node.worldPosition;
            const myWorldPos = this.node.worldPosition;
            // X のみ合わせる
            this.node.setWorldPosition(ladderWorldPos.x, myWorldPos.y, myWorldPos.z);
        }
    }

    /**
     * 梯子モードから出る処理
     */
    private _exitLadder() {
        if (!this._rb2d) {
            return;
        }

        this._isOnLadder = false;
        this._currentLadder = null;
        this._verticalInput = 0;

        // 重力スケールを元に戻す
        this._rb2d.gravityScale = this._originalGravityScale;
    }

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

        if (!this._isOnLadder) {
            return;
        }

        // 上下入力に応じて Y 速度を設定
        const v = this._rb2d.linearVelocity;
        const targetY = this._verticalInput * this.climbSpeed;
        const targetX = this.stopHorizontalOnLadder ? 0 : v.x;

        this._rb2d.linearVelocity = new Vec2(targetX, targetY);
    }
}

コードの要点解説

  • onLoad
    • 同じノードから RigidBody2D を取得し、なければ warn を出して終了(防御的)。
    • Input.on でキーボード入力(上下キー + 任意で W/S)を監視。
    • PhysicsSystem2D に対して BEGIN_CONTACT / END_CONTACT を登録し、梯子トリガーへの侵入/離脱を検出。
  • _onBeginContact / _onEndContact
    • 接触したコライダーのうち、自分のノードでない方を「相手」として扱う。
    • その相手が Trigger かつ、指定タグ(ladderTag)を持つ場合にのみ梯子として扱う。
    • 侵入時は _enterLadder、離脱時は _exitLadder を呼ぶ。
  • _enterLadder
    • 現在の重力スケールを保存し、gravityScale = 0 に設定。
    • 現在の速度から Y を 0 にし、オプションで X も 0 にする(stopHorizontalOnLadder)。
    • snapToLadderX が true の場合、梯子コライダーのノード中心 X にキャラクターの X を合わせる。
  • _exitLadder
    • 梯子モードフラグを false にし、入力値をリセット。
    • 保存しておいた _originalGravityScaleRigidBody2D.gravityScale に戻す。
  • update
    • 梯子モード中のみ動作。
    • _verticalInput(-1 ~ 1)に climbSpeed を掛けた値を Y 速度として設定。
    • 横方向は stopHorizontalOnLadder に応じて 0 または既存の X 速度を維持。

使用手順と動作確認

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

  1. Assets パネルで右クリック → Create → TypeScript を選択し、ファイル名を LadderClimber.ts にします。
  2. 自動生成されたコードをすべて削除し、本記事の LadderClimber クラスのコードをそのまま貼り付けて保存します。

2. プレイヤーノード(キャラクター)の準備

  1. Hierarchy パネルで、プレイヤー用のノード(例: Player)を用意します。
    • 2D スプライトの場合: 右クリック → Create → 2D Object → Sprite などで作成。
  2. Player ノードを選択し、Inspector で以下を追加します:
    • Add Component → Physics 2D → RigidBody2D
    • Add Component → Physics 2D → Collider2D(BoxCollider2D など)
  3. Collider2D の Is TriggerOFF にしておきます(プレイヤーは通常の物理挙動をするため)。
  4. RigidBody2D の Body TypeDynamic を推奨します。

3. LadderClimber コンポーネントのアタッチ

  1. 再び Player ノードを選択し、Inspector で:
    • Add Component → Custom → LadderClimber を選択します。
  2. LadderClimber のプロパティを設定します:
    • Climb Speed: 例として 3 と入力。
    • Ladder Tag: デフォルトの Ladder のままで構いません。
    • Use WASD: W/S でも操作したい場合は ON
    • Stop Horizontal On Ladder: 梯子中は横移動を完全に止めたい場合は ON(推奨)。
    • Snap To Ladder X: 梯子の中心に吸着させたい場合は ON(推奨)。

4. 梯子ノードの作成

梯子側は「Trigger の Collider2D + 指定タグ」で表現します。

  1. Hierarchy パネルで右クリック → Create → 2D Object → Node を選択し、名前を Ladder にします。
  2. Ladder ノードを選択し、Inspector で:
    • Add Component → Physics 2D → BoxCollider2D を追加します。
  3. BoxCollider2D の設定:
    • Is Trigger にチェックを入れて ON にします。
    • Size を調整して、プレイヤーが重なれる縦長のエリアにします(例: Width=0.5, Height=4)。
    • Tag フィールドに Ladder と入力します(LadderClimber の ladderTag と一致させる)。
  4. 見た目のために、Ladder ノードにスプライトを付けたい場合:
    • Add Component → Render → Sprite を追加し、梯子の画像を割り当てます。
    • Sprite のサイズと BoxCollider2D のサイズを合わせると分かりやすいです。

5. 物理 2D の有効化確認

Cocos Creator 3.8 プロジェクトで Physics2D を使うには、Project Settings で 2D 物理が有効になっている必要があります。

  1. メニューから Project → Project Settings を開きます。
  2. Feature Cropping または Module(バージョンによって名称が異なる場合あり)で、Physics 2D が有効になっていることを確認します。

6. シーンでの配置とテスト

  1. Scene ビューで、Ladder ノードを Player の近くに配置し、プレイヤーがジャンプや移動で重なれる位置に調整します。
  2. 再生ボタン(▶)でゲームをプレビューします。
  3. プレイヤーを梯子の下あたりに移動させ(既存の移動スクリプトや仮の操作でOK)、Ladder の Trigger エリアに入るようにします。
  4. プレイヤーが梯子エリアに入った状態で:
    • 上キー(↑)または W を押す → プレイヤーが 上方向に一定速度で移動する。
    • 下キー(↓)または S を押す → プレイヤーが 下方向に一定速度で移動する。
    • キーを離す → プレイヤーがその場で停止し、重力で落下しない(梯子モード中は gravityScale=0)。
  5. プレイヤーが梯子エリアから完全に出ると:
    • 重力スケールが 元の値 に戻り、通常の落下挙動に復帰します。
    • 横移動スクリプトがある場合も、再び通常どおり動作します。

7. よくあるつまずきポイント

  • プレイヤーが梯子に入っても何も起きない場合:
    • Player に RigidBody2D が付いているか確認。
    • Ladder の BoxCollider2D が Is Trigger=ON になっているか確認。
    • Ladder の Tag が LadderClimber の ladderTag と一致しているか確認(例: “Ladder”)。
    • Project Settings で Physics 2D が有効になっているか確認。
  • 梯子中に横移動してしまう場合:
    • LadderClimber の Stop Horizontal On Ladder が ON になっているか確認。
  • 梯子の真ん中に吸着しない場合:
    • LadderClimber の Snap To Ladder X が ON になっているか確認。
    • Ladder ノードの位置(Transform)が想定どおりか確認。

まとめ

この LadderClimber コンポーネントは、キャラクター側にアタッチするだけで、梯子エリア内の重力オフ+上下移動を完結させる汎用スクリプトです。
外部の GameManager や入力管理クラスに依存せず、必要な設定はすべて Inspector のプロパティから行えるため、どのプロジェクトにもそのまま持ち込んで再利用できます。

応用例として:

  • 梯子だけでなく、「ロープ」「ツタ」「エレベーターシャフト」など、縦方向専用の移動エリアに名前だけ変えて使う。
  • climbSpeed を状況に応じて変更し、「アイテム取得で梯子移動速度アップ」などの演出に使う。
  • ladderTag を複数用意して、「通常梯子」「高速梯子」「水中梯子」などをタグで切り替える。

ゲーム内の「登る/降りる」処理をこの 1 コンポーネントに閉じ込めておくことで、プレイヤー制御のメインスクリプトをシンプルに保ちながら、拡張性の高い梯子システムを構築できます。
そのままコピペで導入し、必要に応じてプロパティを調整しながら、自分のゲームに合った梯子挙動を作り込んでみてください。