【Cocos Creator 3.8】DoubleJump の実装:アタッチするだけで「2段ジャンプ+パーティクル演出」を実現する汎用スクリプト

このガイドでは、任意のキャラノードにアタッチするだけで「空中で1回だけ追加ジャンプ(2段ジャンプ)」ができるようになる DoubleJump コンポーネントを実装します。
キーボード入力(デフォルトは Space / W / ↑)でジャンプし、着地までにもう一度だけジャンプ可能。2段目ジャンプ時には任意のパーティクル演出も再生できます。

物理挙動は RigidBody2D を利用し、ジャンプ力や入力キー、パーティクルなどはすべてインスペクタから調整可能です。他のスクリプトやシングルトンには一切依存しない、単体で完結する汎用コンポーネントとして設計します。


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

機能要件の整理

  • 1回目ジャンプ:地面に接地しているときのみ実行可能。
  • 2回目ジャンプ:空中にいる間に、1回だけ追加ジャンプ可能。
  • 3回目以降:着地するまでジャンプ不可。
  • 着地判定:Y方向速度と「地面判定ライン」を用いて、簡易的に接地状態を判断。
  • 入力:キーボードの指定キーを押した瞬間にジャンプ。
  • 物理:RigidBody2D の速度ベクトルを直接書き換えて上方向へ跳ねさせる。
  • 演出:2段目ジャンプ時に任意のパーティクルシステムを再生(なくても動作する)。

重要な点は「外部依存をなくすこと」です。GameManager などの外部スクリプトや、特定のステートマシンには一切依存しません。必要な情報はすべて以下のような @property から受け取ります。

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

  • jumpKeySpace: boolean
    tooltip: “Space キーでジャンプを許可するか”
    – true の場合、Space キー押下でジャンプ入力を受け付けます。
  • jumpKeyUpArrow: boolean
    tooltip: “↑ キーでジャンプを許可するか”
    – true の場合、↑ キー押下でジャンプ入力を受け付けます。
  • jumpKeyW: boolean
    tooltip: “W キーでジャンプを許可するか”
    – true の場合、W キー押下でジャンプ入力を受け付けます。
  • jumpForce: number
    tooltip: “ジャンプの初速度(上向き Y 速度)。値が大きいほど高く跳ぶ”
    – 1段目・2段目共通で使用する上方向速度。例: 500 ~ 1200 程度。
  • allowHoldToBoost: boolean
    tooltip: “ジャンプボタンを押し続けたときに、少しだけジャンプを伸ばすか”
    – true の場合、押しっぱなしで短時間だけ追加上昇を付与します。
  • holdBoostDuration: number
    tooltip: “押しっぱなしブーストが有効な最大時間(秒)”
    – allowHoldToBoost が true のときのみ使用。0.05 ~ 0.2 秒程度。
  • holdBoostForce: number
    tooltip: “押しっぱなしブースト時に毎フレーム加える上向き加速量”
    – 値を大きくしすぎると不自然な急上昇になるので注意。
  • groundCheckOffsetY: number
    tooltip: “ノードの原点からどれだけ下を地面判定ラインとみなすか(ローカル Y)”
    – キャラの足元がおおよそこの位置に来るように調整。例: -40 など。
  • groundCheckTolerance: number
    tooltip: “Y 位置変化がこの値以内で、かつ Y 速度が小さい場合に着地とみなす”
    – 0.5 ~ 3 程度。値が大きいほど着地判定が甘くなります。
  • groundedVelocityThreshold: number
    tooltip: “Y 速度がこの絶対値未満なら停止状態として着地判定に使う”
    – 1 ~ 5 程度。小さすぎると着地判定がシビアになります。
  • doubleJumpParticles: ParticleSystem2D | null
    tooltip: “2段ジャンプ時に再生するパーティクル。未設定でも動作する”
    – 任意の ParticleSystem2D をアサイン。2段目ジャンプで play() されます。
  • resetOnDisable: boolean
    tooltip: “コンポーネントが無効化されたときにジャンプ回数などをリセットするか”
    – true の場合、シーン切り替えやノードの再利用時に状態を初期化します。

内部で取得する標準コンポーネント

  • RigidBody2D
    – ジャンプのために必須。
    onLoadgetComponent(RigidBody2D) を試み、存在しなければエラーログを出して自動無効化します。

TypeScriptコードの実装

以下が完成した DoubleJump.ts の全コードです。


import { _decorator, Component, Node, RigidBody2D, Vec2, input, Input, EventKeyboard, KeyCode, ParticleSystem2D, CCBoolean, CCFloat } from 'cc';
const { ccclass, property } = _decorator;

