Unityを触り始めると、つい Update() に「移動も」「ジャンプも」「アニメーションも」「UI更新も」全部書いてしまいがちですよね。最初は動くので満足できますが、あとから「ジャンプ処理だけ別の敵にも使いたい」「プレイヤーの挙動を少し変えたい」となったときに、巨大なスクリプトを前にして手が止まりがちです。

そこで今回は、「ジャンプだけ」をきれいに切り出したコンポーネント JumpController を作ってみましょう。
役割はシンプルで、「ジャンプボタン入力を検知し、親が床にいるなら上方向の velocity を与える」だけ。移動やアニメーションは別コンポーネントに任せて、ジャンプに関する責務だけを持たせる設計です。

【Unity】入力と物理をきれいに分離!「JumpController」コンポーネント

以下は Unity 6 / C# 向けの、そのままコピペで動かせる完全版コードです。
Rigidbody を使った 2D/3D 共通設計にしてあり、どちらのプロジェクトでも使えるようにしています。


using UnityEngine;
using UnityEngine.InputSystem; // 新Input Systemを利用

/// <summary>
/// 親オブジェクトにジャンプ機能を付与するコンポーネント。
/// - ジャンプボタン入力の検知(Input System)
/// - 接地判定(レイキャスト or オプションで外部から設定)
/// - 接地していれば、上方向にvelocityを与える
/// 
/// 「移動」「アニメーション」などは別コンポーネントに分離して、
/// このクラスは「ジャンプ」だけを責務とすることを想定しています。
/// </summary>
[DisallowMultipleComponent]
[RequireComponent(typeof(Rigidbody))]
public class JumpController : MonoBehaviour
{
    // ====== 設定パラメータ ======

    [Header("ジャンプ設定")]
    [SerializeField]
    [Tooltip("1回のジャンプで与える上方向速度(m/s)。")]
    private float jumpVelocity = 7.5f;

    [SerializeField]
    [Tooltip("多段ジャンプを許可する回数(1なら通常の1段ジャンプ)。")]
    private int maxJumpCount = 1;

    [SerializeField]
    [Tooltip("ジャンプボタンを押してから、この秒数だけは猶予を与える(いわゆるジャンプバッファ)。0で無効。")]
    private float jumpBufferTime = 0.1f;

    [Header("接地判定設定")]
    [SerializeField]
    [Tooltip("接地判定に使用するレイの原点(未指定ならTransform.positionを使用)。")]
    private Transform groundCheckOrigin;

    [SerializeField]
    [Tooltip("接地判定のレイ長。キャラクターの足元から少し下に伸ばすイメージ。")]
    private float groundCheckDistance = 0.2f;

    [SerializeField]
    [Tooltip("どのレイヤーを床として扱うか。")]
    private LayerMask groundLayer = ~0; // デフォルトは全レイヤー

    [SerializeField]
    [Tooltip("接地判定を2D物理で行うかどうか。trueならPhysics2D、falseならPhysicsを使用。")]
    private bool use2DPhysics = false;

    [Header("入力設定")]
    [SerializeField]
    [Tooltip("Input Actions のアクション名(例: \"Jump\")。空欄の場合は自動で \"Jump\" を探す。")]
    private string jumpActionName = "Jump";

    [SerializeField]
    [Tooltip("ジャンプ入力をInput Systemから受け取らず、外部スクリプトから呼び出す場合はfalseにする。")]
    private bool useInputSystem = true;

    // ====== 内部状態 ======

    private Rigidbody rb3D;          // 3D Rigidbody
    private Rigidbody2D rb2D;        // 2D Rigidbody
    private int currentJumpCount;    // 現在のジャンプ回数
    private bool isGrounded;         // 接地しているか
    private float lastJumpPressedTime = -999f; // 最後にジャンプボタンが押された時間

    // Input System関連
    private PlayerInput playerInput;
    private InputAction jumpAction;

    private void Awake()
    {
        // 2D / 3D どちらかのRigidbodyを取得
        if (use2DPhysics)
        {
            rb2D = GetComponent();
            if (rb2D == null)
            {
                Debug.LogError("[JumpController] use2DPhysics が true ですが、Rigidbody2D が見つかりません。");
            }
        }
        else
        {
            rb3D = GetComponent<Rigidbody>();
            if (rb3D == null)
            {
                Debug.LogError("[JumpController] Rigidbody が見つかりません。");
            }
        }

        // groundCheckOrigin が未設定なら、自分自身を使う
        if (groundCheckOrigin == null)
        {
            groundCheckOrigin = transform;
        }

        // Input System の設定
        if (useInputSystem)
        {
            playerInput = GetComponent<PlayerInput>();
            if (playerInput == null)
            {
                Debug.LogWarning("[JumpController] PlayerInput が見つかりません。Input Systemを使う場合は同じオブジェクトに追加してください。");
            }
            else
            {
                // アクション名が空なら "Jump" を探す
                if (string.IsNullOrEmpty(jumpActionName))
                {
                    jumpActionName = "Jump";
                }

                jumpAction = playerInput.actions.FindAction(jumpActionName);
                if (jumpAction == null)
                {
                    Debug.LogWarning($"[JumpController] アクション \"{jumpActionName}\" が見つかりません。Input Actions の設定を確認してください。");
                }
                else
                {
                    // performed(押された瞬間)を購読
                    jumpAction.performed += OnJumpPerformed;
                }
            }
        }
    }

