Unityを触り始めた頃、ジャンプ処理を全部 Update() に書いてしまいがちですよね。

  • ボタン入力の取得
  • 接地判定
  • ジャンプ力の計算
  • アニメーションの切り替え

これらを1つの巨大なスクリプトに押し込んでしまうと、

  • ジャンプの仕様を少し変えるだけで他の処理が壊れる
  • 「先行入力」「ジャンプ猶予」などの細かい調整が地獄
  • プレイヤー以外(敵やギミック)に使い回しづらい

といった問題が出てきます。

そこでこの記事では、「ジャンプ処理」の中でもよくあるゲーム的テクニックである「先行入力(ジャンプバッファ)」だけを切り出したコンポーネント JumpBuffer を作っていきます。

このコンポーネントは、

  • 着地する直前にジャンプボタンを押しても
  • 着地した瞬間に自動でジャンプしてくれる

という、遊び心地を底上げしてくれる小さな部品です。プレイヤーキャラだけでなく、敵AIや動く床のジャンプギミックなどにも流用しやすい形で実装していきましょう。

【Unity】着地前に押してもジャンプが通る!「JumpBuffer」コンポーネント

今回のコンポーネントは「ジャンプの先行入力」のみを担当し、

  • 「ジャンプしたいかどうか」を bool で提供する
  • 実際のジャンプ物理(Rigidbody への力の付与など)は別コンポーネントに任せる

という責務分離を徹底します。これによって、

  • プレイヤーの移動コンポーネント
  • AI のジャンプ制御コンポーネント

などから共通して利用できるようになります。

JumpBuffer コンポーネントのフルコード


using UnityEngine;
using UnityEngine.InputSystem; // 新Input Systemを使う場合

/// <summary>
/// ジャンプの「先行入力(ジャンプバッファ)」を扱うコンポーネント。
/// - 一定時間内に押されたジャンプボタンを記録しておき、
///   着地した瞬間にジャンプ要求を成立させる役割を持つ。
/// - 実際のジャンプ挙動(Rigidbody に力を加えるなど)は別コンポーネントに任せる。
/// </summary>
public class JumpBuffer : MonoBehaviour
{
    // --- 設定項目 ---

    [Header("先行入力の受付時間(秒)")]
    [Tooltip("着地の何秒前までに押されたジャンプ入力を有効にするか。0.1〜0.2秒程度が目安。")]
    [SerializeField] private float bufferTime = 0.15f;

    [Header("地上判定の設定")]
    [Tooltip("現在地上にいるかどうかを外部から教えてもらうか、内部でRaycast判定するかを選ぶ。")]
    [SerializeField] private bool useExternalGroundFlag = true;

    [Tooltip("useExternalGroundFlag=false のときに使う、足元の原点。未設定なら transform.position を使用。")]
    [SerializeField] private Transform groundCheckOrigin;

    [Tooltip("地面までのRaycast距離。キャラクターの足元から少し下に伸ばすイメージ。")]
    [SerializeField] private float groundCheckDistance = 0.2f;

    [Tooltip("地面とみなすレイヤー。プレイヤーの足場となるレイヤーを指定。")]
    [SerializeField] private LayerMask groundLayer = ~0;

    // --- 状態管理用 ---

    // 最後にジャンプ入力が行われた時刻(Time.time)
    private float lastJumpPressedTime = -999f;

    // 直近の地上状態
    private bool isGrounded = false;

    // 前フレームの地上状態(着地瞬間を検出するため)
    private bool wasGrounded = false;

    // 外部から渡される地上フラグ(useExternalGroundFlag=true のときに利用)
    private bool externalGroundedFlag = false;

    // このフレームで「バッファからジャンプ成立」が起きたかどうか
    private bool consumedBufferedJumpThisFrame = false;

    // --- 公開プロパティ ---