/**
 * DoubleJump
 * - RigidBody2D を利用した 2 段ジャンプコンポーネント
 * - 他スクリプト非依存、インスペクタ設定のみで完結
 */
@ccclass('DoubleJump')
export class DoubleJump extends Component {

    // ===== 入力関連設定 =====
    @property({
        tooltip: 'Space キーでジャンプを許可するか',
    })
    public jumpKeySpace: boolean = true;

    @property({
        tooltip: '↑ キーでジャンプを許可するか',
    })
    public jumpKeyUpArrow: boolean = false;

    @property({
        tooltip: 'W キーでジャンプを許可するか',
    })
    public jumpKeyW: boolean = false;

    // ===== ジャンプ挙動設定 =====
    @property({
        tooltip: 'ジャンプの初速度(上向き Y 速度)。値が大きいほど高く跳ぶ',
    })
    public jumpForce: number = 800;

    @property({
        tooltip: 'ジャンプボタンを押し続けたときに、少しだけジャンプを伸ばすか',
    })
    public allowHoldToBoost: boolean = false;

    @property({
        tooltip: '押しっぱなしブーストが有効な最大時間(秒)',
        visible() {
            return this.allowHoldToBoost;
        }
    })
    public holdBoostDuration: number = 0.12;

    @property({
        tooltip: '押しっぱなしブースト時に毎フレーム加える上向き加速量',
        visible() {
            return this.allowHoldToBoost;
        }
    })
    public holdBoostForce: number = 50;

    // ===== 簡易地面判定設定 =====
    @property({
        tooltip: 'ノードの原点からどれだけ下を地面判定ラインとみなすか(ローカル Y)',
    })
    public groundCheckOffsetY: number = -40;

    @property({
        tooltip: 'Y 位置変化がこの値以内で、かつ Y 速度が小さい場合に着地とみなす',
    })
    public groundCheckTolerance: number = 1.0;

    @property({
        tooltip: 'Y 速度がこの絶対値未満なら停止状態として着地判定に使う',
    })
    public groundedVelocityThreshold: number = 3.0;

    // ===== 演出設定 =====
    @property({
        type: ParticleSystem2D,
        tooltip: '2段ジャンプ時に再生するパーティクル。未設定でも動作する',
    })
    public doubleJumpParticles: ParticleSystem2D | null = null;

    // ===== その他設定 =====
    @property({
        tooltip: 'コンポーネントが無効化されたときにジャンプ回数などをリセットするか',
    })
    public resetOnDisable: boolean = true;

    // ===== 内部状態 =====
    private _body: RigidBody2D | null = null;
    private _isGrounded: boolean = false;
    private _jumpCount: number = 0;
    private _lastGroundCheckY: number = 0;
    private _jumpButtonHeld: boolean = false;
    private _currentHoldTime: number = 0;

