【Cocos Creator 3.8】WallJump の実装:アタッチするだけで「壁に張り付いてから反対方向へジャンプ」できる汎用スクリプト

このガイドでは、2Dアクションゲームでよくある「壁に張り付いている状態から、反対側へジャンプする」挙動を、1つのコンポーネントをアタッチするだけで実現できる WallJump を実装します。

プレイヤーキャラのノードにこのコンポーネントを付けて、インスペクタからいくつかのパラメータを調整するだけで、壁ジャンプ・壁スライド・接地判定まで含めた挙動を実現できるように設計します。


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

想定する利用シーン

  • 2D横スクロールアクションゲーム(プラットフォーマー)
  • プレイヤーキャラが左右移動・ジャンプでき、壁に接触したときに「張り付き」状態になり、そこから反対方向へジャンプさせたい
  • 壁に張り付いている間は、緩やかに落下(壁スライド)させたい

前提と依存コンポーネント

この WallJump は以下の標準コンポーネントに依存しますが、外部のカスタムスクリプトには一切依存しません

  • RigidBody2D(必須:物理挙動と速度制御)
  • Collider2D(必須:壁・床との接触判定)

コード内で getComponent により取得を試み、見つからなければ error ログを出して動作を抑制します。
また、ジャンプボタンや左右入力はキーボードで完結させ、外部の入力管理スクリプトにも依存しません。

実装する主な機能

  • 左右移動(A/D または ←/→ キー)
  • 通常ジャンプ(接地時に Space キー)
  • 壁張り付き判定(左右の壁に接触していて空中の場合)
  • 壁スライド(張り付き中は落下速度を制限)
  • 壁ジャンプ(張り付き中にジャンプボタンで、反対方向へジャンプ)

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

以下のように、すべてインスペクタから調整できるようにします。

  • moveSpeed: number
    • 左右移動速度(単位:m/s 相当)。
    • 例:5〜10 くらいで調整。
  • jumpVelocity: number
    • 通常ジャンプ時の上方向速度。
    • 例:8〜15。
  • wallJumpHorizontalVelocity: number
    • 壁ジャンプ時の「壁と反対方向」への水平方向速度。
    • 例:6〜12。
  • wallJumpVerticalVelocity: number
    • 壁ジャンプ時の上方向速度。
    • 例:8〜15。
  • maxFallSpeed: number
    • 通常落下時の最大落下速度(下方向の速度制限)。
    • 例:-20 など、負の値で指定。
  • wallSlideSpeed: number
    • 壁に張り付いているときの最大落下速度。
    • 通常の maxFallSpeed よりも小さい(ゆっくり落ちる)値を推奨。
    • 例:-3 〜 -5。
  • groundCheckDistance: number
    • 接地判定に使うレイキャストの距離(キャラの足元からの下方向距離)。
    • 例:0.1〜0.3。
  • wallCheckDistance: number
    • 壁判定に使うレイキャストの距離(キャラ中心から左右方向)。
    • 例:0.1〜0.3。
  • groundLayerMask: number
    • 「床」として扱うレイヤーのビットマスク。
    • Physics2D の衝突レイヤー設定に応じて指定。
  • wallLayerMask: number
    • 「壁」として扱うレイヤーのビットマスク。
    • groundLayerMask と同じでもよいし、壁専用レイヤーを使ってもよい。
  • coyoteTime: number
    • いわゆる「コヨーテタイム」。地面から少し離れても、指定秒数以内ならジャンプを許可する猶予時間。
    • 例:0.1〜0.2。
  • jumpBufferTime: number
    • ジャンプボタン先行入力の猶予時間(着地前に押しても、指定秒数以内に着地したらジャンプする)。
    • 例:0.1〜0.2。
  • enableDebugDraw: boolean
    • レイキャスト位置などをログ出力するかどうかの簡易デバッグフラグ。

このように、物理挙動・操作感をすべてインスペクタから調整できるようにしておくことで、ゲームごとのチューニングがしやすくなります。


TypeScriptコードの実装