    /// <summary>
    /// このフレームで「ジャンプすべきかどうか」を返す。
    /// - 入力が押された瞬間(空中でも)true
    /// - 着地した瞬間に、バッファ内の入力があれば true
    /// - それ以外は false
    /// 呼び出し側は、このフレームで一度だけ参照してジャンプ処理を行う想定。
    /// </summary>
    public bool ShouldJumpThisFrame
    {
        get { return consumedBufferedJumpThisFrame; }
    }

    /// <summary>
    /// 現在の地上状態。地上判定の方式に応じて値が変わる。
    /// </summary>
    public bool IsGrounded
    {
        get { return isGrounded; }
    }

    // --- Unity イベント ---

    private void Update()
    {
        // 1. 地上状態の更新
        UpdateGroundedState();

        // 2. このフレームの「ジャンプすべきか」を初期化
        consumedBufferedJumpThisFrame = false;

        // 3. 着地瞬間を検出
        bool justLanded = !wasGrounded && isGrounded;

        // 4. 着地瞬間に、バッファ内に有効なジャンプ入力があればジャンプ成立
        if (justLanded && IsBufferedJumpValid())
        {
            consumedBufferedJumpThisFrame = true;

            // 一度使った入力は無効化しておく(同じ入力で連続ジャンプしないように)
            lastJumpPressedTime = -999f;
        }

        // 5. 状態を次フレームに引き継ぎ
        wasGrounded = isGrounded;
    }

    // --- 公開メソッド ---

    /// <summary>
    /// 新Input System の InputAction から呼び出す想定のハンドラ。
    /// 「ジャンプボタンが押された瞬間」に呼び出してください。
    /// PlayerInput の Events から Jump アクションの Performed に紐付けると楽です。
    /// </summary>
    public void OnJumpInputPerformed(InputAction.CallbackContext context)
    {
        // ボタンが押された瞬間のみ受け付ける
        if (context.performed)
        {
            RegisterJumpPressed();
        }
    }

    /// <summary>
    /// 旧Input Manager(Input.GetButtonDown など)を使う場合に、
    /// Update から手動で呼び出す用のメソッド。
    /// &lt/summary>
    public void ManualCheckJumpInput(string buttonName = "Jump")
    {
        if (Input.GetButtonDown(buttonName))
        {
            RegisterJumpPressed();
        }
    }

    /// <summary>
    /// 外部から地上フラグを渡したい場合に使用。
    /// 例: 別コンポーネントで CharacterController.isGrounded を見ている場合など。
    /// useExternalGroundFlag=true のときのみ有効。
    /// </summary>
    public void SetExternalGrounded(bool grounded)
    {
        externalGroundedFlag = grounded;
    }

    // --- 内部処理 ---

    /// <summary>
    /// ジャンプボタンが押された瞬間を記録する。
    /// 空中かどうかは問わない(先行入力を許可するため)。
    /// </summary>
    private void RegisterJumpPressed()
    {
        lastJumpPressedTime = Time.time;

        // もしすでに地上にいるなら、このフレームですぐにジャンプしてもよい。
        // その場合は呼び出し側が「地上かつ ShouldJumpThisFrame または 即時ジャンプ入力」
        // といった条件で処理してもOK。
        if (isGrounded)
        {
            consumedBufferedJumpThisFrame = true;
        }
    }

    /// <summary>
    /// バッファ内のジャンプ入力がまだ有効かどうかを判定する。
    /// </summary>
    private bool IsBufferedJumpValid()
    {
        // 直近のジャンプ入力からの経過時間
        float elapsed = Time.time - lastJumpPressedTime;
        return elapsed >= 0f && elapsed <= bufferTime;
    }

