Unityを触り始めた頃って、つい何でもかんでも Update() の中に書いてしまいがちですよね。
移動処理、ジャンプ、アニメーション、当たり判定、UI更新…全部1つのスクリプトに押し込んでしまうと、

  • どこを直せばいいか分からない
  • 機能追加のたびに巨大な if 文が増えていく
  • シーンごと・プレハブごとに微妙に違う挙動を分岐で書き始めて地獄

といった「Godクラス」状態になりやすいです。

今回は「梯子(はしご)を登る」という、よくあるゲーム要素を
専用のコンポーネントとして切り出してみましょう。

プレイヤーの通常移動とは別に、

  • 梯子エリアに入ったら重力をオフ
  • 上下入力だけを受け付けてY軸のみ移動
  • 梯子エリアを出たら重力をオンに戻す

といった挙動を、LadderClimber コンポーネントで完結させていきます。


【Unity】梯子エリアでだけ重力オフ&縦移動!「LadderClimber」コンポーネント

ここでは、Rigidbody を使った2D/3D共通で扱いやすい、シンプルな梯子昇降コンポーネントを紹介します。
キャラクター本体には通常の移動コンポーネント(横移動やジャンプなど)を付けたまま、
梯子に入っている間だけこの LadderClimber が「縦移動モード」を担当するイメージです。


フルコード:LadderClimber.cs


using UnityEngine;
using UnityEngine.InputSystem; // 新Input Systemを利用する場合

/// <summary>
/// 梯子エリア内では重力を無効化し、
/// 上下入力によるY軸移動のみを行うコンポーネント。
/// 
/// ・Rigidbody 必須
/// ・梯子側には "Ladder" タグ付きの Trigger Collider を用意
/// </summary>
[RequireComponent(typeof(Rigidbody))]
public class LadderClimber : MonoBehaviour
{
    [Header("移動設定")]
    [SerializeField] private float climbSpeed = 3f; // 梯子を登る速度(上下共通)

    [Header("入力設定")]
    [SerializeField] private bool useNewInputSystem = true;
    [SerializeField] private string verticalAxisName = "Vertical"; 
    // 旧Input Manager使用時の縦入力軸名(デフォルト: "Vertical")

    [Header("デバッグ表示")]
    [SerializeField] private bool showDebugLog = false;

    // 内部状態
    private Rigidbody rb;
    private bool isOnLadder = false;      // 現在梯子エリア内かどうか
    private float originalGravityScale;   // 元の重力スケール(2D風に扱うためYだけ固定)
    private Vector3 originalGravity;      // Rigidbodyの元の重力(3D物理向け)

    // 新InputSystem用の入力値
    private float verticalInput = 0f;

    private void Awake()
    {
        rb = GetComponent<Rigidbody>();

        // 物理演算の重力設定を保存しておく
        originalGravity = Physics.gravity;

        // Rigidbodyには「重力を使うか」のフラグしかないので、
        // 疑似的に重力スケールを扱うため、自分で値を保持しておくイメージ
        originalGravityScale = 1f;
    }

    private void OnEnable()
    {
        // 新InputSystemを使う場合は、PlayerInputなどから
        // このコンポーネントの OnMove(InputAction.CallbackContext) を呼ぶ構成がオススメです。
        // ここでは「自前でActionを持つ」まではやらず、フック用の関数だけ用意しています。
    }

    private void Update()
    {
        // 梯子に乗っていないときは何もしない
        if (!isOnLadder) return;

        // 入力の取得
        float inputY = 0f;

        if (useNewInputSystem)
        {
            // 新Input Systemの場合:
            // PlayerInputからイベントで受け取った値をそのまま使う
            inputY = verticalInput;
        }
        else
        {
            // 旧Input Managerの場合:
            // Project Settings > Input Manager の "Vertical" などから取得
            inputY = Input.GetAxisRaw(verticalAxisName);
        }

        // 梯子上では、Y方向の速度のみを制御する
        Vector3 velocity = rb.velocity;
        velocity.y = inputY * climbSpeed;
        rb.velocity = velocity;
    }

