【Cocos Creator 3.8】CoyoteTime の実装:アタッチするだけで「足場から落ちた直後もジャンプ可能」にする汎用スクリプト
2D/3Dの横スクロールアクションでよくある「コヨーテタイム(Coyote Time)」を、1コンポーネントだけで実現します。
このコンポーネントをプレイヤーキャラのノードにアタッチするだけで、「足場(接地状態)から離れてから一定時間だけジャンプ入力を受け付ける」挙動を実装できます。
ジャンプ処理そのもの(上方向の速度を与える・アニメーションを再生する等)は、このコンポーネント内で完結させており、他のGameManagerや外部スクリプトには一切依存しません。
インスペクタで「コヨーテタイムの長さ」「ジャンプキー」「ジャンプ力」「接地判定のレイキャスト設定」などを調整可能にし、どのプロジェクトにもそのまま持ち込んで使えるようにします。
コンポーネントの設計方針
1. 機能要件の整理
- ノードが「地面に接地しているかどうか」を自前のレイキャストで判定する。
- 接地している間は「地上状態」とし、離れた瞬間に「空中状態」へ遷移する。
- 空中状態になってから一定時間だけ「コヨーテタイム有効」としてジャンプ入力を受け付ける。
- その時間を過ぎたら、ジャンプ入力は無効にする。
- ジャンプ入力が押されたら、Rigidbody2D に上方向の速度を与えてジャンプさせる。
- ジャンプは「接地中」または「コヨーテタイム中」にだけ許可する。
- 物理挙動は Box2D(Rigidbody2D)に任せる。
重要:このコンポーネントは完全に独立して動作するため、他のスクリプトから「ジャンプして」などと呼ばれることを前提にしません。
「ジャンプキー入力の取得」「接地判定」「ジャンプの物理処理」を全てこのコンポーネント内部で行います。
2. 外部依存をなくすためのアプローチ
- 入力: Cocos Creator の
input.onAPI を直接利用し、指定キー(例: Space, Z, X など)を監視。 - 物理: 自身のノードにアタッチされた
Rigidbody2Dを取得してジャンプ力を付与。 - 接地判定: 自身のノードの位置から下方向にレイキャストして、一定距離内にコライダーがあれば接地とみなす。
- 時間管理: Cocos の
deltaTimeを使って経過時間を管理し、コヨーテタイムの残り時間をカウントダウン。
防御的実装:
Rigidbody2D が見つからない場合は console.error でエラーを出し、動作を停止します。
また、物理レイヤーやレイキャスト距離の設定はプロパティ化して、インスペクタから調整できるようにします。
3. インスペクタで設定可能なプロパティ設計
本コンポーネント CoyoteTime で用意する主なプロパティは以下の通りです。
-
coyoteTimeDuration: number
– コヨーテタイムの長さ(秒)。
– 例:0.1~0.2くらいが一般的。
– 足場から離れてからこの時間だけジャンプ入力を許可します。 -
jumpVelocity: number
– ジャンプ時に Rigidbody2D のlinearVelocity.yに設定する上方向の速度。
– 例:10~20など、ゲームのスケールに合わせて調整。 -
groundCheckDistance: number
– 接地判定のために下方向に飛ばすレイキャストの長さ(メートル)。
– 小さすぎると接地を取りこぼし、大きすぎると空中でも地面判定してしまうので注意。
– 例:0.1~0.3。 -
groundCheckOffset: Vec2
– レイキャストの開始位置をノードの中心からずらしたい場合のオフセット。
– 例: キャラの足元に合わせるために(0, -0.5)など。 -
groundLayerMask: number
– 接地判定対象とする物理レイヤーのマスク。
– 物理レイヤー設定に応じて、地面だけを判定対象にしたい場合に使用。
– 例: 「Default」レイヤーだけなら1 << 0など。 -
jumpKey: string
– ジャンプに使用するキーボードキーを文字列で指定。
– 例:"Space","Z","X","W"など。
– Cocos のKeyCode名と対応するように実装します。 -
allowHoldJump: boolean
– true の場合、ジャンプキーを押しっぱなしでもジャンプを許可(1回だけ)。
– false の場合、キーを押した瞬間(押下イベント)のみジャンプを受け付ける。 -
debugDraw: boolean
– true の場合、接地判定レイの情報をログに出すなど、簡易デバッグ出力を有効にする。
TypeScriptコードの実装
以下が完成した CoyoteTime.ts コンポーネントの全コードです。
import {
_decorator,
Component,
Node,
Vec2,
Vec3,
RigidBody2D,
PhysicsSystem2D,
ERaycast2DType,
Contact2DType,
Collider2D,
input,
Input,
EventKeyboard,
KeyCode,
} from 'cc';
const { ccclass, property } = _decorator;
/**
* CoyoteTime
* 足場から落ちた直後の短時間だけ、ジャンプ入力を許容するコンポーネント。
* - 自身のノードに Rigidbody2D が必要です。
* - 接地判定は下方向レイキャストで行います。
* - ジャンプ入力はキーボードから取得します(指定キー)。
*/
@ccclass('CoyoteTime')
export class CoyoteTime extends Component {
@property({
tooltip: 'コヨーテタイムの長さ(秒)。足場から離れてからこの時間だけジャンプ入力を許可します。',
})
public coyoteTimeDuration: number = 0.15;
@property({
tooltip: 'ジャンプ時に与える上方向の速度(Rigidbody2D.linearVelocity.y に設定されます)。',
})
public jumpVelocity: number = 12;
@property({
tooltip: '接地判定のために下方向に飛ばすレイキャストの長さ(メートル)。',
})
public groundCheckDistance: number = 0.2;
@property({
tooltip: '接地判定レイの開始位置オフセット(ノードのローカル座標系)。',
})
public groundCheckOffset: Vec2 = new Vec2(0, -0.5);
@property({
tooltip: '接地判定対象とする物理レイヤーマスク(Physics2D のレイヤー設定に合わせてください)。\n例: Default レイヤーのみ = 1 << 0',
})
public groundLayerMask: number = 0xffffffff; // 全レイヤー対象
@property({
tooltip: 'ジャンプに使用するキー名。\n例: "Space", "Z", "X", "W" など。大文字・小文字は区別されません。',
})
public jumpKey: string = 'Space';
@property({
tooltip: 'true: キー押しっぱなしでもジャンプを許可(1回だけ)。\nfalse: キーを押した瞬間のみジャンプを受け付けます。',
})
public allowHoldJump: boolean = false;
@property({
tooltip: 'true にすると接地判定レイの結果などをログ出力します(開発用)。',
})
public debugDraw: boolean = false;
// 内部状態
private _rb2d: RigidBody2D | null = null;
private _isGrounded: boolean = false;
private _wasGrounded: boolean = false;
private _coyoteTimer: number = 0;
private _jumpKeyCode: KeyCode = KeyCode.SPACE;
private _jumpKeyPressed: boolean = false;
private _jumpKeyHeld: boolean = false;
onLoad() {
// 必要なコンポーネントの取得
this._rb2d = this.getComponent(RigidBody2D);
if (!this._rb2d) {
console.error('[CoyoteTime] このコンポーネントを使用するには、同じノードに Rigidbody2D が必要です。');
}
// ジャンプキー文字列を KeyCode に変換
this._jumpKeyCode = this._convertKeyStringToKeyCode(this.jumpKey);
// 入力イベント登録
input.on(Input.EventType.KEY_DOWN, this._onKeyDown, this);
input.on(Input.EventType.KEY_UP, this._onKeyUp, this);
}
start() {
// 初期接地状態をチェック
this._isGrounded = this._checkGrounded();
this._wasGrounded = this._isGrounded;
this._coyoteTimer = 0;
}
onDestroy() {
// 入力イベント解除
input.off(Input.EventType.KEY_DOWN, this._onKeyDown, this);
input.off(Input.EventType.KEY_UP, this._onKeyUp, this);
}
update(deltaTime: number) {
if (!this._rb2d) {
// 必須コンポーネントが無い場合は何もしない
return;
}
// 接地状態を更新
this._wasGrounded = this._isGrounded;
this._isGrounded = this._checkGrounded();
// 地面から離れた瞬間にコヨーテタイム開始
if (this._wasGrounded && !this._isGrounded) {
this._coyoteTimer = this.coyoteTimeDuration;
if (this.debugDraw) {
console.log('[CoyoteTime] 離陸: コヨーテタイム開始', this._coyoteTimer);
}
}
// 接地している間はコヨーテタイムタイマーをリセット
if (this._isGrounded) {
this._coyoteTimer = this.coyoteTimeDuration;
} else {
// 空中ではコヨーテタイムをカウントダウン
if (this._coyoteTimer > 0) {
this._coyoteTimer -= deltaTime;
if (this._coyoteTimer < 0) {
this._coyoteTimer = 0;
}
}
}
// ジャンプ処理
this._handleJump();
}
/**
* キー押下イベント
*/
private _onKeyDown(event: EventKeyboard) {
if (event.keyCode === this._jumpKeyCode) {
this._jumpKeyPressed = true;
this._jumpKeyHeld = true;
}
}
/**
* キー離しイベント
*/
private _onKeyUp(event: EventKeyboard) {
if (event.keyCode === this._jumpKeyCode) {
this._jumpKeyHeld = false;
}
}
/**
* ジャンプ処理の判定と実行
*/
private _handleJump() {
if (!this._rb2d) {
return;
}
const canUseCoyote = this._coyoteTimer > 0;
const canJumpNow = this._isGrounded || canUseCoyote;
let wantJump = false;
if (this.allowHoldJump) {
// 押しっぱなしでもジャンプ許可(ただし 1 回)
wantJump = this._jumpKeyHeld;
} else {
// 押した瞬間のみ
wantJump = this._jumpKeyPressed;
}
if (wantJump && canJumpNow) {
// 実際にジャンプを実行
const v = this._rb2d.linearVelocity;
v.y = this.jumpVelocity;
this._rb2d.linearVelocity = v;
// コヨーテタイムを消費
this._coyoteTimer = 0;
if (this.debugDraw) {
console.log('[CoyoteTime] ジャンプ実行: isGrounded=', this._isGrounded, ' coyote=', canUseCoyote);
}
}
// 1 フレームだけ有効な「押した瞬間」フラグをリセット
this._jumpKeyPressed = false;
}
/**
* 接地判定を行う
* 下方向にレイキャストして、指定距離内に Collider2D があれば接地とみなします。
*/
private _checkGrounded(): boolean {
const physics = PhysicsSystem2D.instance;
if (!physics) {
console.error('[CoyoteTime] PhysicsSystem2D が有効になっていません。Project Settings の Physics で 2D Physics を有効にしてください。');
return false;
}
const worldPos = this.node.worldPosition;
const origin = new Vec2(
worldPos.x + this.groundCheckOffset.x,
worldPos.y + this.groundCheckOffset.y
);
const target = new Vec2(origin.x, origin.y - this.groundCheckDistance);
const results = physics.raycast(
origin,
target,
ERaycast2DType.Closest,
this.groundLayerMask
);
const grounded = results.length > 0;
if (this.debugDraw) {
console.log(
'[CoyoteTime] GroundCheck:',
'origin=', origin,
'target=', target,
'hit=', grounded
);
}
return grounded;
}
/**
* インスペクタの jumpKey 文字列を Cocos の KeyCode に変換します。
* 未対応の文字列の場合は Space にフォールバックします。
*/
private _convertKeyStringToKeyCode(key: string): KeyCode {
const normalized = key.trim().toUpperCase();
switch (normalized) {
case 'SPACE':
return KeyCode.SPACE;
case 'Z':
return KeyCode.KEY_Z;
case 'X':
return KeyCode.KEY_X;
case 'C':
return KeyCode.KEY_C;
case 'W':
return KeyCode.KEY_W;
case 'A':
return KeyCode.KEY_A;
case 'S':
return KeyCode.KEY_S;
case 'D':
return KeyCode.KEY_D;
case 'UP':
case 'ARROWUP':
return KeyCode.ARROW_UP;
case 'DOWN':
case 'ARROWDOWN':
return KeyCode.ARROW_DOWN;
case 'LEFT':
case 'ARROWLEFT':
return KeyCode.ARROW_LEFT;
case 'RIGHT':
case 'ARROWRIGHT':
return KeyCode.ARROW_RIGHT;
default:
console.warn(`[CoyoteTime] 未対応の jumpKey "${key}" が指定されました。Space にフォールバックします。`);
return KeyCode.SPACE;
}
}
}
コードのポイント解説
-
onLoad
–Rigidbody2Dを取得し、存在しなければconsole.error。
– インスペクタで指定されたjumpKeyをKeyCodeに変換。
– キーボード入力イベント (KEY_DOWN,KEY_UP) を登録。 -
start
– ゲーム開始時の接地状態を_checkGrounded()で判定し、内部フラグを初期化。 -
update
– 毎フレーム、現在の接地状態をレイキャストで更新。
– 地面から離れた瞬間に_coyoteTimerにcoyoteTimeDurationをセット。
– 接地している間はタイマーを常にリセット、空中ではdeltaTimeでカウントダウン。
– 最後に_handleJump()を呼び出し、ジャンプ入力とコヨーテタイム条件を満たす場合にジャンプを実行。 -
_checkGrounded
–PhysicsSystem2D.instance.raycastを使用して、ノードの足元から下方向へレイを飛ばし、groundLayerMaskで指定されたレイヤーにヒットすれば接地と判定。
–groundCheckOffsetとgroundCheckDistanceでレイの位置と長さを調整可能。 -
_handleJump
–allowHoldJumpに応じて、「押しっぱなし」または「押した瞬間」のどちらでジャンプを受け付けるかを切り替え。
–_isGroundedまたは_coyoteTimer > 0の場合にだけジャンプを許可。
– ジャンプ時にはRigidbody2D.linearVelocity.yをjumpVelocityに設定し、コヨーテタイムを 0 にリセット。 -
_convertKeyStringToKeyCode
– インスペクタに入力された文字列(例: “Space”, “Z”, “ArrowUp”)をKeyCodeに変換します。
– 想定外の文字列の場合は警告を出しつつ Space にフォールバック。
使用手順と動作確認
1. スクリプトファイルの作成
- エディタ上部の Assets パネルで、任意のフォルダ(例:
assets/scripts)を右クリックします。 - Create > TypeScript を選択し、ファイル名を
CoyoteTime.tsとします。 - 自動生成されたファイルをダブルクリックして開き、内容をすべて削除して、前述のコードを丸ごと貼り付けて保存します。
2. テスト用プレイヤーノードの準備
ここでは 2D プロジェクトを前提に、簡単なテストシーンを作ります。
- Hierarchy パネルで右クリック → Create > 2D Object > Sprite を選択し、ノード名を
Playerに変更します。 Playerノードを選択し、Inspector で以下を設定します:- Add Component > Physics 2D > RigidBody2D を追加
- Type: Dynamic
- Gravity Scale: 必要に応じて調整(例:
1)
- Add Component > Physics 2D > BoxCollider2D を追加
- サイズをスプライトに合うように調整(自動サイズでも可)。
- Add Component > Physics 2D > RigidBody2D を追加
注意: CoyoteTime は Rigidbody2D を必須とするため、これを付け忘れるとコンソールにエラーが出てジャンプしません。
3. 地面(足場)の作成
- Hierarchy で右クリック → Create > 2D Object > Sprite を選択し、ノード名を
Groundに変更します。 Groundノードを選択し、Inspector で以下を設定します:- Transform の Scale を X 方向に大きくして床っぽくします(例:
(10, 1, 1))。 - Add Component > Physics 2D > BoxCollider2D を追加。
- Add Component > Physics 2D > RigidBody2D を追加し、Type を Static に設定。
- Transform の Scale を X 方向に大きくして床っぽくします(例:
Playerの初期位置をGroundの少し上に配置しておきます。
物理レイヤーをカスタマイズしている場合は、Ground の Collider2D が属するレイヤーと、CoyoteTime の groundLayerMask が一致するように調整してください。
(デフォルト設定のままなら、groundLayerMask = 0xffffffff のままで全レイヤー対象として機能します)
4. CoyoteTime コンポーネントのアタッチ
Playerノードを選択します。- Inspector の一番下で Add Component > Custom > CoyoteTime を選択して追加します。
- 追加された CoyoteTime コンポーネントのプロパティを設定します:
- Coyote Time Duration:
0.15(まずは 0.15 秒程度から試す) - Jump Velocity:
12(シーンのスケールに応じて 8〜20 程度で調整) - Ground Check Distance:
0.2 - Ground Check Offset:
(0, -0.5)(プレイヤーの足元に合わせて微調整) - Ground Layer Mask:
0xffffffff(デフォルトのまま) - Jump Key:
Space - Allow Hold Jump: 好みで
trueorfalse - Debug Draw: 最初は
trueにしてログを見ながら調整すると便利
- Coyote Time Duration:
5. 動作確認
- エディタ右上の Play ボタンでシーンを実行します。
- ゲームウィンドウで、
PlayerがGroundの上に立っていることを確認します。 - Space キー(または設定したジャンプキー)を押してジャンプできることを確認します。
- 次に、走る機能がない場合は、
Playerの初期位置をGroundの端ギリギリに配置し、落ちる直前〜落ちた直後に Space を押してみてください。- 足場から完全に離れても、
Coyote Time Durationで指定した時間内であればジャンプが発動するはずです。 - 時間を過ぎると、空中ではジャンプできなくなります。
- 足場から完全に離れても、
- コンソールに
[CoyoteTime] GroundCheck:や[CoyoteTime] ジャンプ実行:のログが出ていれば、接地判定とコヨーテタイムが正しく動いています。
6. よくある調整ポイント
-
ジャンプが高すぎる / 低すぎる
→Jump Velocityを上下して調整します。 -
コヨーテタイムが長すぎる / 短すぎる
→Coyote Time Durationを0.05〜0.25秒くらいで微調整すると、自然な操作感を得やすいです。 -
接地しているのに「落ちている」と判定される
→Ground Check Distanceを少し長くする(例: 0.2 → 0.3)。
→Ground Check Offsetをプレイヤーの足元にしっかり合わせる。 -
空中でもずっと接地扱いになる
→Ground Check Distanceが長すぎる可能性があります。短くしてみてください。
→ 物理レイヤーが広すぎて、別のオブジェクトを拾っている場合もあるので、Ground Layer Maskを地面専用レイヤーに絞るのも有効です。
まとめ
この CoyoteTime コンポーネントは、
- Rigidbody2D とレイキャストだけで接地判定とコヨーテタイムを実装
- 入力取得からジャンプ処理までを1ファイルで完結
- インスペクタからパラメータを調整可能で、どのプロジェクトにも簡単に持ち込める
という特徴を持つ、完全独立型の汎用コンポーネントです。
プレイヤー制御の中でも「ジャンプ入力の猶予時間」は、ゲームの手触りを大きく左右する重要な要素です。
このスクリプトをそのまま使うだけでも十分ですが、慣れてきたら
- 多段ジャンプや壁ジャンプと組み合わせる
- アニメーション再生や SE 再生を
_handleJump()に追加する - 接地判定の方法を「フットコライダーの OnCollisionEnter2D / Exit2D」に切り替える
などの発展も可能です。
まずはこのままプロジェクトに組み込んで、「足場から落ちても一瞬だけジャンプできる」感覚を体験しながら、Coyote Time Duration を調整してみてください。
それだけで、プレイヤーの操作感がぐっと「遊びやすい」方向に変わるはずです。