    /// <summary>
    /// 地上判定の更新。
    /// - useExternalGroundFlag=true: 外部から渡されたフラグをそのまま使用
    /// - useExternalGroundFlag=false: 足元からのRaycastで地面を検出
    /// </summary>
    private void UpdateGroundedState()
    {
        if (useExternalGroundFlag)
        {
            isGrounded = externalGroundedFlag;
            return;
        }

        // Raycastによる簡易的な地上判定
        Vector3 origin = groundCheckOrigin != null
            ? groundCheckOrigin.position
            : transform.position;

        Ray ray = new Ray(origin, Vector3.down);
        isGrounded = Physics.Raycast(ray, groundCheckDistance, groundLayer);
    }

#if UNITY_EDITOR
    private void OnDrawGizmosSelected()
    {
        // エディタ上で地上判定用Rayを可視化(useExternalGroundFlag=false のときだけ)
        if (useExternalGroundFlag) return;

        Gizmos.color = Color.yellow;
        Vector3 origin = groundCheckOrigin != null
            ? groundCheckOrigin.position
            : transform.position;
        Vector3 end = origin + Vector3.down * groundCheckDistance;
        Gizmos.DrawLine(origin, end);
        Gizmos.DrawSphere(end, 0.02f);
    }
#endif
}

簡単なジャンプ実行コンポーネントの例

JumpBuffer は「いつジャンプすべきか」だけを教えてくれるので、実際のジャンプは別コンポーネントに分けましょう。Rigidbody を使ったシンプルな例を載せておきます。


using UnityEngine;

/// <summary>
/// Rigidbody を使って実際にジャンプさせるコンポーネント。
/// JumpBuffer と組み合わせて使うことを想定。
/// </summary>
[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(JumpBuffer))]
public class SimpleRigidbodyJumper : MonoBehaviour
{
    [Header("ジャンプ設定")]
    [Tooltip("ジャンプの初速度(上方向)。")]
    [SerializeField] private float jumpVelocity = 5f;

    [Tooltip("ジャンプ方向。通常は上方向 (0,1,0)。")]
    [SerializeField] private Vector3 jumpDirection = Vector3.up;

    private Rigidbody rb;
    private JumpBuffer jumpBuffer;

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

    private void Update()
    {
        // 旧Input Manager を使う場合はここで入力チェック
        // (新Input Systemの場合は JumpBuffer.OnJumpInputPerformed を使う)
        // jumpBuffer.ManualCheckJumpInput("Jump");

        // このフレームでジャンプすべきなら、Rigidbody に速度を与える
        if (jumpBuffer.ShouldJumpThisFrame && jumpBuffer.IsGrounded)
        {
            Vector3 vel = rb.velocity;

            // 垂直方向の速度だけ上書きして、横移動は維持する
            Vector3 dir = jumpDirection.normalized;
            vel.y = dir.y * jumpVelocity;

            rb.velocity = vel;
        }
    }
}

使い方の手順

  1. プレイヤーにコンポーネントを追加
    プレイヤーの GameObject(Rigidbody 付き)に以下をアタッチします。
    • JumpBuffer
    • SimpleRigidbodyJumper(または自作のジャンプコンポーネント)
  2. 地上判定の設定
    地上判定の方法は2通りあります。
    • 別コンポーネントで地上判定している場合(推奨)
      例: CharacterController ベースのプレイヤーで、すでに isGrounded を持っている場合。
      そのコンポーネントから毎フレーム jumpBuffer.SetExternalGrounded(controller.isGrounded); のように渡します。
      このとき useExternalGroundFlag = true にしておきましょう。
    • 簡易的に Raycast で地上判定する場合
      useExternalGroundFlag = false にし、groundCheckOrigin にキャラの足元の Transform を指定します。
      地面のレイヤーを groundLayer に設定し、groundCheckDistance をキャラの高さに合わせて調整します。
  3. 入力の紐付け
    新Input System を使うか、旧Input Manager を使うかで設定が変わります。
    • 新Input System の場合(PlayerInput 推奨)
      1. プレイヤーに PlayerInput コンポーネントを追加
      2. Input Actions アセットに「Jump」アクション(Button)を作成
      3. PlayerInput の Behavior を「Invoke Unity Events」に設定
      4. PlayerInput の「Jump」イベントの performedJumpBuffer.OnJumpInputPerformed を割り当て
    • 旧Input Manager の場合
      • SimpleRigidbodyJumperUpdate() でコメントアウトしている
        jumpBuffer.ManualCheckJumpInput("Jump"); を有効にする
      • Project Settings > Input Manager で「Jump」ボタンを設定しておく
  4. 動作確認と調整
    プレイして、以下を確認します。
    • 地上にいる状態でジャンプボタンを押すと普通にジャンプする
    • ジャンプの着地直前(ほんの少し早め)にジャンプボタンを押しても、着地した瞬間に再ジャンプする
    • bufferTime を変えると、「どれくらい早押ししても許されるか」が変わる

    例として、2Dアクションゲーム風のプレイヤーであれば bufferTime = 0.1〜0.2 秒くらいが扱いやすいですね。