以下が完成版の WallJump.ts です。


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

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

    @property({
        tooltip: '左右移動速度(m/s 相当)。正の値で指定します。'
    })
    public moveSpeed: number = 7;

    @property({
        tooltip: '通常ジャンプ時の上方向速度。正の値で指定します。'
    })
    public jumpVelocity: number = 12;

    @property({
        tooltip: '壁ジャンプ時の水平方向速度(壁と反対方向へ加える速度)。正の値で指定します。'
    })
    public wallJumpHorizontalVelocity: number = 10;

    @property({
        tooltip: '壁ジャンプ時の上方向速度。正の値で指定します。'
    })
    public wallJumpVerticalVelocity: number = 12;

    @property({
        tooltip: '通常落下時の最大落下速度(下方向)。負の値で指定します。'
    })
    public maxFallSpeed: number = -20;

    @property({
        tooltip: '壁に張り付いているときの最大落下速度(ゆっくり落ちる)。負の値で指定します。'
    })
    public wallSlideSpeed: number = -4;

    @property({
        tooltip: '接地判定に使うレイキャストの距離(下方向)。'
    })
    public groundCheckDistance: number = 0.2;

    @property({
        tooltip: '壁判定に使うレイキャストの距離(左右方向)。'
    })
    public wallCheckDistance: number = 0.2;

    @property({
        tooltip: '床として扱うレイヤーのビットマスク(Physics2D のレイヤー設定に合わせてください)。'
    })
    public groundLayerMask: number = 1 << 1; // 例: レイヤー1を地面とする

    @property({
        tooltip: '壁として扱うレイヤーのビットマスク(Physics2D のレイヤー設定に合わせてください)。'
    })
    public wallLayerMask: number = 1 << 1; // 例: 同じレイヤーを壁としてもよい

    @property({
        tooltip: '地面から離れても、この秒数以内ならジャンプを許可するコヨーテタイム。'
    })
    public coyoteTime: number = 0.15;

    @property({
        tooltip: 'ジャンプボタン先行入力の猶予時間(この秒数以内に着地したらジャンプが発動)。'
    })
    public jumpBufferTime: number = 0.15;

    @property({
        tooltip: 'デバッグ用ログを有効にします。'
    })
    public enableDebugDraw: boolean = false;

    // 内部状態
    private _rigidBody: RigidBody2D | null = null;
    private _collider: Collider2D | null = null;

    private _moveDir: number = 0; // -1: 左, 1: 右, 0: なし
    private _jumpPressed: boolean = false;

    private _isGrounded: boolean = false;
    private _isOnLeftWall: boolean = false;
    private _isOnRightWall: boolean = false;

    private _lastGroundedTime: number = -999;
    private _lastJumpPressedTime: number = -999;

    private _isWallJumping: boolean = false;
    private _wallJumpDirection: number = 0; // -1: 左へ跳ぶ, 1: 右へ跳ぶ

    onLoad() {
        this._rigidBody = this.getComponent(RigidBody2D);
        if (!this._rigidBody) {
            error('[WallJump] RigidBody2D が見つかりません。このコンポーネントを使用するノードに RigidBody2D を追加してください。');
        }

        this._collider = this.getComponent(Collider2D);
        if (!this._collider) {
            error('[WallJump] Collider2D が見つかりません。このコンポーネントを使用するノードに Collider2D(BoxCollider2D 等)を追加してください。');
        }

        // 入力イベント登録
        input.on(Input.EventType.KEY_DOWN, this._onKeyDown, this);
        input.on(Input.EventType.KEY_UP, this._onKeyUp, this);
    }

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

    start() {
        // 物理システムが有効か確認
        if (!PhysicsSystem2D.instance) {
            error('[WallJump] PhysicsSystem2D が有効ではありません。Project Settings > Physics 2D を確認してください。');
        }
    }

    update(deltaTime: number) {
        const now = director.getTotalTime() / 1000; // 秒に変換

        // 接地・壁判定を更新
        this._updateGroundAndWallState();

        // 接地していればコヨーテタイム更新
        if (this._isGrounded) {
            this._lastGroundedTime = now;
            this._isWallJumping = false; // 地面に着いたら壁ジャンプ状態解除
        }

        // ジャンプ入力があれば時刻を記録
        if (this._jumpPressed) {
            this._lastJumpPressedTime = now;
            this._jumpPressed = false; // 1フレームで消費
        }

        // 実際の挙動更新
        this._handleMovement(deltaTime, now);
    }

    // キー入力処理
    private _onKeyDown(event: EventKeyboard) {
        switch (event.keyCode) {
            case KeyCode.KEY_A:
            case KeyCode.ARROW_LEFT:
                this._moveDir = -1;
                break;
            case KeyCode.KEY_D:
            case KeyCode.ARROW_RIGHT:
                this._moveDir = 1;
                break;
            case KeyCode.SPACE:
                this._jumpPressed = true;
                break;
        }
    }

    private _onKeyUp(event: EventKeyboard) {
        switch (event.keyCode) {
            case KeyCode.KEY_A:
            case KeyCode.ARROW_LEFT:
                if (this._moveDir === -1) {
                    this._moveDir = 0;
                }
                break;
            case KeyCode.KEY_D:
            case KeyCode.ARROW_RIGHT:
                if (this._moveDir === 1) {
                    this._moveDir = 0;
                }
                break;
        }
    }

    /**
     * 接地・壁判定をレイキャストで更新
     */
    private _updateGroundAndWallState() {
        const physics = PhysicsSystem2D.instance;
        if (!physics) {
            return;
        }

        const worldPos = this.node.worldPosition;
        const origin = new Vec2(worldPos.x, worldPos.y);

        // --- 地面判定(下方向) ---
        const groundStart = new Vec2(origin.x, origin.y);
        const groundEnd = new Vec2(origin.x, origin.y - this.groundCheckDistance);

        let isGrounded = false;
        const groundResults = physics.raycast(groundStart, groundEnd, ERaycast2DType.Closest, this.groundLayerMask);
        if (groundResults.length > 0) {
            isGrounded = true;
        }

        // --- 壁判定(左右) ---
        const leftStart = new Vec2(origin.x, origin.y);
        const leftEnd = new Vec2(origin.x - this.wallCheckDistance, origin.y);
        const rightStart = new Vec2(origin.x, origin.y);
        const rightEnd = new Vec2(origin.x + this.wallCheckDistance, origin.y);

        let onLeftWall = false;
        let onRightWall = false;

        const leftHits = physics.raycast(leftStart, leftEnd, ERaycast2DType.Closest, this.wallLayerMask);
        if (leftHits.length > 0) {
            onLeftWall = true;
        }

        const rightHits = physics.raycast(rightStart, rightEnd, ERaycast2DType.Closest, this.wallLayerMask);
        if (rightHits.length > 0) {
            onRightWall = true;
        }

        this._isGrounded = isGrounded;
        this._isOnLeftWall = onLeftWall && !isGrounded;  // 地面にいるときは壁張り付きとはみなさない
        this._isOnRightWall = onRightWall && !isGrounded;

        if (this.enableDebugDraw) {
            log(`[WallJump] grounded=${this._isGrounded}, leftWall=${this._isOnLeftWall}, rightWall=${this._isOnRightWall}`);
        }
    }

    /**
     * 水平方向移動・ジャンプ・壁ジャンプ・落下制御
     */
    private _handleMovement(deltaTime: number, now: number) {
        if (!this._rigidBody) {
            return;
        }

        const v = this._rigidBody.linearVelocity;

        // --- 水平方向移動 ---
        let targetVX = this._moveDir * this.moveSpeed;

        // 壁ジャンプ直後は、少しの間プレイヤー入力を無視して壁ジャンプ方向を優先してもよいが、
        // シンプルにするため今回は常に入力を反映させる。

        // --- ジャンプ処理 ---
        const canUseCoyote = (now - this._lastGroundedTime) <= this.coyoteTime;
        const bufferedJump = (now - this._lastJumpPressedTime) <= this.jumpBufferTime;

        if (bufferedJump) {
            // 壁ジャンプ優先
            if (this._isOnLeftWall || this._isOnRightWall) {
                this._performWallJump();
                this._lastJumpPressedTime = -999; // 消費
            } else if (canUseCoyote) {
                // 通常ジャンプ
                v.y = this.jumpVelocity;
                this._rigidBody.linearVelocity = v;
                this._lastJumpPressedTime = -999; // 消費
            }
        }

        // 壁ジャンプ中のフラグは _performWallJump 内で設定される

        // --- 落下制御 ---
        // 壁に張り付いている間は落下速度を制限(壁スライド)
        if ((this._isOnLeftWall || this._isOnRightWall) && v.y < this.wallSlideSpeed) {
            v.y = this.wallSlideSpeed;
        }

        // 通常の最大落下速度制限
        if (v.y < this.maxFallSpeed) {
            v.y = this.maxFallSpeed;
        }

        // 最終的な速度を適用
        v.x = targetVX;
        this._rigidBody.linearVelocity = v;
    }

    /**
     * 壁ジャンプ実行
     */
    private _performWallJump() {
        if (!this._rigidBody) {
            return;
        }

        const v = this._rigidBody.linearVelocity;

        // 左壁に張り付いているときは右へ、右壁に張り付いているときは左へ跳ぶ
        if (this._isOnLeftWall) {
            this._wallJumpDirection = 1;
        } else if (this._isOnRightWall) {
            this._wallJumpDirection = -1;
        } else {
            // 壁にいない場合は何もしない
            return;
        }

        v.x = this._wallJumpDirection * this.wallJumpHorizontalVelocity;
        v.y = this.wallJumpVerticalVelocity;

        this._rigidBody.linearVelocity = v;

        this._isWallJumping = true;

        if (this.enableDebugDraw) {
            log(`[WallJump] Perform wall jump dir=${this._wallJumpDirection}, vx=${v.x}, vy=${v.y}`);
        }
    }
}

