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 < 0before 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.