【Godot 4】LadderClimber (梯子昇降) コンポーネントの作り方

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

Godot 4 でキャラの移動を作っていると、CharacterBody2D のスクリプトがどんどん肥大化しがちですよね。歩く、ジャンプする、ダッシュする、壁ジャンプする… そして「梯子(はしご)での昇り降り」まで同じスクリプトに詰め込むと、_physics_process() が if 文だらけになってカオスになりがちです。

さらに、階段や動く床、泳ぎ(スイミング)など別の移動モードを追加したくなると、「プレイヤー継承ツリー」を増やしたり、「状態ごとの巨大ステートマシン」を書いたりして、保守がつらくなります。

そこで今回は、梯子の挙動だけをきれいに「コンポーネント化」する LadderClimber を用意しました。
プレイヤー本体には「通常の移動処理」だけを書いておき、梯子に入ったときだけこのコンポーネントが重力オフ+Y軸移動を肩代わりしてくれる構成です。

【Godot 4】重力オフでスイスイ昇降!「LadderClimber」コンポーネント

このコンポーネントはざっくり言うと:

  • 指定した「プレイヤー(CharacterBody2D)」を対象にする
  • 指定した「梯子エリア(Area2D)」に入っている間だけ有効になる
  • 有効な間は
    • 重力を無効化(外部の移動スクリプトから書き換えられても毎フレームで打ち消す)
    • 上下入力で Y 軸方向だけに移動速度を与える
    • 左右入力は無視(もしくはオプションで許可)

つまり「はしごにいる間だけ、移動ロジックを乗っ取る」コンポーネントです。
プレイヤー側のスクリプトは「通常移動」だけに集中できるので、かなりスッキリします。


フルコード:LadderClimber.gd


extends Node
class_name LadderClimber
"""
LadderClimber コンポーネント

- 梯子用 Area2D にアタッチすることを想定。
- 対象の CharacterBody2D(プレイヤーなど)を指定しておくと、
  そのキャラが梯子エリア内にいる間だけ、重力を打ち消して
  上下入力による Y 軸移動を行う。

想定するプレイヤー側の前提:
- velocity プロパティを持つ CharacterBody2D(Godot 標準の CharacterBody2D)を想定
- 通常時の移動はプレイヤー自身のスクリプトで行う
- 「梯子中かどうか」はこのコンポーネントが管理する
"""

@export_category("基本設定")
## 対象となるキャラクター(通常はプレイヤー)
@export var target_body: CharacterBody2D

## 梯子の範囲を表す Area2D
## 通常はこのコンポーネントと同じノードにアタッチしておく
@export var ladder_area: Area2D

## 上下方向の移動速度(ピクセル/秒)
@export var climb_speed: float = 120.0

## 梯子にいる間、X 方向の速度を完全にゼロにするかどうか
## false の場合、「横入力で梯子から降りる」などの応用も可能
@export var lock_x_velocity: bool = true

@export_category("入力設定")
## 上昇に使うアクション名(InputMap で定義しておく)
@export var action_up: StringName = &"ui_up"

## 下降に使うアクション名
@export var action_down: StringName = &"ui_down"

## 梯子から降りる(キャンセル)に使うアクション名(任意)
## 空文字列のままなら無効
@export var action_leave_ladder: StringName = &""

@export_category("高度な設定")
## 梯子に入った瞬間に、Y 速度をゼロにリセットするかどうか
@export var reset_vertical_velocity_on_enter: bool = true

## 梯子にいる間、重力を完全に打ち消すかどうか
## false にすると「ゆっくり落下しながら登る」などの特殊挙動も作れる
@export var cancel_gravity: bool = true

## 梯子にいる間だけ、target_body の "is_on_ladder" という変数を true/false で書き換える
## プレイヤー側スクリプトで状態判定に使いたい場合に便利
@export var write_flag_to_target: bool = true
@export var flag_name_in_target: StringName = &"is_on_ladder"

## デバッグ表示用:現在誰かがこの梯子を使っているか
var _is_body_on_ladder: bool = false

## 内部状態:今このコンポーネントが制御している対象
var _current_body: CharacterBody2D = null

func _ready() -> void:
    # ladder_area が未設定なら、自分の親か自分自身を自動検出してみる
    if ladder_area == null:
        if self is Area2D:
            ladder_area = self
        else:
            # 親が Area2D ならそれを使う
            if get_parent() is Area2D:
                ladder_area = get_parent()
    
    if ladder_area == null:
        push_warning("LadderClimber: ladder_area が設定されていません。梯子の Area2D を export で指定してください。")
        return
    
    # Area2D のシグナルに接続して、出入りを検知する
    ladder_area.body_entered.connect(_on_body_entered)
    ladder_area.body_exited.connect(_on_body_exited)