コードのポイント解説

  • onLoad
    • RigidBody2DCollider2D を取得し、存在しない場合は error ログを出します。
    • キーボード入力イベント(左右移動・ジャンプ)を登録します。
  • update
    • _updateGroundAndWallState でレイキャストによる接地・壁判定を更新。
    • 接地中は _lastGroundedTime を更新し、コヨーテタイムを実現。
    • ジャンプ入力があったフレームで _lastJumpPressedTime を更新し、ジャンプ先行入力(バッファ)を実現。
    • _handleMovement で実際の速度制御・ジャンプ・壁ジャンプ・落下制限を行います。
  • _updateGroundAndWallState
    • 足元から下方向にレイキャストして groundLayerMask に当たれば接地と判定。
    • キャラ中心から左右にレイキャストして wallLayerMask に当たれば壁と判定。
    • 地面にいるときは壁張り付き状態にはしないようにしています。
  • _handleMovement
    • 左右入力に応じて moveSpeed を用いた水平方向速度を設定。
    • コヨーテタイムとジャンプバッファを組み合わせて、自然なジャンプ操作感を実現。
    • 壁に張り付いている間は wallSlideSpeed を上限として落下速度を制限(壁スライド)。
    • さらに maxFallSpeed で通常の落下速度の下限も制限。
  • _performWallJump
    • 左壁に張り付いていれば右方向、右壁なら左方向に wallJumpHorizontalVelocity をセット。
    • 縦方向には wallJumpVerticalVelocity をセットし、上方向へジャンプ。
    • _isWallJumping フラグは拡張用(空中制御制限など)として用意しています。