    onLoad() {
        // 必須コンポーネント取得
        this._body = this.getComponent(RigidBody2D);
        if (!this._body) {
            console.error('[DoubleJump] RigidBody2D が見つかりません。このコンポーネントを使うノードには RigidBody2D を追加してください。', this.node);
            // 物理ボディがないと動作できないので無効化
            this.enabled = false;
            return;
        }

        // 初期位置を記録(簡易地面判定に利用)
        this._lastGroundCheckY = this.node.worldPosition.y + this.groundCheckOffsetY;

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

    onEnable() {
        // 状態リセット
        this._isGrounded = false;
        this._jumpCount = 0;
        this._jumpButtonHeld = false;
        this._currentHoldTime = 0;
    }

    onDisable() {
        if (this.resetOnDisable) {
            this._isGrounded = false;
            this._jumpCount = 0;
            this._jumpButtonHeld = false;
            this._currentHoldTime = 0;
        }

        // 入力イベントは onLoad で登録しているが、
        // 無効化時にも二重登録を避けるため念のためオフにする。
        input.off(Input.EventType.KEY_DOWN, this._onKeyDown, this);
        input.off(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);
    }

    /**
     * キー押下時の処理
     */
    private _onKeyDown(event: EventKeyboard) {
        if (!this.enabled || !this._body) {
            return;
        }

        if (!this._isJumpKey(event.keyCode)) {
            return;
        }

        // 押した瞬間にジャンプ処理
        this._jump();

        // 押しっぱなし判定用フラグ
        this._jumpButtonHeld = true;
    }

    /**
     * キー解放時の処理
     */
    private _onKeyUp(event: EventKeyboard) {
        if (!this.enabled) {
            return;
        }

        if (!this._isJumpKey(event.keyCode)) {
            return;
        }

        // 押しっぱなしブーストを終了
        this._jumpButtonHeld = false;
        this._currentHoldTime = 0;
    }

    /**
     * 指定されたキーコードがジャンプに割り当てられているか判定
     */
    private _isJumpKey(keyCode: KeyCode): boolean {
        if (this.jumpKeySpace && keyCode === KeyCode.SPACE) return true;
        if (this.jumpKeyUpArrow && keyCode === KeyCode.ARROW_UP) return true;
        if (this.jumpKeyW && keyCode === KeyCode.KEY_W) return true;
        return false;
    }

    /**
     * 実際のジャンプ処理
     */
    private _jump() {
        if (!this._body) return;

        // 1段目:地面にいるときのみ
        if (this._isGrounded && this._jumpCount === 0) {
            this._applyJumpVelocity();
            this._jumpCount = 1;
            this._isGrounded = false;
            this._currentHoldTime = 0;
            return;
        }

        // 2段目:空中で 1 回だけ
        if (!this._isGrounded && this._jumpCount === 1) {
            this._applyJumpVelocity();
            this._jumpCount = 2;
            this._currentHoldTime = 0;

            // 2段目ジャンプパーティクル再生
            if (this.doubleJumpParticles) {
                this.doubleJumpParticles.play();
            }
        }

        // 3回目以降は何もしない
    }

    /**
     * RigidBody2D の速度ベクトルを上方向に設定
     */
    private _applyJumpVelocity() {
        if (!this._body) return;

        const v = this._body.linearVelocity;
        // X 速度は維持し、Y だけ上向きに設定
        v.y = this.jumpForce;
        this._body.linearVelocity = v;
    }

    update(dt: number) {
        if (!this._body) return;

        this._updateGroundedState();

        // 押しっぱなしブースト処理
        if (this.allowHoldToBoost && this._jumpButtonHeld) {
            this._currentHoldTime += dt;
            if (this._currentHoldTime <= this.holdBoostDuration) {
                const v = this._body.linearVelocity;
                v.y += this.holdBoostForce;
                this._body.linearVelocity = v;
            }
        }
    }

    /**
     * 簡易的な地面判定
     * - Y 位置の変化量
     * - Y 速度
     * を用いて「ほぼ静止している」ときに接地とみなす。
     */
    private _updateGroundedState() {
        if (!this._body) return;

        const worldY = this.node.worldPosition.y + this.groundCheckOffsetY;
        const vy = this._body.linearVelocity.y;

        const deltaY = Math.abs(worldY - this._lastGroundCheckY);

        // ほぼ同じ高さ & ほぼ停止 = 接地とみなす
        if (deltaY <= this.groundCheckTolerance && Math.abs(vy) <= this.groundedVelocityThreshold) {
            if (!this._isGrounded) {
                // 新たに着地した瞬間
                this._isGrounded = true;
                this._jumpCount = 0;
                this._currentHoldTime = 0;
            }
        } else {
            this._isGrounded = false;
        }

        this._lastGroundCheckY = worldY;
    }
}

主要メソッドの解説

  • onLoad
    RigidBody2D を取得し、存在しなければエラーログ+enabled = false で防御。
    – 入力イベント(KEY_DOWN / KEY_UP)を登録。
  • _onKeyDown / _onKeyUp
    – 押されたキーがジャンプ用に許可されているかを _isJumpKey で判定。
    – 押下時に _jump() を呼び、押しっぱなし判定用のフラグを更新。
  • _jump
    _isGrounded_jumpCount を見て、1段目/2段目のどちらかを実行。
    – 2段目ジャンプ時に、doubleJumpParticles が設定されていれば play() を呼び出して演出。
  • _applyJumpVelocity
    RigidBody2D.linearVelocity の Y だけを jumpForce に書き換え、X 速度は維持。
  • update
    – 毎フレーム _updateGroundedState() で接地状態を更新。
    allowHoldToBoost が true の場合、押しっぱなし時間を計測し、一定時間だけ上向き加速を付与。
  • _updateGroundedState
    – ノードのワールド Y + offset を「足元高さ」とみなし、前フレームとの差分と Y 速度を見て「ほぼ静止=接地」と判定。
    – 新たに接地した瞬間に _jumpCount = 0 にリセット。

使用手順と動作確認

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

  1. Creator の Assets パネルで任意のフォルダを右クリックします。
  2. Create → TypeScript を選択し、ファイル名を DoubleJump.ts として作成します。
  3. 作成した DoubleJump.ts をダブルクリックしてエディタで開き、上記コードをそのまま貼り付けて保存します。

2. テスト用ノードの作成

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite などを選択し、テスト用キャラノード(例: Player)を作成します。
  2. Inspector で Player ノードを選択し、Add Component → Physics 2D → RigidBody2D を追加します。
    Body TypeDynamic に設定してください。
    – 必要に応じて Gravity Scale や Linear Damping を調整します。
  3. 同じく Player ノードに、必要であれば Collider2D(BoxCollider2D など)を追加して地面との衝突判定をとるようにしておくと自然な動きになります。

注意: RigidBody2D を付け忘れると、ゲーム開始時にコンソールに「RigidBody2D が見つかりません」とエラーが出て、このコンポーネントは自動的に無効化されます。

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

