Skip to content

Jump buffering

const JUMP_BUFFER_TIME = 0.12  // seconds

state jump_buffer_timer = 0.0

function record_jump_buffer():
    if input.was_pressed("jump"):
        jump_buffer_timer = JUMP_BUFFER_TIME

function update_jump_buffer(dt):
    jump_buffer_timer = max(0.0, jump_buffer_timer - dt)

function try_consume_buffered_jump():
    if jump_buffer_timer > 0.0 and coyote_timer > 0.0:
        start_jump()             // from section 5
        jump_buffer_timer = 0.0  // consume — don't let it fire twice
        coyote_timer = 0.0

function update(dt):
    is_grounded = check_grounded()
    update_coyote_timer(dt)

    record_jump_buffer()
    update_jump_buffer(dt)
    try_consume_buffered_jump()

    handle_jump_release()
    apply_variable_gravity(dt)
    integrate_vertical(dt)

How it works

Jump buffering is coyote time's mirror image. Coyote Time forgives pressing jump slightly too late (after leaving the ground); jump buffering forgives pressing it slightly too early (before landing on the ground). Without it, a player who presses jump one frame before touchdown has that input evaluated while is_grounded is still false, and it's silently dropped — the character lands and just stands there, which reads as an unresponsive or "laggy" jump even though the game is behaving exactly as coded.

The fix is symmetrical to coyote time's: instead of checking input only in the instant it happens, record that it happened into a timer (jump_buffer_timer = JUMP_BUFFER_TIME), and let every subsequent frame within that window check "is there a buffered jump waiting, and am I now in a state where a jump is legal (grounded / coyote-eligible)?" The moment both are true, fire the jump and zero the buffer out immediately — leaving it non-zero would let a single early press cause a jump, then another jump the following frame if grounded state is still true, which is a real bug that shows up if you forget this line.

Ordering in the main loop matters: grounded/coyote state must be refreshed, then the buffer recorded and ticked, then consumption checked — all before gravity and integration run for the frame, so a successfully consumed buffered jump applies its velocity in the same frame it becomes eligible rather than one frame late.