func _physics_process(delta: float) -> void:
    if _current_body == null:
        return
    
    # 対象が queue_free されていた場合の保険
    if not is_instance_valid(_current_body):
        _clear_current_body()
        return
    
    # 対象が CharacterBody2D でなければ何もしない
    if not (_current_body is CharacterBody2D):
        return
    
    var body := _current_body
    var v := body.velocity
    
    # 梯子から降りる入力があれば、制御を解除する
    if action_leave_ladder != &"" and Input.is_action_just_pressed(action_leave_ladder):
        _leave_ladder()
        return
    
    # 重力を打ち消す(プレイヤー側が毎フレーム重力加算していても、ここでゼロにする)
    if cancel_gravity:
        v.y = 0.0
    
    # X 速度を固定する場合
    if lock_x_velocity:
        v.x = 0.0
    
    # 入力から上下方向の速度を決定
    var input_y := 0.0
    if Input.is_action_pressed(action_up):
        input_y -= 1.0
    if Input.is_action_pressed(action_down):
        input_y += 1.0
    
    v.y = input_y * climb_speed
    
    body.velocity = v
    body.move_and_slide()


func _on_body_entered(body: Node) -> void:
    # 対象が CharacterBody2D でなければ無視
    if not (body is CharacterBody2D):
        return
    
    # target_body が指定されている場合は、それと一致するものだけを対象にする
    if target_body != null and body != target_body:
        return
    
    # すでに別のボディを制御している場合はスキップ(単純化のため)
    if _current_body != null and _current_body != body:
        return
    
    _current_body = body
    _is_body_on_ladder = true
    
    # Y 速度をリセットしたい場合
    if reset_vertical_velocity_on_enter:
        var v := _current_body.velocity
        v.y = 0.0
        _current_body.velocity = v
    
    # プレイヤー側にフラグを書き込む
    if write_flag_to_target:
        _set_flag_in_target(_current_body, true)


func _on_body_exited(body: Node) -> void:
    if body == _current_body:
        _leave_ladder()


func _leave_ladder() -> void:
    if _current_body == null:
        return
    
    # フラグを戻す
    if write_flag_to_target:
        _set_flag_in_target(_current_body, false)
    
    _clear_current_body()


func _clear_current_body() -> void:
    _current_body = null
    _is_body_on_ladder = false


func _set_flag_in_target(body: Node, value: bool) -> void:
    # body に flag_name_in_target というメンバがあれば書き込む
    # ない場合は何もしない(エラーにはしない)
    if body.has_variable(flag_name_in_target):
        body.set(flag_name_in_target, value)

使い方の手順

ここでは典型的な 2D プラットフォーマーを想定して、プレイヤーが梯子を登る例で説明します。

手順①:プレイヤー側のノード構成

まずはシンプルなプレイヤーシーンを用意します。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── PlayerController (スクリプトのみ or Node)

プレイヤーのスクリプトは、通常の移動だけを書いておきます。
例として、最低限の移動スクリプトはこんな感じです(梯子ロジックは書かない):


# Player.gd (例)
extends CharacterBody2D

@export var move_speed := 200.0
@export var gravity := 900.0
@export var jump_speed := -350.0

var is_on_ladder: bool = false  # LadderClimber から書き込まれるフラグ

func _physics_process(delta: float) -> void:
    # 梯子にいる間は、ここでの重力・移動は基本的に無視されるが、
    # 「梯子中は移動を止めたい」などのときにフラグで分岐できる
    if not is_on_ladder:
        # 通常の重力
        if not is_on_floor():
            velocity.y += gravity * delta
        
        # 左右移動
        var input_x := Input.get_axis("ui_left", "ui_right")
        velocity.x = input_x * move_speed
        
        # ジャンプ
        if is_on_floor() and Input.is_action_just_pressed("ui_accept"):
            velocity.y = jump_speed
    
    move_and_slide()

ポイントは is_on_ladder フラグだけです。
このフラグは LadderClimber コンポーネントが自動で true/false を書き換えてくれます。

手順②:梯子シーンのノード構成

次に梯子用のシーンを作ります。

Ladder (Node2D)
 ├── Sprite2D                # 梯子の見た目
 ├── Area2D                  # 梯子判定用
 │    └── CollisionShape2D   # 梯子の当たり判定(矩形など)
 └── LadderClimber (Node)    # ← このコンポーネントをアタッチ

実際のシーン構成図の例:

Ladder (Node2D)
 ├── Sprite2D
 ├── LadderArea (Area2D)
 │    └── CollisionShape2D
 └── LadderClimber (Node)

