Skip to content

Jump gravity and coyote time

const GRAVITY        = 1200.0  // units/sec^2
const MAX_FALL_SPEED = 900.0   // units/sec, terminal velocity
const JUMP_VELOCITY  = 480.0   // units/sec, upward impulse magnitude
const COYOTE_TIME    = 0.10    // seconds

state is_grounded    = false
state coyote_timer   = 0.0

function check_grounded():
    // Shapecast/raycast a small distance downward from the collider's feet.
    return cast_down(distance = 2.0) != null

function update_coyote_timer(dt):
    if is_grounded:
        coyote_timer = COYOTE_TIME
    else:
        coyote_timer = max(0.0, coyote_timer - dt)

function handle_jump_input():
    if input.was_pressed("jump") and coyote_timer > 0.0:
        velocity.y = -JUMP_VELOCITY
        coyote_timer = 0.0      // consume it — no double jumping off the same fall
        is_grounded = false

function apply_gravity(dt):
    velocity.y = min(velocity.y + GRAVITY * dt, MAX_FALL_SPEED)

function integrate_vertical(dt):
    position.y += velocity.y * dt
    if resolve_collision(axis = "y"):
        if velocity.y > 0:
            is_grounded = true
        velocity.y = 0

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

    apply_horizontal_acceleration(dt)   // from section 1
    handle_jump_input()
    apply_gravity(dt)

    integrate_position(dt)              // horizontal, from section 1
    integrate_vertical(dt)

How it works

Gravity - apply_gravity runs every frame regardless of grounded state; the collision resolution in integrate_vertical is what actually zeroes it out and sets is_grounded back to true.

Coyote Time

The detail that matters most here is consuming the timer (coyote_timer = 0.0) the moment it's used. Without that, a player who jumps, then falls for COYOTE_TIME seconds without touching ground, could jump again off nothing, because the timer hasn't been explicitly invalidated — only decremented. Setting it to zero on use closes that hole and keeps coyote time feeling like forgiveness rather than a free extra jump.

Update-order matters too: grounded state and the coyote timer must be refreshed before you read jump input for the frame, otherwise you're checking against last frame's grounded state.