Skip to content

Variable jump height

5. Variable jump height

const JUMP_VELOCITY_FULL  = 480.0  // max height if held to apex
const JUMP_CUT_MULTIPLIER = 0.45   // fraction of remaining upward velocity kept on early release
const MIN_JUMP_TIME       = 0.08   // seconds jump is guaranteed uncuttable, prevents zero-height taps
const RISE_GRAVITY_SCALE  = 1.0    // gravity while moving up
const FALL_GRAVITY_SCALE  = 1.6    // gravity while falling — snappier descent than ascent

state jump_time_elapsed = 0.0
state jump_held         = false

function start_jump():
    velocity.y = -JUMP_VELOCITY_FULL
    jump_time_elapsed = 0.0
    jump_held = true

function handle_jump_release():
    if input.was_released("jump") and jump_held:
        jump_held = false
        if jump_time_elapsed >= MIN_JUMP_TIME and velocity.y < 0:
            // Cut the jump short: shrink remaining upward velocity instead of
            // zeroing it outright, so the cut still reads as a smooth arc.
            velocity.y *= JUMP_CUT_MULTIPLIER

function apply_variable_gravity(dt):
    jump_time_elapsed += dt
    scale = RISE_GRAVITY_SCALE if velocity.y < 0 else FALL_GRAVITY_SCALE
    velocity.y = min(velocity.y + GRAVITY * scale * dt, MAX_FALL_SPEED)

function update(dt):
    if input.was_pressed("jump") and coyote_timer > 0.0:
        start_jump()
        coyote_timer = 0.0

    handle_jump_release()
    apply_variable_gravity(dt)
    integrate_vertical(dt)

How it works

The robust, industry-standard technique for variable jump height is cut-on-release, not "add force every frame the button is held." Every jump starts with the same full-height impulse (JUMP_VELOCITY_FULL); if the button is released while the character is still moving upward, remaining upward velocity is multiplied down by JUMP_CUT_MULTIPLIER rather than zeroed. Multiplying (not clamping to a flat value) keeps the deceleration looking like part of a continuous arc instead of an obvious snap.

Two guardrails make this feel good instead of janky:

  • MIN_JUMP_TIME — without it, a very quick tap-and-release (common — controllers and human reaction time aren't instant) can register the release before even a full frame of upward motion, producing a jump that barely leaves the ground. Cutting is only allowed after this minimum window, guaranteeing every jump — even a 1-frame tap — reaches some sane minimum height.
  • Checking velocity.y < 0 before cutting — if the player releases jump after the character has already started falling (apex passed), there's nothing to cut; applying the multiplier to downward velocity would make releasing jump make you fall slower, which is backwards.

RISE_GRAVITY_SCALE vs FALL_GRAVITY_SCALE is a separate, complementary knob, not required for variable height but almost always paired with it: using stronger gravity on the way down than the way up makes jumps feel snappy and game-y (a fast, weighty landing) rather than symmetric and floaty, independent of whatever height the player chose by how long they held the button.

An alternative, less-recommended approach some tutorials use is "hold to sustain low gravity while ascending, up to a max hold time," which continuously reduces gravity while the button stays down. It produces a similar height range but is more sensitive to frame-timing (input polled every frame rather than once on release) and tends to make the jump feel mushy/floaty rather than crisp — cut-on-release is generally the better default.