使用手順と動作確認

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

  1. Assets パネルで右クリック → Create → TypeScript を選択します。
  2. ファイル名を WallJump.ts にします。
  3. 自動生成された中身をすべて削除し、本記事の 「TypeScriptコードの実装」 セクションのコードを丸ごと貼り付けて保存します。

2. プレイヤーノードの準備

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite などを選択し、プレイヤー用ノードを作成します(名前例:Player)。
  2. Player ノードを選択し、Inspector で以下のコンポーネントを追加します。
    • Add Component → Physics 2D → RigidBody2D
      • Type: Dynamic
      • Gravity Scale: 1(必要に応じて調整)
    • Add Component → Physics 2D → BoxCollider2D など
      • キャラの当たり判定に合うようにサイズを調整してください。

※ もし RigidBody2DCollider2D を付け忘れていると、実行時にコンソールにエラーが表示され、WallJump は動作しません。

3. WallJump コンポーネントをアタッチ

  1. Player ノードを選択した状態で、Inspector の Add Component ボタンをクリックします。
  2. Custom → WallJump を選択して追加します。

4. Physics2D レイヤー設定(床・壁のレイヤー)

  1. メニューから Project → Project Settings を開きます。
  2. 左のリストから Physics 2D を選択します。
  3. 「Layer」や「Collision Matrix」を確認し、床・壁に使うレイヤーを決めてください。例えば:
    • Layer 0: Default(使用しない)
    • Layer 1: GroundAndWall(床と壁用)
  4. 床や壁となるノード(Tilemap や Sprite など)の Collider2D コンポーネントで、Group / Layer を上記の「GroundAndWall」に設定します。