具体的な使用例

  • プレイヤーキャラクター
    高速なアクションゲームでは、プレイヤーが「着地する瞬間」を正確に見てボタンを押すのはかなり難しいです。
    JumpBuffer を入れておくと、プレイヤーは「そろそろ着地しそうだな」というタイミングでジャンプボタンを連打するだけで、自然に二段ジャンプのようなテンポで動けるようになります。
  • 敵のジャンプAI
    敵が一定間隔でジャンプするような AI を作るときにも、JumpBuffer を使うと「着地前に次のジャンプタイミングが来た」ケースをうまく吸収できます。
    AI から RegisterJumpPressed() 相当のメソッドを呼ぶだけで、「次の着地でジャンプ」が自動的に成立します。
  • 動く床やジャンプ台ギミック
    プレイヤーが動く床に乗っているときに、床の側でジャンプタイミングを調整したい場合にも応用できます。
    床側のスクリプトで「プレイヤーをジャンプさせたいタイミング」を JumpBuffer に渡しておくと、プレイヤーの地上判定と連動して気持ちよく飛ばせます。

メリットと応用

JumpBuffer を使うことで、

  • 「ジャンプ入力の受付時間」という1つの責務に処理が分離される
  • プレハブのジャンプ挙動を、インスペクタ上の bufferTime だけで調整できる
  • プレイヤー以外の GameObject(敵、ギミック)にもそのまま流用できる

といったメリットがあります。

特にレベルデザインの観点では、

  • 「このステージはシビアにしたいから bufferTime=0.05
  • 「このステージは初心者向けだから bufferTime=0.2

のように、プレハブ単位で遊びやすさを調整できるのが嬉しいポイントですね。ジャンプの物理パラメータ(速度や重力)をいじらずに「遊び心地」だけを変えられるので、デザイナーとエンジニアのコミュニケーションも楽になります。

改造案:ジャンプボタン長押しで「優先度高い即時ジャンプ」を追加する

例えば、「地上にいるときはバッファよりも即時ジャンプを優先したい」「長押し中は常にジャンプを試みたい」といった仕様を足したい場合、JumpBuffer に次のようなメソッドを追加できます。


    /// <summary>
    /// 長押し中のジャンプ入力を処理する例。
    /// - 地上にいる場合は即時ジャンプ要求を立てる。
    /// - 空中の場合は通常どおりバッファに記録。
    /// 旧Input Manager の Input.GetButton("Jump") などから呼び出す想定。
    /// </summary>
    public void HandleJumpHoldInput(bool isHolding)
    {
        if (!isHolding) return;

        // ボタンが押されている間、毎フレーム「押された」とみなす
        RegisterJumpPressed();

        // もしすでに地上なら、このフレームで即座にジャンプ要求を立てる
        if (isGrounded)
        {
            consumedBufferedJumpThisFrame = true;
        }
    }

このように小さなコンポーネントとして切り出しておくと、「長押し」「二段ジャンプ」「壁ジャンプ」などの仕様追加も、別メソッドや別コンポーネントとして綺麗に積み上げていけます。God クラス化を避けつつ、遊び心地の良いアクションゲームを組み立てていきましょう。