  1. Hierarchy で Player ノードを選択します。
  2. Inspector の下部で Add Component → Custom → DoubleJump を選択します。
  3. Inspector に DoubleJump のプロパティが表示されるので、以下のように設定してみましょう(例):
    • Jump Key Space: チェック ON
    • Jump Key Up Arrow: チェック OFF
    • Jump Key W: チェック OFF
    • Jump Force: 900
    • Allow Hold To Boost: チェック OFF(まずはシンプルに)
    • Ground Check Offset Y: キャラの足元に合わせて -20 ~ -40 前後に調整
    • Ground Check Tolerance: 1 ~ 2
    • Grounded Velocity Threshold: 3
    • Double Jump Particles: いったん None のままでもよい
    • Reset On Disable: チェック ON

4. パーティクル演出の設定(任意)

2段ジャンプ時にエフェクトを出したい場合は、以下の手順でパーティクルを準備します。

  1. Hierarchy で Player ノードを右クリック → Create → 2D Object → Particle System 2D を選択し、子ノードとしてパーティクルを作成します(例: DoubleJumpEffect)。
  2. Inspector で DoubleJumpEffect ノードを選択し、ParticleSystem2D のパラメータ(Duration, EmissionRate, StartSpeed など)を好みに調整します。
  3. Player ノードを選択し、Inspector の DoubleJump コンポーネントにある Double Jump Particles プロパティに、DoubleJumpEffect ノードの ParticleSystem2D をドラッグ&ドロップでアサインします。

これで、2段目ジャンプが発生したときだけパーティクルが再生されるようになります。

5. シーンの物理設定と地面の準備

  1. メニューから Project → Project Settings → Physics 2D を開き、重力(例: Y = -1000)などを確認・調整します。
  2. Hierarchy で地面用のノード(例: Ground)を作成し、Sprite などで見た目を作成します。
  3. Ground ノードに Collider2D(BoxCollider2D など)を追加し、Player の RigidBody2D と接触するようにサイズ・位置を調整します。

これにより、Player が自然に地面に落下・着地するようになります。
DoubleJump コンポーネントの簡易地面判定は「Y 位置の変化量+Y 速度」で行っているため、完全なコリジョン情報に依存せずに動きますが、Collider2D を付けておくと物理的な見た目が自然になります。

6. プレイして動作確認

  1. 上部ツールバーの Play(▶) ボタンを押してゲームを再生します。
  2. シーン内で Player が地面に落ちて静止したら、Space キー を押してジャンプさせてみます。
  3. ジャンプ中(まだ落下中)のタイミングで、もう一度 Space キーを押すと、2段目ジャンプが発生し、さらに高く跳ぶはずです。
  4. 3回目以降は、着地するまでジャンプできないことを確認してください。
  5. パーティクルを設定している場合は、2段目ジャンプ時だけパーティクルが再生されるかも確認します。

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

  • Player ノードに RigidBody2D(Dynamic) が付いているか。
  • DoubleJump コンポーネントが 有効(Enabled) になっているか。
  • ジャンプに使いたいキー(Space / ↑ / W)が インスペクタで有効化 されているか。
  • Jump Force が小さすぎないか(300 以下だと重力設定によってはほとんど跳ねない場合があります)。
  • Ground ノードが十分に大きく、Player が落下してちゃんと着地できているか。

まとめ

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

  • RigidBody2D を持つ任意のノードにアタッチするだけで、
  • 単純な 2 段ジャンプ挙動と、
  • 2段目ジャンプ専用のパーティクル演出

を実現できる、再利用性の高い汎用スクリプトです。
外部の GameManager や入力管理スクリプトに依存せず、すべてをインスペクタのプロパティで完結させているため、

  • 別プロジェクトへのコピペ導入
  • 複数キャラでの使い回し(キャラごとに JumpForce を変えるなど)
  • 試作段階での素早いモックアップ

といった用途に非常に向いています。

応用として、

  • 2段目だけ JumpForce を変えるプロパティを追加する
  • 残りジャンプ回数を UI に表示するコンポーネントと組み合わせる
  • 二段目ジャンプ時にサウンドを再生する AudioSource2D を追加する

などを行えば、さらにリッチなジャンプシステムに発展させられます。
まずはこの基本形をベースに、あなたのゲームに合わせてプロパティや演出を拡張してみてください。