Game Feel · Blog
Hands holding a game controller, fingers on the buttons
Photo: Kevin Malik / Pexels
Contents7
  1. Why It Works
  2. The Recipe
  3. The Code
  4. Tuning Tips & Common Mistakes
  5. Famous Examples
  6. Tuning Reference
  7. FAQ

Your jump feels broken because Godot's physics don't forgive timing errors — these two mechanics fix that in 20 lines of GDScript.

100 msCeleste coyote time (6 frames)feelframes.dev, 2024
67 msCeleste jump buffer (4 frames)feelframes.dev, 2024
8 framesMario Odyssey coyote timefeelframes.dev, 2024

Why It Works

Human reaction time for visual input is ~150–200 ms. At 60 fps, a platform edge lasts one pixel — no one can time that consistently. Forgiveness mechanics absorb the gap between intention and execution.

  • Coyote time catches too late — the player pressed jump just after the edge.
  • Jump buffering catches too early — the player pressed jump just before landing.

Together, they make the player feel skilled, not lucky.

Pixel art platformer level with floating platforms
Pixel art platformer level with floating platforms — Photo: Steve A Johnson / Pexels

The Recipe

ParameterStarting valuePractical rangeWhat it changes
coyote_time0.10 s (6 frames)0.06–0.15 sGrace window after leaving edge
jump_buffer_time0.08 s (5 frames)0.05–0.12 sPre-landing jump acceptance

Start with these exact values. Tight precision games go lower; casual or mobile games go higher. Adjust in 10 ms steps and playtest after each change.

Game controller with colorful face buttons close up
Game controller with colorful face buttons close up — Photo: Ömer Yılmaz / Pexels

The Code

Attach to your CharacterBody2D. Integrate into your existing _physics_process, or drop it in as a standalone starting point.

extends CharacterBody2D

const GRAVITY: float = 980.0
const JUMP_VELOCITY: float = -400.0
const SPEED: float = 200.0

@export var coyote_time: float = 0.10       # seconds — Celeste uses 0.10 s
@export var jump_buffer_time: float = 0.08  # seconds — slightly shorter than coyote

var _coyote_timer: float = 0.0
var _jump_buffer_timer: float = 0.0

func _physics_process(delta: float) -> void:
    # 1. Gravity — only applied when airborne
    if not is_on_floor():
        velocity.y += GRAVITY * delta

    # 2. Coyote timer — resets on floor, counts down in the air
    if is_on_floor():
        _coyote_timer = coyote_time
    else:
        _coyote_timer -= delta

    # 3. Jump buffer — records a jump press for a short window
    if Input.is_action_just_pressed("ui_accept"):
        _jump_buffer_timer = jump_buffer_time
    else:
        _jump_buffer_timer -= delta

    # 4. Jump — fires if the buffer holds a pending press AND coyote is valid
    if _jump_buffer_timer > 0.0 and _coyote_timer > 0.0:
        velocity.y = JUMP_VELOCITY
        _coyote_timer = 0.0       # consume coyote — prevents aerial double-jump
        _jump_buffer_timer = 0.0  # consume buffer — prevents unintended queued jump

    # 5. Horizontal movement
    var direction: float = Input.get_axis("ui_left", "ui_right")
    velocity.x = direction * SPEED

    move_and_slide()

The key insight: because _coyote_timer resets every frame while on the floor, the single check _coyote_timer > 0.0 handles both "currently grounded" and "just left the ground." No separate is_on_floor() check needed at the jump site.

Developer coding on screen showing game engine editor
Developer coding on screen showing game engine editor — Photo: ThisIsEngineering / Pexels

Tuning Tips & Common Mistakes

Tips

  • Lower coyote_time for precision games (0.06 s, ~4 frames). Too much grace feels like air-walking.
  • Keep jump_buffer_time slightly shorter than coyote_time — it feels tighter and more intentional.
  • Export both variables so you can tweak them from the Inspector without recompiling.

Common mistakes

  • Not zeroing _coyote_timer after a jump → double jump from thin air on the next platform edge.
  • Not zeroing _jump_buffer_timer after a jump → queued input fires again on the next landing, surprising the player.
  • Calling is_on_floor() directly at the jump check instead of using the timer → you throw away all forgiveness.
Game design sketches and notes on paper
Game design sketches and notes on paper — Photo: picjumbo.com / Pexels

Famous Examples

Maddy Thorson (Celeste co-creator) published a thread in March 2020 listing Celeste's hidden forgiveness mechanics. Coyote time is item #1 — JumpGraceTime = 0.1f in the source, measured at exactly 6 frames by feelframes.dev. Thorson's note: "I don't think we invented any of these."

Coyote time window by game (ms at 60 fps)
Mario Odyssey
ms133
Celeste
ms100
Hollow Knight
ms67
Super Meat Boy
ms50
feelframes.dev, 2024
Retro arcade cabinet glowing in a dark room
Retro arcade cabinet glowing in a dark room — Photo: Corey Dupree / Pexels
Jump buffer window by game (ms at 60 fps)
Mario Odyssey
ms100
Celeste
ms67
Hollow Knight
ms50
feelframes.dev, 2024

Mario Odyssey's 133 ms suits its casual, exploration-first design. Celeste's tighter 100 ms is deliberate — the game is hard, but precision is fair. Super Meat Boy's ~50 ms demands real mastery. GMTK's Behind the Code devlog includes both mechanics as essential defaults in the Platformer Toolkit.

Indie game developer working at laptop in dark studio
Indie game developer working at laptop in dark studio — Photo: Cord Allman / Pexels

Tuning Reference

If you're unsure where to land, here's a decision table:

Game styleCoyote timeJump buffer
Tight precision (Super Meat Boy)0.06 s0.05 s
Action platformer (Hollow Knight)0.10 s0.08 s
Casual / beginner-friendly (Mario)0.13 s0.10 s
Video game character jumping platforms pixel art illustration
Video game character jumping platforms pixel art illustration — Photo: Sami Aksu / Pexels

FAQ

Does coyote time break wall jumps or double jumps?

It can. The fix is already in the code above: zero _coyote_timer immediately after any jump — ground, wall, or double. Without this, a wall jump might immediately consume coyote time and award an extra jump.

Should I use Timer nodes instead of float timers?

Float timers in _physics_process are simpler, deterministic, and add no scene tree nodes. Use Timer nodes only if you need signals (e.g., to trigger a sound or animation exactly when the grace period ends).

My jump still feels floaty — what else should I add?

Add variable jump height: when the player releases the jump button early, multiply gravity by 2.0–3.0 to cut the arc short. This gives snappy, controllable jump peaks and is the next natural step after coyote time.

Does this conflict with animation state machines?

No. The timers are movement logic only — they don't touch animation. Connect your animation by checking velocity.y and is_on_floor() in a separate function.

Does this work with Godot 4.3 or 4.4?

Yes. CharacterBody2D, is_on_floor(), and move_and_slide() have been stable across Godot 4.x. No migration required.


Add coyote time and jump buffer once and you stop hearing "the controls feel unresponsive." For the full toolkit — screen shake, hit stop, squash & stretch — visit the Game Feel Cookbook.