    /// <summary>
    /// 新Input System用の入力ハンドラ。
    /// PlayerInput の Action (例: "Move") に
    /// このメソッドをイベントとして登録して使う想定。
    /// 
    /// 例:
    /// - PlayerInput > Events > Move(performed/canceled) に
    ///   LadderClimber.OnMove をアサイン
    /// </summary>
    /// <param name="context">入力コンテキスト</param>
    public void OnMove(InputAction.CallbackContext context)
    {
        // 上下方向のみを取り出す(Vector2/Vector3どちらでもOK)
        Vector2 value = context.ReadValue<Vector2>();
        verticalInput = value.y;

        if (showDebugLog)
        {
            Debug.Log($"[LadderClimber] OnMove verticalInput = {verticalInput}");
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        // 梯子エリアに入ったかどうかをタグで判定
        if (!other.CompareTag("Ladder")) return;

        EnterLadder();
    }

    private void OnTriggerExit(Collider other)
    {
        // 梯子エリアから出たかどうかをタグで判定
        if (!other.CompareTag("Ladder")) return;

        ExitLadder();
    }

    /// <summary>
    /// 梯子エリアに入ったときの処理。
    /// 重力を無効化し、Y軸のみの移動モードにする。
    /// </summary>
    private void EnterLadder()
    {
        if (isOnLadder) return; // 二重呼び出し防止

        isOnLadder = true;

        // 現在の速度を少し抑える(急な落下を止めるため)
        Vector3 v = rb.velocity;
        v.x = 0f; // 横移動を止める(必要なければコメントアウト)
        rb.velocity = v;

        // 重力をオフ
        rb.useGravity = false;

        // 物理エンジン全体の重力は変更せず、個別のRigidbodyのみに適用
        Physics.gravity = Vector3.zero;

        if (showDebugLog)
        {
            Debug.Log("[LadderClimber] Enter Ladder: Gravity OFF");
        }
    }

    /// <summary>
    /// 梯子エリアから出たときの処理。
    /// 重力を元に戻し、通常の移動モードに戻す。
    /// </summary>
    private void ExitLadder()
    {
        if (!isOnLadder) return;

        isOnLadder = false;

        // 重力をオンに戻す
        rb.useGravity = true;

        // グローバル重力を元に戻す
        Physics.gravity = originalGravity * originalGravityScale;

        // 梯子から降りた直後に、少しだけ下向きの速度を与えて
        // ふわっと浮かないようにするのもアリ
        Vector3 v = rb.velocity;
        if (v.y > 0f) v.y = 0f;
        rb.velocity = v;

        if (showDebugLog)
        {
            Debug.Log("[LadderClimber] Exit Ladder: Gravity ON");
        }
    }

    /// <summary>
    /// 外部から「強制的に梯子モードを終了させたい」とき用のAPI。
    /// 例: ダメージを受けたら梯子から落とす、など。
    /// </summary>
    public void ForceExitLadder()
    {
        ExitLadder();
    }
}

ポイント

  • [RequireComponent(typeof(Rigidbody))] で、Rigidbody 付け忘れを防止
  • [SerializeField] private でインスペクタから調整可能&カプセル化
  • OnTriggerEnter/Exit で「Ladder」タグのコライダーに入っているかどうかを判定
  • 梯子上では Y速度だけ を上書きし、X/Zは他のコンポーネントに任せる方針

使い方の手順

① プレイヤーキャラクターの準備

  • プレイヤーの GameObject に以下をアタッチしておきます。
    • Rigidbody(Use Gravity: ON)
    • 通常の移動スクリプト(任意)
    • LadderClimber(今回のスクリプト)
  • プレイヤーに Collider(例: CapsuleCollider)を付け、
    「Is Trigger」はオフ(通常の当たり判定用)にしておきます。

② 梯子エリアの作成