その上で、Player ノードの WallJump コンポーネントのインスペクタで:

  • groundLayerMask = 1 << 1(Layer 1 を床として扱う)
  • wallLayerMask = 1 << 1(同じく Layer 1 を壁として扱う)

と設定しておくと、Layer 1 のコライダーが床・壁の両方として判定されます。床と壁を分けたい場合は、それぞれ別のレイヤーを割り当て、groundLayerMask / wallLayerMask を変更してください。

5. WallJump プロパティの設定例

Inspector で WallJump コンポーネントを選択し、次のように設定してみてください。

  • moveSpeed: 7
  • jumpVelocity: 12
  • wallJumpHorizontalVelocity: 10
  • wallJumpVerticalVelocity: 12
  • maxFallSpeed: -20
  • wallSlideSpeed: -4
  • groundCheckDistance: 0.2
  • wallCheckDistance: 0.2
  • groundLayerMask: 1 << 1
  • wallLayerMask: 1 << 1
  • coyoteTime: 0.15
  • jumpBufferTime: 0.15
  • enableDebugDraw: 必要に応じて ON

6. シーンの簡単なテスト環境を作る

  1. Hierarchy で右クリック → Create → 2D Object → Sprite を選択し、床用のノード(例:Ground)を作成します。
  2. Ground ノードに BoxCollider2D を追加し、横長の床になるようにサイズを調整します。
  3. Ground の Collider2D の Layer / Group を、先ほど決めた床・壁用レイヤー(例:Layer 1)に設定します。
  4. 同様に、左右の壁用ノード(例:LeftWall, RightWall)を作成し、縦長の BoxCollider2D を追加してレイヤーも設定します。
  5. Player ノードを床の上に配置し、壁の近くに移動させます。

7. 実行して挙動を確認

  1. 上部の Play ボタン(▶)でプレビューを開始します。
  2. 以下のキー操作で挙動を確認します。
    • A / ←: 左に移動
    • D / →: 右に移動
    • Space: ジャンプ
  3. プレイヤーを壁に向かってジャンプさせ、空中で壁に接触させると、ゆっくり落ちる(壁スライド)ことを確認します。
  4. 壁に張り付いている状態で Space を押すと、壁と反対方向へジャンプすることを確認します。

もしうまく動かない場合は、以下を確認してください。

  • PlayerRigidBody2DCollider2D が追加されているか。
  • 床・壁ノードの Collider2D のレイヤーが、groundLayerMask / wallLayerMask と一致しているか。
  • レイキャスト距離(groundCheckDistance / wallCheckDistance)が小さすぎず、大きすぎないか。
  • Console に [WallJump] からのエラーログが出ていないか。

まとめ

この WallJump コンポーネントは、

  • プレイヤーノードにアタッチ
  • RigidBody2DCollider2D を追加
  • インスペクタで速度・レイヤー・距離を設定

という手順だけで、

  • 左右移動
  • 通常ジャンプ + コヨーテタイム / ジャンプバッファ
  • 壁張り付き判定
  • 壁スライド
  • 壁ジャンプ(反対方向へ跳ぶ)

までを一括で実現できる、完全に独立した汎用コンポーネントです。

ゲームごとに挙動を変えたい場合も、すべてインスペクタのプロパティで調整できるため、コードを書き換える必要はありません。例えば、

  • よりスピーディなアクションにしたい → moveSpeed, wallJumpHorizontalVelocity, wallJumpVerticalVelocity を大きめに
  • 初心者向けに優しくしたい → coyoteTimejumpBufferTime を少し長めに
  • 壁スライドの落下速度をもっと遅くしたい → wallSlideSpeed を 0 に近づける(例:-2)

といった形で、ゲーム性に合わせたチューニングが可能です。

このコンポーネントをベースに、

  • 空中ダッシュ
  • 二段ジャンプ
  • 壁キックからの連続ジャンプ

などを追加していけば、よりリッチなアクションゲームの土台として再利用していくことができます。