    private void OnDestroy()
    {
        // イベント登録解除
        if (jumpAction != null)
        {
            jumpAction.performed -= OnJumpPerformed;
        }
    }

    private void Update()
    {
        // 接地判定を毎フレーム更新
        UpdateGroundedState();

        // 接地したらジャンプ回数をリセット
        if (isGrounded)
        {
            currentJumpCount = 0;
        }

        // ジャンプバッファが有効なら、入力から少し遅れてもジャンプを受け付ける
        if (Time.time - lastJumpPressedTime <= jumpBufferTime)
        {
            TryJump();
        }
    }

    /// <summary>
    /// Input System から呼ばれるコールバック。
    /// 「ジャンプボタンが押された瞬間」に実行される。
    /// </summary>
    /// <param name="context">入力コンテキスト</param>
    private void OnJumpPerformed(InputAction.CallbackContext context)
    {
        // ボタンが押された瞬間の時間を記録
        lastJumpPressedTime = Time.time;

        // バッファ時間が0の場合は即ジャンプを試みる
        if (jumpBufferTime <= 0f)
        {
            TryJump();
        }
    }

    /// <summary>
    /// 外部スクリプトからジャンプを要求したいときに呼び出すメソッド。
    /// 例: 独自の入力システムやAIからのジャンプ命令など。
    /// </summary>
    public void RequestJump()
    {
        lastJumpPressedTime = Time.time;

        if (jumpBufferTime <= 0f)
        {
            TryJump();
        }
    }

    /// <summary>
    /// 実際にジャンプ可能かチェックし、可能ならvelocityを上方向に与える。
    /// </summary>
    private void TryJump()
    {
        // すでに最大ジャンプ回数に達しているなら何もしない
        if (currentJumpCount >= maxJumpCount)
        {
            return;
        }

        // 1段目のジャンプは接地しているときだけ許可する
        if (currentJumpCount == 0 && !isGrounded)
        {
            // 1段目なのに空中ならジャンプしない(多段ジャンプを厳密に制御したい場合)
            return;
        }

        // 実際にジャンプさせる
        ApplyJumpVelocity();

        currentJumpCount++;
        // ジャンプ入力を消費
        lastJumpPressedTime = -999f;
    }

    /// <summary>
    /// Rigidbody に上方向の速度を与える。
    /// 既存のy速度を上書きすることで、安定したジャンプ高さにする。
    /// </summary>
    private void ApplyJumpVelocity()
    {
        if (use2DPhysics && rb2D != null)
        {
            Vector2 velocity = rb2D.velocity;
            // 上方向速度だけを上書き
            velocity.y = jumpVelocity;
            rb2D.velocity = velocity;
        }
        else if (!use2DPhysics && rb3D != null)
        {
            Vector3 velocity = rb3D.velocity;
            velocity.y = jumpVelocity;
            rb3D.velocity = velocity;
        }
    }

    /// <summary>
    /// レイキャストを使って接地しているかどうかを更新する。
    /// </summary>
    private void UpdateGroundedState()
    {
        Vector3 origin = groundCheckOrigin.position;
        Vector3 direction = Vector3.down;

        if (use2DPhysics)
        {
            RaycastHit2D hit = Physics2D.Raycast(origin, direction, groundCheckDistance, groundLayer);
#if UNITY_EDITOR
            Debug.DrawRay(origin, direction * groundCheckDistance, hit.collider ? Color.green : Color.red);
#endif
            isGrounded = hit.collider != null;
        }
        else
        {
            bool hit = Physics.Raycast(origin, direction, groundCheckDistance, groundLayer);
#if UNITY_EDITOR
            Debug.DrawRay(origin, direction * groundCheckDistance, hit ? Color.green : Color.red);
#endif
            isGrounded = hit;
        }
    }

    // ====== デバッグ用 Gizmos 表示 ======
#if UNITY_EDITOR
    private void OnDrawGizmosSelected()
    {
        if (groundCheckOrigin == null)
        {
            groundCheckOrigin = transform;
        }

        Gizmos.color = Color.yellow;
        Gizmos.DrawLine(groundCheckOrigin.position, groundCheckOrigin.position + Vector3.down * groundCheckDistance);
    }
#endif
}

