【Cocos Creator 3.8】JumpBuffer(先行入力)の実装:アタッチするだけで「着地直前のジャンプ入力を自動で保持して着地と同時にジャンプ」させる汎用スクリプト
2Dアクションゲームでよくある「ジャンプ先行入力(ジャンプバッファ)」を、1つのコンポーネントとして簡単に使えるようにします。
この JumpBuffer をプレイヤーノードにアタッチし、インスペクタでキーバインドやジャンプ力、バッファ時間などを設定するだけで、着地の少し前にジャンプボタンを押しても、着地した瞬間に自動でジャンプする挙動を実現できます。
目次
コンポーネントの設計方針
機能要件の整理
- ジャンプボタン(キーボード or パッドボタン)を監視する。
- ジャンプ可能でない状態(空中)でジャンプボタンが押された場合、
- その入力を「バッファ」として一定時間保持する。
- その後、プレイヤーが着地(地面に接触)した瞬間に、バッファが有効なうちならジャンプさせる。
- バッファ時間を過ぎたら、保持していたジャンプ入力は破棄する。
- 外部の GameManager や InputManager に依存せず、このコンポーネント単体で完結させる。
- 物理挙動は RigidBody2D を利用し、ジャンプは
linearVelocityを直接書き換えるシンプルな方式にする。 - 地面判定は 2D コリジョンの接触情報から行う(タグやレイヤーで地面を判別)。
外部依存をなくすためのアプローチ
- キーボード入力は
systemEvent.on(SystemEventType.KEY_DOWN)/KEY_UPをこのコンポーネント内で直接購読。 - ゲームパッド入力(任意)は
Input.instance.getGamepad()をupdate()内で直接ポーリング。 - ジャンプ実行もこのコンポーネントが
RigidBody2Dに直接アクセスして行う。 - 「地面かどうか」の判定は、インスペクタで指定したタグ名を持つ Collider2D と接触しているかで判定する。
- 必要な標準コンポーネント(RigidBody2D / Collider2D)は
onLoadで取得を試み、見つからない場合はerrorログを出す。
@property で設定可能なプロパティ
jumpKeyCode: KeyCode- ジャンプに使用するキーボードキー。
- デフォルトは
KeyCode.SPACE。
jumpButtonIndex: number- ゲームパッドのボタンインデックス(0 以上)。
- -1 の場合はゲームパッド入力を無効化する。
jumpVelocity: number- ジャンプ時に
RigidBody2D.linearVelocity.yに設定する上向き速度。 - 例:
jumpVelocity = 12など。
- ジャンプ時に
bufferTime: number- ジャンプボタンの先行入力を保持する時間(秒)。
- 例:
0.15(150ms 前の入力まで有効)。
coyoteTime: number- 「コヨーテタイム」(足場から落ちて少しの間だけジャンプ可能にする猶予時間・任意)。
- 0 の場合は無効。
- 例:
0.1(100ms)。
groundTag: string- 「地面」とみなす
Collider2Dのタグ名。 - 接触した Collider2D の
tag文字列と比較し、一致したものを地面として扱う。 - タグを使わない場合は空文字にして、すべての接触を地面扱いにすることも可能。
- 「地面」とみなす
maxJumpCount: number- 連続ジャンプの最大回数(1=シングルジャンプ、2=2段ジャンプ)。
- バッファは「次に可能なジャンプ」に対して有効。
resetVerticalVelocityOnJump: boolean- ジャンプ時に現在の
y速度をリセットしてからjumpVelocityを設定するかどうか。 - オンにすると、落下中にジャンプしても一定の高さになりやすい。
- ジャンプ時に現在の
TypeScriptコードの実装
import {
_decorator,
Component,
Node,
systemEvent,
SystemEventType,
EventKeyboard,
KeyCode,
input,
Input,
EventGamepad,
RigidBody2D,
Vec2,
Collider2D,
IPhysics2DContact,
game,
} from 'cc';
const { ccclass, property } = _decorator;
/**
* JumpBuffer
* - ジャンプボタンの先行入力(バッファ)とコヨーテタイムをサポートする汎用コンポーネント
* - このコンポーネントがアタッチされた Node に RigidBody2D と Collider2D があることを前提とする
*/
@ccclass('JumpBuffer')
export class JumpBuffer extends Component {
@property({
type: KeyCode,
tooltip: 'ジャンプに使用するキーボードキー。\n例: SPACE, KEY_Z など。',
})
public jumpKeyCode: KeyCode = KeyCode.SPACE;
@property({
tooltip: 'ゲームパッドのボタンインデックス。\n-1 の場合はゲームパッド入力を無効にします。'
})
public jumpButtonIndex: number = -1;
@property({
tooltip: 'ジャンプ時に与える上向き速度(RigidBody2D.linearVelocity.y)。'
})
public jumpVelocity: number = 12;
@property({
tooltip: 'ジャンプ先行入力(バッファ)を保持する時間(秒)。\n例: 0.15 なら 150ms 前の入力まで有効。'
})
public bufferTime: number = 0.15;
@property({
tooltip: 'コヨーテタイム(地面から離れてもこの秒数だけジャンプ可能)。\n0 で無効。'
})
public coyoteTime: number = 0.1;
@property({
tooltip: '「地面」とみなす Collider2D のタグ名。\n空文字の場合は、接触したすべてのコライダーを地面扱いにします。'
})
public groundTag: string = 'Ground';
@property({
tooltip: '連続ジャンプの最大回数。\n1 = シングルジャンプ, 2 = 2段ジャンプ など。'
})
public maxJumpCount: number = 1;
@property({
tooltip: 'ジャンプ時に現在の垂直速度(y)を 0 にリセットしてから jumpVelocity を適用するかどうか。'
})
public resetVerticalVelocityOnJump: boolean = true;
// 内部状態
private _rb2d: RigidBody2D | null = null;
private _collider: Collider2D | null = null;
private _isGrounded: boolean = false;
private _groundContactCount: number = 0;
private _lastGroundedTime: number = 0; // 最後に地面にいた時刻(秒)
private _lastJumpPressedTime: number = -999; // 最後にジャンプボタンが押された時刻(秒)
private _jumpPressed: boolean = false; // 現在ジャンプボタンが押されているか(キーボード)
private _gamepadButtonPrevPressed: boolean = false;
private _currentJumpCount: number = 0; // 現在のジャンプ回数(地面にいるとき 0)
onLoad() {
// 必要なコンポーネントを取得
this._rb2d = this.getComponent(RigidBody2D);
if (!this._rb2d) {
console.error('[JumpBuffer] RigidBody2D が見つかりません。このノードに RigidBody2D を追加してください。');
}
this._collider = this.getComponent(Collider2D);
if (!this._collider) {
console.error('[JumpBuffer] Collider2D が見つかりません。このノードに Collider2D を追加してください。');
} else {
// 衝突イベントの購読
this._collider.on('onBeginContact', this._onBeginContact, this);
this._collider.on('onEndContact', this._onEndContact, this);
}
// キーボード入力の購読
systemEvent.on(SystemEventType.KEY_DOWN, this._onKeyDown, this);
systemEvent.on(SystemEventType.KEY_UP, this._onKeyUp, this);
}
onDestroy() {
// イベントの解除
systemEvent.off(SystemEventType.KEY_DOWN, this._onKeyDown, this);
systemEvent.off(SystemEventType.KEY_UP, this._onKeyUp, this);
if (this._collider) {
this._collider.off('onBeginContact', this._onBeginContact, this);
this._collider.off('onEndContact', this._onEndContact, this);
}
}
start() {
// 初期状態の更新
this._updateGroundedState(0);
}
update(deltaTime: number) {
const now = game.totalTime / 1000; // ミリ秒 → 秒
// ゲームパッド入力のチェック(任意)
this._checkGamepadInput(now);
// 地面接触状態からコヨーテタイムを考慮した「ジャンプ可能か」を判定
const canJump = this._canJump(now);
// 先行入力(バッファ)の有効性チェック
const hasBufferedJump = this._hasBufferedJump(now);
// 条件を満たしていればジャンプを実行
if (canJump && hasBufferedJump) {
this._performJump();
}
}
//=====================
// 入力処理
//=====================
private _onKeyDown(event: EventKeyboard) {
if (event.keyCode === this.jumpKeyCode) {
this._jumpPressed = true;
this._lastJumpPressedTime = game.totalTime / 1000;
}
}
private _onKeyUp(event: EventKeyboard) {
if (event.keyCode === this.jumpKeyCode) {
this._jumpPressed = false;
}
}
private _checkGamepadInput(now: number) {
if (this.jumpButtonIndex < 0) {
return;
}
const gamepads = Input.instance.getGamepads();
if (!gamepads || gamepads.length === 0) {
this._gamepadButtonPrevPressed = false;
return;
}
const pad = gamepads[0];
if (!pad) {
this._gamepadButtonPrevPressed = false;
return;
}
const buttons = pad.buttonStates;
if (!buttons || this.jumpButtonIndex >= buttons.length) {
this._gamepadButtonPrevPressed = false;
return;
}
const pressed = buttons[this.jumpButtonIndex].pressed;
// 立ち上がりエッジでのみバッファ登録
if (pressed && !this._gamepadButtonPrevPressed) {
this._lastJumpPressedTime = now;
}
this._gamepadButtonPrevPressed = pressed;
}
//=====================
// 地面判定
//=====================
private _onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
if (this._isGroundCollider(otherCollider)) {
this._groundContactCount++;
if (this._groundContactCount > 0) {
if (!this._isGrounded) {
this._setGrounded(true);
}
}
}
}
private _onEndContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
if (this._isGroundCollider(otherCollider)) {
this._groundContactCount--;
if (this._groundContactCount <= 0) {
this._groundContactCount = 0;
if (this._isGrounded) {
this._setGrounded(false);
}
}
}
}
private _isGroundCollider(other: Collider2D): boolean {
if (!other) return false;
if (!this.groundTag || this.groundTag === '') {
// groundTag が空の場合、すべての接触を地面扱い
return true;
}
return other.tag === this.groundTag;
}
private _setGrounded(value: boolean) {
this._isGrounded = value;
if (value) {
this._currentJumpCount = 0;
this._lastGroundedTime = game.totalTime / 1000;
}
}
private _updateGroundedState(now: number) {
// 明示的に呼び出したい場合のために残しているが、
// 実際の更新は衝突コールバックで行っている。
if (this._isGrounded) {
this._lastGroundedTime = now;
}
}
//=====================
// ジャンプ判定と実行
//=====================
private _canJump(now: number): boolean {
if (!this._rb2d) {
return false;
}
// まだ最大ジャンプ回数に達していないか
if (this._currentJumpCount >= this.maxJumpCount) {
return false;
}
// 地面にいる or コヨーテタイム内なら OK
if (this._isGrounded) {
return true;
}
if (this.coyoteTime > 0) {
const timeSinceGrounded = now - this._lastGroundedTime;
if (timeSinceGrounded <= this.coyoteTime) {
return true;
}
}
// 空中での追加ジャンプ(2段ジャンプなど)
if (this._currentJumpCount > 0 && this._currentJumpCount < this.maxJumpCount) {
return true;
}
return false;
}
private _hasBufferedJump(now: number): boolean {
const timeSincePressed = now - this._lastJumpPressedTime;
return timeSincePressed <= this.bufferTime;
}
private _performJump() {
if (!this._rb2d) {
console.error('[JumpBuffer] RigidBody2D がないためジャンプできません。');
return;
}
// 現在の速度を取得
const v = this._rb2d.linearVelocity.clone();
if (this.resetVerticalVelocityOnJump) {
v.y = 0;
}
// 上向き速度を設定
v.y = this.jumpVelocity;
this._rb2d.linearVelocity = v;
// ジャンプ回数を更新
this._currentJumpCount++;
// このジャンプに対応したバッファを消費(2回連続でジャンプしないように)
this._lastJumpPressedTime = -999;
}
}
コードの主要部分の解説
onLoadRigidBody2DとCollider2Dを取得し、存在しなければconsole.errorで警告。Collider2Dが取れた場合はonBeginContact/onEndContactを購読して地面判定に使う。systemEventでキーボードのKEY_DOWN/KEY_UPを購読。
update- 現在時刻
nowをgame.totalTime / 1000から取得。 - ゲームパッド入力をチェックし、ジャンプボタンの立ち上がりで
_lastJumpPressedTimeを更新。 _canJump(now)で「ジャンプ可能か」(地面 or コヨーテタイム or 空中ジャンプ)を判定。_hasBufferedJump(now)で「バッファ内のジャンプ入力があるか」を判定。- 両方満たしていれば
_performJump()を呼び出してジャンプを実行。
- 現在時刻
- 地面判定
_onBeginContact/_onEndContactで、接触している「地面」コライダーの数をカウント。groundTagが空文字なら、すべての接触を地面扱いにする簡易モード。- 地面に初めて接触したタイミングで
_setGrounded(true)を呼び、_currentJumpCountを 0 にリセット。 - 地面から完全に離れたら
_setGrounded(false)を呼ぶ。
- ジャンプバッファ
- キーボード or ゲームパッドでジャンプボタンが押された瞬間に
_lastJumpPressedTimeを更新。 _hasBufferedJumpでは「現在時刻 – 最後に押された時刻 <= bufferTime」なら有効とみなす。- ジャンプを実行したら
_lastJumpPressedTimeを大きく負の値にしてバッファを消費。
- キーボード or ゲームパッドでジャンプボタンが押された瞬間に
- コヨーテタイム
- 地面にいる間は
_lastGroundedTimeを更新。 - 地面から離れた後も、
now - _lastGroundedTime <= coyoteTimeならジャンプ可能とする。
- 地面にいる間は
使用手順と動作確認
1. スクリプトファイルの作成
- エディタ下部の Assets パネルで、スクリプトを置きたいフォルダ(例:
assets/scripts)を選択します。 - フォルダ上で右クリック → Create → TypeScript を選択します。
- 新しく作成されたファイル名を
JumpBuffer.tsに変更します。 JumpBuffer.tsをダブルクリックして開き、既存の中身をすべて削除し、前述のコードをそのまま貼り付けて保存します。
2. テスト用プレイヤーノードの準備
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、テスト用のプレイヤーノードを作成します。
- 分かりやすいように名前を
Playerに変更しておきましょう。
- 分かりやすいように名前を
- Inspector パネルで、
Playerノードを選択した状態で以下を追加します:- Add Component → Physics 2D → RigidBody2D
Typeを Dynamic に設定します。
- Add Component → Physics 2D → BoxCollider2D(または CircleCollider2D)
- Sprite のサイズに合わせて
Sizeを調整します。
- Sprite のサイズに合わせて
- Add Component → Physics 2D → RigidBody2D
3. 地面(Ground)ノードの作成
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、地面用のノードを作成します。
- 名前を
Groundに変更し、Inspector のTransformでScaleやSizeを横長の床になるように調整します。Position.yを 0 や -200 など、プレイヤーが落ちて乗れる位置に設定します。
- Inspector で以下のコンポーネントを追加します:
- Add Component → Physics 2D → RigidBody2D
Typeを Static に設定します。
- Add Component → Physics 2D → BoxCollider2D
- Add Component → Physics 2D → RigidBody2D
- Ground の Collider2D にタグを設定
- Ground ノードの
BoxCollider2Dコンポーネントを選択し、TagフィールドにGroundと入力します。 - これは
JumpBufferコンポーネントのgroundTagと一致させるためです。
- Ground ノードの
4. JumpBuffer コンポーネントをアタッチ
- Hierarchy で
Playerノードを選択します。 - Inspector の下部で Add Component → Custom → JumpBuffer を選択します。
JumpBufferコンポーネントのプロパティを設定します:Jump Key Code:SPACE(デフォルトのままで OK)Jump Button Index:-1(ゲームパッドを使う場合は 0 などに変更)Jump Velocity:12(ゲームのスケールに応じて 8~20 程度で調整)Buffer Time:0.15(0.1~0.2 の範囲がおすすめ)Coyote Time:0.1(任意。不要なら 0)Ground Tag:Ground(先ほど地面コライダーに設定したタグと一致させる)Max Jump Count:1(2段ジャンプを試したい場合は 2 に)Reset Vertical Velocity On Jump:チェックを入れる(オン)
5. シーンの物理設定を確認
- Project → Project Settings → Physics 2D を開き、物理が有効になっていることを確認します。
- 重力(
Gravity)が適切な値(例:(0, -10)など)になっているかを確認します。
6. 動作確認
- シーンを保存し、上部の Play ボタン(▶)を押して再生します。
- ゲーム画面でプレイヤーが地面の上に落ちて静止することを確認します。
- 通常ジャンプの確認:
- プレイヤーが地面にいる状態で
SPACEキーを押すと、指定したjumpVelocityでジャンプするはずです。
- プレイヤーが地面にいる状態で
- 先行入力(バッファ)の確認:
- プレイヤーが空中から地面に落ちてくるタイミングで、着地より少し早めに
SPACEキーを押します。 - キーを押した時点では空中なので通常ならジャンプできませんが、このコンポーネントが入力をバッファしているため、着地した瞬間に自動でジャンプするはずです。
- バッファ時間
bufferTimeを 0.05 にしてみると、かなりシビアな先行入力に、0.25 にするとかなり余裕のある先行入力になります。
- プレイヤーが空中から地面に落ちてくるタイミングで、着地より少し早めに
- コヨーテタイムの確認(任意):
- プレイヤーを小さな足場の端まで歩かせ、足場から落ちた直後に
SPACEを押します。 coyoteTimeを 0.1 にしている場合、足場から少し離れていてもジャンプが発動することを確認できます。
- プレイヤーを小さな足場の端まで歩かせ、足場から落ちた直後に
- 2段ジャンプの確認(任意):
JumpBufferのMax Jump Countを 2 に変更します。- プレイヤーをジャンプさせ、空中でもう一度
SPACEを押すと 2 段目のジャンプが発動します。 - このときも、空中でのジャンプボタン押下に対してバッファが効くため、次に可能なジャンプタイミングで自動発動します。
まとめ
この JumpBuffer コンポーネントを使うことで、
- 先行入力(ジャンプバッファ)
- コヨーテタイム
- 最大ジャンプ回数(2段ジャンプなど)
- キーボード / ゲームパッド両対応
といった、2Dアクションゲームのジャンプに必要な要素を、1つのスクリプトをプレイヤーノードにアタッチするだけでまとめて実現できます。
このスクリプトは他のカスタムスクリプトに依存していないため、
- 別プロジェクトへのコピペ
- 他のキャラクターへの流用(別の Node にアタッチしてパラメータだけ変える)
- プロトタイプ段階での素早い調整
に非常に向いています。
ジャンプの「気持ちよさ」はアクションゲームのプレイ感を大きく左右します。
bufferTime や coyoteTime、jumpVelocity をゲームに合わせて調整しながら、理想のジャンプフィールを作り込んでみてください。
