【Cocos Creator 3.8】LadderClimber(梯子昇降)の実装:アタッチするだけで「梯子エリア内だけ上下移動+重力オフ」を実現する汎用スクリプト
2Dアクションゲームでよくある「梯子(はしご)を登る/降りる」挙動を、1つのコンポーネントをキャラクターノードにアタッチするだけで実現できるようにします。
このコンポーネントは、梯子エリアに入ったときだけ重力を無効化し、上下入力でY軸方向にのみ移動させます。
梯子エリア外では、通常どおり Rigidbody2D の重力・横移動などが働くため、既存のプレイヤー制御スクリプトとも共存しやすい設計です。
コンポーネントの設計方針
機能要件の整理
- このコンポーネントはキャラクター側(登る側)にアタッチする。
- 梯子は「Ladder」などの専用タグを持つコライダーとして用意し、Trigger(IsTrigger)で判定する。
- キャラクターが梯子トリガーに重なっている間のみ、「梯子モード(climbing)」に入る。
- 梯子モード中は:
- Rigidbody2D の重力スケールを 0 にして落下しないようにする。
- 上下入力(キーボードの Up/Down, W/S など)でY軸方向の速度のみを制御する。
- 横方向の速度は 0 に固定(または元の速度を抑制)する。
- 梯子トリガーから外れたら:
- 重力スケールを元に戻す。
- 梯子用のY方向速度制御をやめる。
- 外部の GameManager や入力管理スクリプトには一切依存しない。
外部依存をなくすためのアプローチ
- Rigidbody2D コンポーネントのみ必須とし、同じノード上から getComponent で取得する。
- 梯子判定は PhysicsSystem2D のコンタクトコールバックを使い、「梯子タグ(文字列)」で判定する。
- 入力は Input.on で直接キーボードを監視し、このコンポーネント内だけで完結させる。
- 元の重力スケールを記録しておき、梯子モード終了時に必ず元に戻す(防御的な実装)。
インスペクタで設定可能なプロパティ
このコンポーネントは、以下のようなプロパティを Inspector から調整できるようにします。
- climbSpeed: number
- ツールチップ: 「梯子を登る/降りる速度(単位: m/s)」
- 役割: 上下入力 1.0 に対する Rigidbody2D の Y 速度。
- 例: 3.0 にすると、上キーで +3 m/s、下キーで -3 m/s。
- ladderTag: string
- ツールチップ: 「梯子コライダーに設定するタグ名」
- 役割: このタグを持つ Collider2D の Trigger 内にいる間だけ梯子モードに入る。
- 例: “Ladder”
- useWASD: boolean
- ツールチップ: 「W/S キーでも梯子の上下操作を行う」
- 役割: true の場合、上下入力として W(上)/S(下)も受け付ける。
- stopHorizontalOnLadder: boolean
- ツールチップ: 「梯子中に横方向の速度を 0 にする」
- 役割: true の場合、梯子モード中は Rigidbody2D の X 速度を 0 に固定して、完全に縦移動のみとする。
- false にすると、他のスクリプトが付ける横移動を残すことも可能。
- snapToLadderX: boolean
- ツールチップ: 「梯子に入った瞬間、X 座標を梯子の中心に合わせる」
- 役割: true の場合、梯子エリアに入ったときにキャラクターの X を梯子コライダーの中心 X にスナップして、見た目をきれいにする。
- 2D アクションで多用される「梯子の真ん中に吸着」挙動。
※ いずれのプロパティも @property で公開されるため、プロジェクトごとに自由に調整できます。
TypeScriptコードの実装
import { _decorator, Component, Node, RigidBody2D, Vec2, Input, input, KeyCode, EventKeyboard, PhysicsSystem2D, Contact2DType, Collider2D, IPhysics2DContact, warn, director } from 'cc';
const { ccclass, property } = _decorator;
/**
* LadderClimber
*
* キャラクターノードにアタッチすると、
* ・指定タグの「梯子」トリガー内にいる間だけ重力を 0 にし、
* ・上下入力で Y 軸方向のみ移動させる汎用コンポーネント。
*/
@ccclass('LadderClimber')
export class LadderClimber extends Component {
@property({
tooltip: '梯子を登る/降りる速度(単位: m/s)。正の値で上、負の値で下方向に移動します。',
})
public climbSpeed: number = 3;
@property({
tooltip: '梯子コライダーに設定するタグ名。このタグを持つ Trigger 内にいる間だけ梯子モードになります。',
})
public ladderTag: string = 'Ladder';
@property({
tooltip: 'W/S キーでも梯子の上下操作を行うかどうか。',
})
public useWASD: boolean = true;
@property({
tooltip: 'true の場合、梯子中に Rigidbody2D の X 速度を常に 0 にして縦移動のみとします。',
})
public stopHorizontalOnLadder: boolean = true;
@property({
tooltip: 'true の場合、梯子に入った瞬間に X 座標を梯子コライダーの中心に合わせます。',
})
public snapToLadderX: boolean = true;
private _rb2d: RigidBody2D | null = null;
private _originalGravityScale: number = 1;
private _isOnLadder: boolean = false;
private _verticalInput: number = 0; // -1 ~ 1
// 現在重なっている梯子コライダー(複数重なりに備えて配列でもよいが、ここでは最後に入ったものを使用)
private _currentLadder: Collider2D | null = null;
onLoad() {
// Rigidbody2D を取得
this._rb2d = this.getComponent(RigidBody2D);
if (!this._rb2d) {
warn('[LadderClimber] このコンポーネントを使用するには、同じノードに RigidBody2D を追加してください。');
return;
}
this._originalGravityScale = this._rb2d.gravityScale;
// キーボード入力の監視を登録
input.on(Input.EventType.KEY_DOWN, this._onKeyDown, this);
input.on(Input.EventType.KEY_UP, this._onKeyUp, this);
// 物理接触コールバックの登録
const physics = PhysicsSystem2D.instance;
physics.on(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this);
physics.on(Contact2DType.END_CONTACT, this._onEndContact, this);
}
onDestroy() {
// 入力イベントの解除
input.off(Input.EventType.KEY_DOWN, this._onKeyDown, this);
input.off(Input.EventType.KEY_UP, this._onKeyUp, this);
// 物理接触コールバックの解除
const physics = PhysicsSystem2D.instance;
physics.off(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this);
physics.off(Contact2DType.END_CONTACT, this._onEndContact, this);
// 念のため、破棄時に重力を元に戻す
if (this._rb2d) {
this._rb2d.gravityScale = this._originalGravityScale;
}
}
/**
* キーが押されたときの処理
*/
private _onKeyDown(event: EventKeyboard) {
switch (event.keyCode) {
case KeyCode.ARROW_UP:
this._verticalInput = 1;
break;
case KeyCode.ARROW_DOWN:
this._verticalInput = -1;
break;
case KeyCode.KEY_W:
if (this.useWASD) {
this._verticalInput = 1;
}
break;
case KeyCode.KEY_S:
if (this.useWASD) {
this._verticalInput = -1;
}
break;
}
}
/**
* キーが離されたときの処理
*/
private _onKeyUp(event: EventKeyboard) {
switch (event.keyCode) {
case KeyCode.ARROW_UP:
case KeyCode.ARROW_DOWN:
// 上下キーが離されたら 0 に戻す(W/S と同時押しのケースは簡易的に無視)
this._verticalInput = 0;
break;
case KeyCode.KEY_W:
case KeyCode.KEY_S:
if (this.useWASD) {
this._verticalInput = 0;
}
break;
}
}
/**
* 2D 物理接触開始
* 梯子タグの Trigger に入ったら梯子モードに入る。
*/
private _onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
// selfCollider がこのノードのコライダーでない場合は無視
if (selfCollider.node !== this.node && otherCollider.node !== this.node) {
return;
}
// どちらが「自分」かを判定
let ladderCollider: Collider2D | null = null;
if (selfCollider.node === this.node) {
ladderCollider = otherCollider;
} else {
ladderCollider = selfCollider;
}
if (!ladderCollider) {
return;
}
// Trigger でなければ梯子とはみなさない
if (!ladderCollider.isTrigger) {
return;
}
// タグで梯子かどうか判定
if (ladderCollider.tag !== this.ladderTag) {
return;
}
// 梯子モード開始
this._enterLadder(ladderCollider);
}
/**
* 2D 物理接触終了
* 梯子タグの Trigger から出たら梯子モードを終了する。
*/
private _onEndContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
if (!this._isOnLadder) {
return;
}
// selfCollider がこのノードのコライダーでない場合は無視
if (selfCollider.node !== this.node && otherCollider.node !== this.node) {
return;
}
let ladderCollider: Collider2D | null = null;
if (selfCollider.node === this.node) {
ladderCollider = otherCollider;
} else {
ladderCollider = selfCollider;
}
if (!ladderCollider) {
return;
}
if (ladderCollider.tag !== this.ladderTag) {
return;
}
// 現在の梯子から離れた場合のみ終了
if (this._currentLadder === ladderCollider) {
this._exitLadder();
}
}
/**
* 梯子モードに入る処理
*/
private _enterLadder(ladderCollider: Collider2D) {
if (!this._rb2d) {
return;
}
this._isOnLadder = true;
this._currentLadder = ladderCollider;
// 重力を 0 にして落下しないようにする
this._originalGravityScale = this._rb2d.gravityScale;
this._rb2d.gravityScale = 0;
// 現在の速度を取得して、Y 方向は一旦 0 にする
const v = this._rb2d.linearVelocity;
this._rb2d.linearVelocity = new Vec2(this.stopHorizontalOnLadder ? 0 : v.x, 0);
// X 座標を梯子の中心にスナップ(オプション)
if (this.snapToLadderX) {
const ladderWorldPos = ladderCollider.node.worldPosition;
const myWorldPos = this.node.worldPosition;
// X のみ合わせる
this.node.setWorldPosition(ladderWorldPos.x, myWorldPos.y, myWorldPos.z);
}
}
/**
* 梯子モードから出る処理
*/
private _exitLadder() {
if (!this._rb2d) {
return;
}
this._isOnLadder = false;
this._currentLadder = null;
this._verticalInput = 0;
// 重力スケールを元に戻す
this._rb2d.gravityScale = this._originalGravityScale;
}
update(deltaTime: number) {
if (!this._rb2d) {
return;
}
if (!this._isOnLadder) {
return;
}
// 上下入力に応じて Y 速度を設定
const v = this._rb2d.linearVelocity;
const targetY = this._verticalInput * this.climbSpeed;
const targetX = this.stopHorizontalOnLadder ? 0 : v.x;
this._rb2d.linearVelocity = new Vec2(targetX, targetY);
}
}
コードの要点解説
- onLoad
- 同じノードから
RigidBody2Dを取得し、なければwarnを出して終了(防御的)。 Input.onでキーボード入力(上下キー + 任意で W/S)を監視。PhysicsSystem2Dに対してBEGIN_CONTACT / END_CONTACTを登録し、梯子トリガーへの侵入/離脱を検出。
- 同じノードから
- _onBeginContact / _onEndContact
- 接触したコライダーのうち、自分のノードでない方を「相手」として扱う。
- その相手が Trigger かつ、指定タグ(ladderTag)を持つ場合にのみ梯子として扱う。
- 侵入時は
_enterLadder、離脱時は_exitLadderを呼ぶ。
- _enterLadder
- 現在の重力スケールを保存し、gravityScale = 0 に設定。
- 現在の速度から Y を 0 にし、オプションで X も 0 にする(
stopHorizontalOnLadder)。 snapToLadderXが true の場合、梯子コライダーのノード中心 X にキャラクターの X を合わせる。
- _exitLadder
- 梯子モードフラグを false にし、入力値をリセット。
- 保存しておいた
_originalGravityScaleをRigidBody2D.gravityScaleに戻す。
- update
- 梯子モード中のみ動作。
_verticalInput(-1 ~ 1)にclimbSpeedを掛けた値を Y 速度として設定。- 横方向は
stopHorizontalOnLadderに応じて 0 または既存の X 速度を維持。
使用手順と動作確認
1. スクリプトファイルの作成
- Assets パネルで右クリック → Create → TypeScript を選択し、ファイル名を LadderClimber.ts にします。
- 自動生成されたコードをすべて削除し、本記事の
LadderClimberクラスのコードをそのまま貼り付けて保存します。
2. プレイヤーノード(キャラクター)の準備
- Hierarchy パネルで、プレイヤー用のノード(例: Player)を用意します。
- 2D スプライトの場合: 右クリック → Create → 2D Object → Sprite などで作成。
- Player ノードを選択し、Inspector で以下を追加します:
- Add Component → Physics 2D → RigidBody2D
- Add Component → Physics 2D → Collider2D(BoxCollider2D など)
- Collider2D の Is Trigger は OFF にしておきます(プレイヤーは通常の物理挙動をするため)。
- RigidBody2D の Body Type は Dynamic を推奨します。
3. LadderClimber コンポーネントのアタッチ
- 再び Player ノードを選択し、Inspector で:
- Add Component → Custom → LadderClimber を選択します。
- LadderClimber のプロパティを設定します:
- Climb Speed: 例として 3 と入力。
- Ladder Tag: デフォルトの Ladder のままで構いません。
- Use WASD: W/S でも操作したい場合は ON。
- Stop Horizontal On Ladder: 梯子中は横移動を完全に止めたい場合は ON(推奨)。
- Snap To Ladder X: 梯子の中心に吸着させたい場合は ON(推奨)。
4. 梯子ノードの作成
梯子側は「Trigger の Collider2D + 指定タグ」で表現します。
- Hierarchy パネルで右クリック → Create → 2D Object → Node を選択し、名前を Ladder にします。
- Ladder ノードを選択し、Inspector で:
- Add Component → Physics 2D → BoxCollider2D を追加します。
- BoxCollider2D の設定:
- Is Trigger にチェックを入れて ON にします。
- Size を調整して、プレイヤーが重なれる縦長のエリアにします(例: Width=0.5, Height=4)。
- Tag フィールドに Ladder と入力します(LadderClimber の
ladderTagと一致させる)。
- 見た目のために、Ladder ノードにスプライトを付けたい場合:
- Add Component → Render → Sprite を追加し、梯子の画像を割り当てます。
- Sprite のサイズと BoxCollider2D のサイズを合わせると分かりやすいです。
5. 物理 2D の有効化確認
Cocos Creator 3.8 プロジェクトで Physics2D を使うには、Project Settings で 2D 物理が有効になっている必要があります。
- メニューから Project → Project Settings を開きます。
- Feature Cropping または Module(バージョンによって名称が異なる場合あり)で、Physics 2D が有効になっていることを確認します。
6. シーンでの配置とテスト
- Scene ビューで、Ladder ノードを Player の近くに配置し、プレイヤーがジャンプや移動で重なれる位置に調整します。
- 再生ボタン(▶)でゲームをプレビューします。
- プレイヤーを梯子の下あたりに移動させ(既存の移動スクリプトや仮の操作でOK)、Ladder の Trigger エリアに入るようにします。
- プレイヤーが梯子エリアに入った状態で:
- 上キー(↑)または W を押す → プレイヤーが 上方向に一定速度で移動する。
- 下キー(↓)または S を押す → プレイヤーが 下方向に一定速度で移動する。
- キーを離す → プレイヤーがその場で停止し、重力で落下しない(梯子モード中は gravityScale=0)。
- プレイヤーが梯子エリアから完全に出ると:
- 重力スケールが 元の値 に戻り、通常の落下挙動に復帰します。
- 横移動スクリプトがある場合も、再び通常どおり動作します。
7. よくあるつまずきポイント
- プレイヤーが梯子に入っても何も起きない場合:
- Player に RigidBody2D が付いているか確認。
- Ladder の BoxCollider2D が Is Trigger=ON になっているか確認。
- Ladder の Tag が LadderClimber の ladderTag と一致しているか確認(例: “Ladder”)。
- Project Settings で Physics 2D が有効になっているか確認。
- 梯子中に横移動してしまう場合:
- LadderClimber の Stop Horizontal On Ladder が ON になっているか確認。
- 梯子の真ん中に吸着しない場合:
- LadderClimber の Snap To Ladder X が ON になっているか確認。
- Ladder ノードの位置(Transform)が想定どおりか確認。
まとめ
この LadderClimber コンポーネントは、キャラクター側にアタッチするだけで、梯子エリア内の重力オフ+上下移動を完結させる汎用スクリプトです。
外部の GameManager や入力管理クラスに依存せず、必要な設定はすべて Inspector のプロパティから行えるため、どのプロジェクトにもそのまま持ち込んで再利用できます。
応用例として:
- 梯子だけでなく、「ロープ」「ツタ」「エレベーターシャフト」など、縦方向専用の移動エリアに名前だけ変えて使う。
- climbSpeed を状況に応じて変更し、「アイテム取得で梯子移動速度アップ」などの演出に使う。
- ladderTag を複数用意して、「通常梯子」「高速梯子」「水中梯子」などをタグで切り替える。
ゲーム内の「登る/降りる」処理をこの 1 コンポーネントに閉じ込めておくことで、プレイヤー制御のメインスクリプトをシンプルに保ちながら、拡張性の高い梯子システムを構築できます。
そのままコピペで導入し、必要に応じてプロパティを調整しながら、自分のゲームに合った梯子挙動を作り込んでみてください。