  • シーン上に空の GameObject を作成し、名前を LadderArea などにする。
  • そこに BoxCollider などのコライダーを追加し、Is Trigger を ON にする。
  • この GameObject の Tag を “Ladder” に設定する。
    • Tag に “Ladder” がなければ、インスペクタ上部の Tag プルダウンから「Add Tag…」で追加。
  • コライダーのサイズを、梯子の範囲(プレイヤーが登ってほしい領域)に合わせて調整する。

これで、プレイヤーが LadderArea の Trigger に入ると LadderClimber が反応します。

③ 入力の設定(新Input System or 旧Input Manager)

新Input Systemを使う場合(推奨)
  1. プロジェクトに Input System パッケージを導入し、PlayerInput を使える状態にしておきます。
  2. プレイヤーの GameObject に PlayerInput コンポーネントを追加。
  3. Input Actions アセット内に「Move」などのアクション(タイプ: Value, Control Type: Vector2)を作成。
  4. PlayerInput の Events モードで、
    • Move (performed)
    • Move (canceled)

    LadderClimberOnMove 関数をアサインします。

  5. LadderClimber のインスペクタで Use New Input System にチェックを入れる。
旧Input Managerを使う場合
  1. Project Settings > Input Manager で “Vertical” 軸が設定されていることを確認。
  2. LadderClimber のインスペクタで
    • Use New Input System のチェックを外す
    • Vertical Axis Name に “Vertical” を指定(デフォルトのままでOK)

④ 実際の使用例

  • プレイヤーキャラクター
    横移動は別コンポーネント(例: CharacterMover)に任せつつ、
    梯子に入ったときだけ LadderClimber がY軸移動を担当します。
    これにより、プレイヤーの移動ロジックを「地上」「空中」「梯子」と分離しやすくなります。
  • 敵キャラクター
    敵にも LadderClimber を付けておけば、
    パスファインディングやAI側から OnMove を直接呼ぶことで、
    敵が梯子を登ってくるような挙動も簡単に作れます。
  • 動く足場+梯子
    エレベーターのような動く足場に梯子エリアを一緒に乗せておけば、
    プレイヤーは動きながら梯子を登ることもできます。
    梯子エリアは単なる Trigger Collider なので、足場ごと動かしてもOKです。

メリットと応用

LadderClimber をコンポーネントとして分離しておくと、

  • 「梯子に入ったときだけ重力オフ+縦移動」という責務が明確で、コードが読みやすい
  • プレイヤーと敵、NPCなど、どのキャラにも同じ挙動を簡単に付け回せる
  • レベルデザイナーは「Ladder」タグ付きの Trigger を置くだけで梯子を追加できる
  • 梯子の高さや位置を変えても、スクリプト側は一切変更不要

といったメリットがあります。
プレハブ管理の観点でも、

  • プレイヤープレハブに LadderClimber を1つ付けておくだけ
  • 梯子は「LadderArea」プレハブを量産してシーンにポンポン置くだけ

という構成にできるので、シーン追加やレベルデザインのスピードがかなり上がります。

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

例えば、「梯子の一番上まで登ったら自動で梯子モードを解除したい」という場合、
Update() の中でY座標をチェックして ForceExitLadder() を呼ぶような拡張ができます。


/// <summary>
/// 梯子の上下端に到達したら自動的に梯子モードを終了する例。
/// ladderTopY / ladderBottomY はインスペクタで設定しておく。
/// </summary>
[SerializeField] private float ladderTopY = 5f;
[SerializeField] private float ladderBottomY = 0f;

private void LateUpdate()
{
    if (!isOnLadder) return;

    float y = transform.position.y;

    // 上端を超えたら梯子モード終了
    if (y >= ladderTopY)
    {
        ForceExitLadder();
    }
    // 下端より下に行ったら梯子モード終了
    else if (y <= ladderBottomY)
    {
        ForceExitLadder();
    }
}

このように、小さな責務ごとにコンポーネントを分けておくと、
「梯子の端で降りる」「途中でジャンプで飛び降りる」「特定の梯子だけ速度を変える」などの
拡張がしやすくなります。
ぜひ、自分のプロジェクトに合わせて LadderClimber を育ててみてください。