
Contents7
Your jump feels broken because Godot's physics don't forgive timing errors — these two mechanics fix that in 20 lines of GDScript.
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.

The Recipe
| Parameter | Starting value | Practical range | What it changes |
|---|---|---|---|
coyote_time | 0.10 s (6 frames) | 0.06–0.15 s | Grace window after leaving edge |
jump_buffer_time | 0.08 s (5 frames) | 0.05–0.12 s | Pre-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.

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.

Tuning Tips & Common Mistakes
Tips
- Lower
coyote_timefor precision games (0.06 s, ~4 frames). Too much grace feels like air-walking. - Keep
jump_buffer_timeslightly shorter thancoyote_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_timerafter a jump → double jump from thin air on the next platform edge. - Not zeroing
_jump_buffer_timerafter 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.

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."

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.

Tuning Reference
If you're unsure where to land, here's a decision table:
| Game style | Coyote time | Jump buffer |
|---|---|---|
| Tight precision (Super Meat Boy) | 0.06 s | 0.05 s |
| Action platformer (Hollow Knight) | 0.10 s | 0.08 s |
| Casual / beginner-friendly (Mario) | 0.13 s | 0.10 s |

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.


