【Cocos Creator 3.8】JumpController の実装:アタッチするだけで「ジャンプボタン入力+接地判定付きジャンプ」を実現する汎用スクリプト
2D/3D プラットフォーマーやアクションゲームで必須の「ジャンプ」。本記事では、Rigidbody2D を持つノードにアタッチするだけで、ジャンプボタン入力を検知し、接地中のみ上方向に速度を与えてジャンプさせる汎用コンポーネント JumpController を実装します。外部の GameManager などには一切依存せず、インスペクタからキー設定やジャンプ力、接地判定方法を調整できるようにします。
目次
コンポーネントの設計方針
実現したい機能
- ジャンプボタン(キーボード or タッチ)入力を検知する。
- 親ノード(自身のノード)が「床にいる(接地している)」ときだけジャンプできる。
- ジャンプ時には Rigidbody2D に上向きの速度(velocity)を与える。
- 接地判定は「足元の小さなレイキャスト(下方向に Line)」「または Rigidbody2D の接触イベント」を使う。
- すべての設定はインスペクタから行え、他のカスタムスクリプトへの依存はゼロ。
外部依存をなくすためのアプローチ
- 必須コンポーネントは Rigidbody2D のみとし、
onLoadでgetComponentによる取得と存在チェックを行う。 - 接地判定には PhysicsSystem2D のレイキャスト を用い、床レイヤーや判定距離をプロパティ化する。
- 入力は Input キーイベント と、任意で タッチ/マウスクリック に対応する。
- ジャンプの「クールタイム」や「ボタンを押した瞬間だけ反応(押しっぱなし無視)」などもプロパティで制御できるようにする。
インスペクタで設定可能なプロパティ設計
以下のようなプロパティを用意します。
- jumpKey(
KeyCode)- ジャンプに使用するキーボードキー。
- 例:
KeyCode.SPACE(デフォルト)。
- enableTouchJump(boolean)
trueの場合、画面タップ/クリックでもジャンプを行う。
- jumpVelocity(number)
- ジャンプ時に Rigidbody2D に与える上方向の速度。
- 正の値で上方向。例: 8 ~ 15 程度。
- overrideHorizontalVelocity(boolean)
trueの場合、ジャンプ時に X 方向の速度を 0 にリセットする。- 横移動と組み合わせる場合、X 速度を保持したいなら
falseにする。
- groundCheckDistance(number)
- 接地判定用のレイキャスト距離(ノードの位置から下向き)。
- 例: 0.1 ~ 0.3 くらい。キャラの足元から床までの隙間に合わせて調整。
- groundLayerMask(number)
- 床として判定する 2D 物理レイヤーのビットマスク。
- 例:
1 << 1(デフォルトでDEFAULTが 1、UIが 2 などプロジェクト設定に依存)。 - インスペクタ上では数値で指定するが、
1 << レイヤー番号で計算する。
- groundCheckOffsetY(number)
- レイキャストの開始位置をノードの中心からどれだけ下にずらすか。
- 例: -0.5 なら、ノードの中心より 0.5 下からレイを飛ばす。
- allowAirJump(boolean)
trueの場合、接地していなくても一定回数まで空中ジャンプを許可する。
- maxAirJumpCount(number)
- 空中ジャンプを許可する回数(通常のジャンプを除く)。
- 例: 1 なら二段ジャンプ、2 なら三段ジャンプ。
- jumpCooldown(number)
- 連続ジャンプを防ぐためのクールタイム(秒)。
- 0 ならクールタイムなし。
- usePressOnce(boolean)
trueの場合、ボタンを押した瞬間だけジャンプし、押しっぱなしでは連続ジャンプをしない。
TypeScriptコードの実装
以下が完成した JumpController.ts の全コードです。
import {
_decorator,
Component,
Node,
RigidBody2D,
Vec2,
PhysicsSystem2D,
EPhysics2DDrawFlags,
ERaycast2DType,
Contact2DType,
IPhysics2DContact,
Collider2D,
input,
Input,
EventKeyboard,
EventTouch,
EventMouse,
KeyCode,
Vec3,
} from 'cc';
const { ccclass, property } = _decorator;
/**
* JumpController
* - Rigidbody2D を持つノードにアタッチするだけで、
* キー入力/タッチ入力に応じてジャンプさせる汎用コンポーネント。
* - 接地判定は PhysicsSystem2D のレイキャストで行う。
*/
@ccclass('JumpController')
export class JumpController extends Component {
@property({
type: KeyCode,
tooltip: 'ジャンプに使用するキーボードキー。\n例: SPACE, W, UP_ARROW など。',
})
public jumpKey: KeyCode = KeyCode.SPACE;
@property({
tooltip: 'true の場合、画面タップ/クリックでもジャンプします。'
})
public enableTouchJump: boolean = true;
@property({
tooltip: 'ジャンプ時に与える上方向の速度(positive で上方向)。\nRigidbody2D の LinearVelocity.y にこの値を設定します。',
})
public jumpVelocity: number = 10;
@property({
tooltip: 'true の場合、ジャンプ時に X 方向の速度を 0 にリセットします。\n横移動スクリプトと併用する場合は false 推奨。'
})
public overrideHorizontalVelocity: boolean = false;
@property({
tooltip: '接地判定に使用するレイキャスト距離(ノード位置から下方向)。\nキャラクターの足元のサイズに合わせて調整してください。',
min: 0.01,
})
public groundCheckDistance: number = 0.2;
@property({
tooltip: '接地判定に使用するレイキャストの開始位置のオフセット(Y)。\n負の値でノードの中心より下からレイを飛ばします。',
})
public groundCheckOffsetY: number = -0.5;
@property({
tooltip: '床として扱う 2D 物理レイヤーのビットマスク。\n例: レイヤー1を床にしたい場合は 1 << 1 = 2 を指定。',
})
public groundLayerMask: number = 0xffffffff; // デフォルトは全レイヤー
@property({
tooltip: 'true の場合、空中でもジャンプ可能になります(多段ジャンプ)。',
})
public allowAirJump: boolean = false;
@property({
tooltip: '許可する空中ジャンプの回数(通常のジャンプは含まない)。\n例: 1 で二段ジャンプ、2 で三段ジャンプ。',
min: 0,
})
public maxAirJumpCount: number = 1;
@property({
tooltip: 'ジャンプのクールタイム(秒)。\n0 でクールタイムなし。',
min: 0,
})
public jumpCooldown: number = 0.0;
@property({
tooltip: 'true の場合、ボタンを押した瞬間だけジャンプします。\n押しっぱなしでは連続ジャンプしません。',
})
public usePressOnce: boolean = true;
@property({
tooltip: 'デバッグ用にレイキャストと接地状態をログ表示します。\n開発中のみ true 推奨。',
})
public debugLog: boolean = false;
private _rigidbody: RigidBody2D | null = null;
private _isGrounded: boolean = false;
private _airJumpCount: number = 0;
private _lastJumpTime: number = -999;
private _jumpKeyPressed: boolean = false;
onLoad() {
// 必須コンポーネントの取得
this._rigidbody = this.getComponent(RigidBody2D);
if (!this._rigidbody) {
console.error(
'[JumpController] このコンポーネントを使用するには、同じノードに RigidBody2D コンポーネントが必要です。',
this.node.name
);
}
// 入力イベント登録
input.on(Input.EventType.KEY_DOWN, this._onKeyDown, this);
input.on(Input.EventType.KEY_UP, this._onKeyUp, this);
if (this.enableTouchJump) {
input.on(Input.EventType.TOUCH_START, this._onTouchStart, this);
input.on(Input.EventType.MOUSE_DOWN, this._onMouseDown, this);
}
// 2D物理のデバッグ描画(必要に応じて有効化)
// PhysicsSystem2D.instance.debugDrawFlags = EPhysics2DDrawFlags.All;
}
start() {
// 開始時に接地状態を初期チェック
this._updateGroundedState();
this._resetAirJumpCountIfGrounded();
}
onDestroy() {
// 入力イベント解除
input.off(Input.EventType.KEY_DOWN, this._onKeyDown, this);
input.off(Input.EventType.KEY_UP, this._onKeyUp, this);
if (this.enableTouchJump) {
input.off(Input.EventType.TOUCH_START, this._onTouchStart, this);
input.off(Input.EventType.MOUSE_DOWN, this._onMouseDown, this);
}
}
update(dt: number) {
if (!this._rigidbody) {
return;
}
// 毎フレーム接地判定を更新
this._updateGroundedState();
this._resetAirJumpCountIfGrounded();
// 入力に応じてジャンプ試行
this._handleJumpInput();
}
//======================
// 入力処理
//======================
private _onKeyDown(event: EventKeyboard) {
if (event.keyCode === this.jumpKey) {
if (this.usePressOnce) {
this._jumpKeyPressed = true;
} else {
// 押しっぱなしでもジャンプを許可する場合は、ここで直接ジャンプ処理を呼ぶ
this._jumpKeyPressed = true;
}
}
}
private _onKeyUp(event: EventKeyboard) {
if (event.keyCode === this.jumpKey) {
this._jumpKeyPressed = false;
}
}
private _onTouchStart(event: EventTouch) {
if (!this.enableTouchJump) return;
this._jumpKeyPressed = true;
}
private _onMouseDown(event: EventMouse) {
if (!this.enableTouchJump) return;
this._jumpKeyPressed = true;
}
private _consumeJumpPress(): boolean {
if (!this._jumpKeyPressed) {
return false;
}
if (this.usePressOnce) {
// 一度処理したらフラグを落とす
this._jumpKeyPressed = false;
}
return true;
}
//======================
// ジャンプ処理
//======================
private _handleJumpInput() {
if (!this._rigidbody) return;
// 入力がなければ何もしない
if (!this._consumeJumpPress()) {
return;
}
const now = performance.now() / 1000; // 秒単位
if (this.jumpCooldown > 0) {
const elapsed = now - this._lastJumpTime;
if (elapsed < this.jumpCooldown) {
if (this.debugLog) {
console.log('[JumpController] クールタイム中のためジャンプ不可。残り秒数:', this.jumpCooldown - elapsed);
}
return;
}
}
// 接地中 or 空中ジャンプ許可チェック
if (this._isGrounded) {
this._doJump();
this._lastJumpTime = now;
this._airJumpCount = 0; // 地上からのジャンプでリセット
} else if (this.allowAirJump && this._airJumpCount < this.maxAirJumpCount) {
this._doJump();
this._lastJumpTime = now;
this._airJumpCount++;
if (this.debugLog) {
console.log('[JumpController] 空中ジャンプ:', this._airJumpCount, '/', this.maxAirJumpCount);
}
} else {
if (this.debugLog) {
console.log('[JumpController] ジャンプ不可(未接地 or 空中ジャンプ回数上限)。');
}
}
}
private _doJump() {
if (!this._rigidbody) return;
const currentVel = this._rigidbody.linearVelocity.clone();
let newVelX = currentVel.x;
if (this.overrideHorizontalVelocity) {
newVelX = 0;
}
const newVel = new Vec2(newVelX, this.jumpVelocity);
this._rigidbody.linearVelocity = newVel;
if (this.debugLog) {
console.log('[JumpController] ジャンプ実行。velocity:', newVel);
}
}
//======================
// 接地判定
//======================
private _updateGroundedState() {
const physics = PhysicsSystem2D.instance;
if (!physics) {
console.error('[JumpController] PhysicsSystem2D が有効になっていません。2D Physics を有効にしてください。');
this._isGrounded = false;
return;
}
const worldPos = this.node.worldPosition;
const start = new Vec2(worldPos.x, worldPos.y + this.groundCheckOffsetY);
const end = new Vec2(start.x, start.y - this.groundCheckDistance);
const results = physics.raycast(
start,
end,
ERaycast2DType.Closest,
this.groundLayerMask
);
const wasGrounded = this._isGrounded;
this._isGrounded = results.length > 0;
if (this.debugLog && wasGrounded !== this._isGrounded) {
console.log('[JumpController] 接地状態変更:', this._isGrounded, ' レイ結果数:', results.length);
}
}
private _resetAirJumpCountIfGrounded() {
if (this._isGrounded) {
if (this._airJumpCount !== 0) {
this._airJumpCount = 0;
if (this.debugLog) {
console.log('[JumpController] 接地により空中ジャンプ回数をリセット。');
}
}
}
}
}
コードの要点解説
- onLoad
RigidBody2Dを取得し、存在しない場合はconsole.errorを出力。input.onでキーボード・タッチ・マウスの入力イベントを登録。
- start
- 開始時に一度だけ接地判定を行い、空中ジャンプカウンタを初期化。
- update
- 毎フレーム
_updateGroundedState()でレイキャストによる接地判定。 _resetAirJumpCountIfGrounded()で地上に戻ったら空中ジャンプ回数をリセット。_handleJumpInput()で入力フラグに応じてジャンプ処理。
- 毎フレーム
- 入力処理
_onKeyDown,_onKeyUp,_onTouchStart,_onMouseDownで_jumpKeyPressedフラグを制御。usePressOnceがtrueの場合、押した瞬間だけ有効にするため、_consumeJumpPress()内でフラグを落とす。
- ジャンプ処理
_handleJumpInput()でクールタイム・接地状態・空中ジャンプ回数をチェック。- 条件を満たした場合
_doJump()を呼び、Rigidbody2D.linearVelocityにjumpVelocityを設定。 overrideHorizontalVelocityがtrueの場合は X 速度を 0 にリセット。
- 接地判定
PhysicsSystem2D.raycastを下方向に飛ばし、結果が 1 件以上あれば接地とみなす。groundCheckOffsetYとgroundCheckDistanceでレイの開始位置と長さを調整可能。groundLayerMaskで床レイヤーを制限(不要ならデフォルトの全レイヤーで OK)。
使用手順と動作確認
1. スクリプトファイルの作成
- エディタ左下の Assets パネルで、スクリプトを置きたいフォルダ(例:
assets/scripts)を選択します。 - フォルダ上で右クリック → Create → TypeScript を選択します。
- 新規作成されたファイル名を
JumpController.tsに変更します。 JumpController.tsをダブルクリックして開き、既存の中身をすべて削除して、前述のコードを貼り付けて保存します。
2. テスト用ノード(プレイヤー)の作成
ここでは 2D プロジェクトを想定します。
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、テスト用のプレイヤーノードを作成します。
- 作成されたノードの名前を
Playerなどに変更します。 - Inspector で
Playerノードを選択し、以下のコンポーネントを追加します:- Add Component → Physics 2D → RigidBody2D
- 必要に応じて Collider2D(例: BoxCollider2D)も追加し、形状をスプライトに合わせて調整します。
- RigidBody2D の設定例:
- Body Type: Dynamic
- Gravity Scale: 1(または好みで調整)
- 他の項目はデフォルトで問題ありません。
3. 床(Ground)ノードの作成
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、床用ノードを作成します。
- 名前を
Groundに変更し、Transform で位置とスケールを調整して足場になるようにします。 - Inspector で
Groundを選択し、以下を追加します:- Add Component → Physics 2D → RigidBody2D
- Add Component → Physics 2D → BoxCollider2D(など)
- Ground の RigidBody2D 設定:
- Body Type: Static(床は動かないので Static 推奨)
- 床用の 2D 物理レイヤーを分けたい場合:
- メニューから Project → Project Settings → Physics 2D → Layer Collision Matrix を開きます。
- 任意のレイヤー番号(例: 1 番)を
Ground用に使うと決め、Groundノードの Collider2D の Group をそのレイヤーに設定します。 - 後で
JumpController.groundLayerMaskを1 << レイヤー番号に設定して床だけを判定できるようにします。
4. JumpController のアタッチ
- Hierarchy で
Playerノードを選択します。 - Inspector の下部で Add Component → Custom → JumpController を選択します。
- これで
PlayerノードにJumpControllerがアタッチされます。
5. インスペクタでプロパティを設定
Player ノードの JumpController を選択し、以下のように設定します(例):
- jumpKey:
SPACE(デフォルトのままで OK) - enableTouchJump:
true(エディタ上でマウスクリックでもジャンプさせたい場合) - jumpVelocity:
10(重力やキャラサイズに応じて 8 ~ 15 の間で調整) - overrideHorizontalVelocity:
false(将来横移動を追加する前提なら false 推奨) - groundCheckDistance:
0.2 - groundCheckOffsetY:
-0.5(キャラの足元に合わせて微調整) - groundLayerMask:
- 床のレイヤーを特に分けていない場合:
0xffffffff(全レイヤー) - 床をレイヤー1にした場合:
2(=1 << 1)
- 床のレイヤーを特に分けていない場合:
- allowAirJump:
false(まずはシンプルに 1 段ジャンプから) - maxAirJumpCount:
1(allowAirJump を true にする場合は 1 で二段ジャンプ) - jumpCooldown:
0(まずはクールタイムなしで挙動確認) - usePressOnce:
true(押しっぱなしで連打されないように) - debugLog:
true(最初は true にしてコンソールログで接地判定を確認すると安心)
6. 動作確認
- 上部メニューから Project → Play(または再生ボタン)を押してゲームを実行します。
- ゲームビューで
Playerが床の上に乗っていることを確認します。 - キーボードの Space キーを押すと、
Playerが上方向にジャンプするはずです。 - 床に着地すると再び Space キーでジャンプできることを確認します。
- allowAirJump を
trueにして maxAirJumpCount を1にすると、空中でさらに 1 回ジャンプできる(二段ジャンプ)ことを確認しましょう。 - デバッグログを有効にしている場合、接地状態の切り替わりやジャンプ実行時に Console パネルにログが出力されます。
まとめ
本記事では、JumpController コンポーネントを実装し、
- Rigidbody2D を持つノードにアタッチするだけでジャンプ機能を追加できる
- 接地判定はレイキャストにより自己完結して行う
- キー/タッチ入力、ジャンプ力、空中ジャンプ、クールタイムなどをインスペクタから柔軟に調整できる
という構成を実現しました。
このコンポーネントは完全に独立しており、外部の GameManager やシングルトンに一切依存しません。
そのため、今後別のプロジェクトでも JumpController.ts を コピーしてアタッチするだけで、すぐにジャンプ機能を再利用できます。
応用例としては:
- 横移動用の
MoveControllerと組み合わせて 2D プラットフォーマーの基本移動を構築 - 空中ジャンプ回数を増やしてアクション性の高いゲームを実現
- 接地判定の距離やオフセットを調整して、キャラのサイズが異なる複数プレイヤーにそのまま使い回す
このような「インスペクタ完結・外部依存なし」の汎用コンポーネントを積み重ねていくことで、Cocos Creator プロジェクトの再利用性と保守性が大きく向上します。ぜひ自分のプロジェクト用にカスタマイズしつつ、共通ライブラリとして育ててみてください。