使い方の手順

ここでは「プレイヤーキャラクターにジャンプ機能を付ける」例で説明します。2Dでも3Dでも流れはほぼ同じです。

  1. プレイヤー用のGameObjectを用意する
    例:
    • 3Dなら: Capsule + Rigidbody
    • 2Dなら: Sprite + Rigidbody2D

    RigidbodyConstraints で不要な回転を固定しておくと、ジャンプ中にキャラクターが倒れにくくなります。

  2. JumpController をアタッチする
    プレイヤーの GameObject に JumpController コンポーネントを追加します。
    2Dの場合は use2DPhysics にチェックを入れてください。
    • jumpVelocity: ジャンプの強さ(最初は 7〜10 くらいから調整)
    • maxJumpCount: 1 なら通常ジャンプ、2 なら2段ジャンプ
    • groundCheckDistance: キャラの足元から少し下(0.1〜0.3 くらい)
    • groundLayer: 「Ground」レイヤーなど、床に使っているレイヤーを指定
  3. Input System を設定する
    新しい Input System を使う前提です。
    • プロジェクト設定で Input System を有効にする
    • PlayerInput コンポーネントをプレイヤーに追加する
    • Input Actions アセットを作成し、Jump という名前のアクションを追加
    • Space キーやゲームパッドの A ボタンなどをバインド

    JumpController 側の jumpActionName"Jump" を設定しておけば、Space を押した瞬間にジャンプが発動します。

  4. 具体的な使用例
    • プレイヤーキャラ
      MovementController(左右移動)
      JumpController(ジャンプ)
      CharacterAnimator(アニメーション)
      のようにコンポーネントを分けてアタッチすると、役割が明確になり、後から差し替えもしやすくなります。
    • 敵キャラのジャンプ行動
      敵のAIスクリプトから JumpController.RequestJump() を呼ぶだけで、
      プレイヤーと同じジャンプ挙動を簡単に再利用できます。
    • 動く足場(トランポリン風)
      動く床側のスクリプトで、プレイヤーに触れたタイミングで
      other.GetComponent<JumpController>()?.RequestJump(); を呼べば、
      その場でジャンプさせる「ジャンプ台」のようなギミックも作れます。

メリットと応用

JumpController を使う最大のメリットは、「ジャンプの責務を1つのコンポーネントに閉じ込められる」ことです。

  • プレハブ管理が楽になる
    プレイヤーや敵のプレハブに JumpController を付けるだけで、同じジャンプ挙動を共有できます。
    「この敵はジャンプしないようにしたい」と思ったら、そのプレハブから JumpController を外すだけです。
  • レベルデザインの自由度が上がる
    ステージごとに jumpVelocitymaxJumpCount を変えたバリエーションプレハブを用意しておけば、「低重力ステージ」「2段ジャンプ解禁ステージ」などを簡単に作れます。
  • テストしやすい
    ジャンプだけを個別に調整・検証できるので、「この高さのブロックは届くか?」のようなバランス調整がやりやすくなります。

改造案として、例えば「ジャンプボタンを押し続けている間だけ、少しだけ上方向の力を追加して、長押しで高くジャンプさせる」といった機能を足すのも面白いですね。
下記はそのための簡単な補助メソッド例です(Update() から呼ぶ想定):


/// <summary>
/// ジャンプボタン長押しでジャンプを少し伸ばす簡易処理の例。
/// 例えば Update() 内から呼び出して、InputAction の押下状態を参照する。
/// </summary>
private void ApplyJumpHoldBoost(bool isJumpButtonHeld, float boostAcceleration, float maxExtraTime)
{
    // ボタンを離している、またはすでに上昇が終わっている場合は何もしない
    if (!isJumpButtonHeld)
    {
        return;
    }

    // 2D / 3D 両対応で上方向に少しだけ加速を与える
    if (use2DPhysics && rb2D != null)
    {
        if (rb2D.velocity.y > 0f)
        {
            rb2D.velocity += Vector2.up * (boostAcceleration * Time.deltaTime);
        }
    }
    else if (!use2DPhysics && rb3D != null)
    {
        if (rb3D.velocity.y > 0f)
        {
            rb3D.velocity += Vector3.up * (boostAcceleration * Time.deltaTime);
        }
    }
}

このように、ジャンプ機能を1つのコンポーネントにまとめておけば、「多段ジャンプ」「長押しジャンプ」「壁ジャンプ」などのバリエーションも、責務を崩さずに少しずつ拡張していけます。
巨大な God クラスではなく、小さなコンポーネントを積み重ねていく設計を意識していきましょう。