In the last post, I showed a fix for the sticky landing bug that wrapped both gravity modifiers in an !isGrounded check. It solved the sticking issue but broke the high-vs-low jump mechanic. After analysis of the timing chain in Unity 6000.x, here is why it broke and how to fix both problems simultaneously.

What Went Wrong

The previous fix wrapped the entire gravity modifier block in a single guard:

// PREVIOUS FIX (broke high/low jump)
if (!isGrounded) {
    if (rb.linearVelocity.y < 0)
    {
        rb.linearVelocity += Vector2.up * Physics2D.gravity.y * (fallMultiplier - 1) * Time.deltaTime;
    }

    else if (rb.linearVelocity.y > 0 && !Input.GetKey(KeyCode.Space) ...)
        
            rb.linearVelocity += Vector2.up * Physics2D.gravity.y * (lowJumpMultiplier - 1) * Time.deltaTime;
}

The problem is timing. In Unity 6000.x, when the player leaves a surface (ground or ledge), OnCollisionExit2D fires and sets isGrounded = false. By the time the next Update() runs, !isGrounded is true and the block should execute. But inside that block, the low jump modifier checks for rb.linearVelocity.y > 0 && !Input.GetKey(KeyCode.Space) — meaning the player must be rising upward with a released jump key.

In practice, by the time Update() runs on the frame after leaving the ground, two things have typically already happened:

  1. isGrounded is false — passes the outer guard.
  2. The player's upward velocity has already been reduced by one frame of normal gravity since jumping. Depending on frame timing, it may still be positive or already negative.

If velocity.y is still positive (rising), the low jump cutoff fires and reduces height. If it has turned negative (already falling due to frame timing), the condition fails and the modifier is skipped entirely. Since this happens on every frame of ascent, the mechanic becomes unreliable — sometimes cutting jumps short, sometimes not, depending on how fast the physics step aligned with your input release.

The root cause was wrapping both modifiers behind a single !isGrounded gate. The fall modifier needed that guard to prevent sticky landings. The low jump modifier did not — it operates entirely in mid-air and is unaffected by ground contact state. They should have separate guards.

The Fix: Separate Guards

// FALL MODIFIER (only during freefall, skips on landing)
if (!isGrounded && rb.linearVelocity.y < 0)
{
    // Only apply fall modifier during freefall, not on landing
    // prevents extra downward force from fighting the ground collision solver
    rb.linearVelocity += Vector2.up * Physics2D.gravity.y * (fallMultiplier - 1) * Time.deltaTime;
}

// LOW JUMP MODIFIER (no ground check needed — operates only during ascent)
if (rb.linearVelocity.y > 0 && !Input.GetKey(KeyCode.Space) && !Input.GetKey(KeyCode.W) && !Input.GetKey(KeyCode.UpArrow))
    
    // Short jump cutoff: released the button while still rising — reduce upward velocity
    rb.linearVelocity += Vector2.up * Physics2D.gravity.y * (lowJumpMultiplier - 1) * Time.deltaTime;

Fall modifier: Still guarded by !isGrounded && rb.linearVelocity.y < 0. The landing frame has the player grounded (isGrounded = true), so the condition fails and fallMultiplier is skipped. Once airborne and falling, both conditions pass and fast-fall resumes.

Low jump modifier: No ground check. It only fires when velocity.y is positive (rising upward) AND the jump key has been released. Since this can only happen during ascent — not on the ground where velocity is clamped to zero by collision resolution — there is no risk of interference with landing physics. The mechanic now works consistently regardless of when isGrounded flips.

Why This Works

The two modifiers serve different purposes and exist in different parts of the jump lifecycle:

Wrapping them together with a single guard meant that whenever isGrounded was false (mid-air), both would be skipped if their respective conditions didn't pass. Separating them gives each the right environment to operate in: one gated for ground safety, one free to run during its narrow window.