Inspector 上で LadderClimber の export 変数を設定します:

  • ladder_areaLadderArea (Area2D) をドラッグ&ドロップ
  • target_body … プレイヤーシーンを直接指定してもいいですし、空のままにして「誰でも登れる梯子」にしても OK
  • climb_speed … 120〜200 あたりから調整
  • action_up / action_down … デフォルトの ui_up / ui_down でよければそのまま
  • action_leave_ladder … 例えば "ui_cancel""jump" を設定すると、「ボタンで梯子から降りる」挙動が作れます

InputMap には最低限以下を定義しておきましょう:

  • ui_up(例: ↑キー / W)
  • ui_down(例: ↓キー / S)
  • ui_left, ui_right(プレイヤー移動で使用)
  • ui_accept(ジャンプ用など)

手順③:シーンに配置してテスト

メインシーンにプレイヤーと梯子シーンを配置します。

Main (Node2D)
 ├── Player (CharacterBody2D)
 ├── Ladder (Node2D)
 └── TileMap / その他

ゲームを再生すると:

  • プレイヤーが梯子エリア(LadderArea)に入ると、LadderClimber がプレイヤーを「捕まえる」
  • その間は
    • 重力がキャンセルされて落ちなくなる
    • ui_up / ui_down で上下にだけ移動する
    • 左右速度はゼロに固定(設定で変更可)
    • is_on_ladder フラグが true になる
  • 梯子エリアから出る、または action_leave_ladder を押すと、制御を解除して通常の移動に戻る

プレイヤー側のスクリプトは「梯子中かどうか」を is_on_ladder で知ることができるので、例えば:

  • 梯子中はジャンプ無効にする
  • 梯子中は攻撃モーションに制限をかける
  • アニメーションを「登りアニメ」に切り替える

といった制御が簡単に書けます。

手順④:敵や動く足場にも使い回す

コンポーネント指向の良いところは、「プレイヤー専用コード」にならないことです。
target_body を空のままにしておけば、どの CharacterBody2D でも梯子に入れば登れるようになります。

例えば:

Enemy (CharacterBody2D)
 ├── Sprite2D
 └── EnemyAI.gd

この敵が AI の都合で梯子に登りたい場合も、Enemy をそのまま梯子エリアに突っ込めば、LadderClimber が同じように Y 軸だけ移動させてくれます。
「敵用の梯子登りロジック」を別に書く必要はありません。


メリットと応用

この LadderClimber コンポーネントを使うことで:

  • プレイヤーのスクリプトがスリムになる
    梯子ロジックを巨大な if is_on_ladder: ブロックで抱え込む必要がなくなります。
  • シーン構造がフラットで見通しが良い
    「梯子専用プレイヤーシーン」を継承で増やすのではなく、梯子側にコンポーネントを生やすだけで済みます。
  • 再利用性が高い
    プレイヤーでも敵でも、動く足場に乗った NPC でも、同じコンポーネントを使い回せます。
  • レベルデザインが楽
    レベルデザイナーは「梯子シーンをポンと置く」だけで、ゲーム中のどのキャラでも登れるようになります。

「継承より合成(Composition)」の考え方とも相性が良くて、
「移動系の特殊挙動」はどんどんコンポーネントに分割していくのがおすすめです。

改造案:梯子の上端で自動的に降りる

例えば、「梯子の一番上まで登ったら自動で梯子を離れる」挙動を足したい場合、
LadderClimber に次のような補助関数を追加することができます:


func _auto_leave_when_above_top(top_y: float) -> void:
    """
    body の Y 座標が top_y より上に来たら、自動で梯子を離れる。
    top_y はワールド座標で指定する想定。
    例えば梯子の一番上のタイルの Y などを渡す。
    """
    if _current_body == null:
        return
    if _current_body.global_position.y < top_y:
        _leave_ladder()

これを _physics_process() の末尾あたりで呼び出して:


func _physics_process(delta: float) -> void:
    if _current_body == null:
        return
    # ... 既存の処理 ...

    body.move_and_slide()

    # 例: この梯子の一番上の Y を基準にする
    var top_y := ladder_area.global_position.y - ladder_area.shape_owner_get_shape(0, 0).get_extents().y
    _auto_leave_when_above_top(top_y)

のようにすれば、「一番上まで来たら自動で梯子を離れる」ような自然な挙動も作れます。
(実際には CollisionShape2D の種類によって shape の扱いが変わるので、プロジェクトに合わせて調整してください。)

このように、梯子のロジックはすべて LadderClimber 側に閉じ込めることで、
プレイヤーや敵のスクリプトは「歩く・走る・攻撃する」などの本質的なロジックだけに集中させることができます。
ぜひ自分のプロジェクト用にカスタマイズして、コンポーネント指向の快適さを体験してみてください。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!