A jump that ignores your button press by a few milliseconds feels broken, even when the code is technically correct. This guide shows how to add a Unity double jump, coyote time and jump buffering to a character so jumps feel fair. You get one small script for a 2D Rigidbody2D player and notes for 3D Rigidbody and CharacterController players, all wired to the Input System.
Why Jumps Feel Unfair
Players don’t press buttons on the exact frame you expect. Two timing problems cause most “I pressed jump!” complaints:
- Late presses. The player runs off a ledge and presses jump a fraction of a second after leaving it. Your ground check already says “not grounded”, so nothing happens. The fix is coyote time: a short grace period after leaving the ground where a jump still counts.
- Early presses. The player presses jump just before landing. The press happens while airborne, so it’s ignored, and they land without jumping. The fix is jump buffering: remember the press for a short time and fire the jump as soon as you land.
Both features are invisible to players. They don’t notice them, they just notice the game “feels tight”. Most well-loved platformers use both, with windows around 0.1 seconds.
If you don’t have basic movement yet, start with the 2D platformer movement guide and come back. This post assumes you already move left and right.
A Reliable Ground Check
Everything here depends on knowing whether the player is grounded. The most reliable simple option in 2D is a small overlap box under the feet, checked against a Ground layer:
bool IsGrounded()
{
Vector2 feet = (Vector2)transform.position + groundCheckOffset;
return Physics2D.OverlapBox(feet, groundCheckSize, 0f, groundLayer) != null;
}
Put your floors and platforms on a layer called Ground and assign it to groundLayer. Make the box a little narrower than the player’s collider so touching a wall doesn’t count as being grounded. If your project already uses layers heavily, the collision layers guide explains how to keep them organised.
Avoid checking velocity.y == 0. At the top of a jump arc velocity passes through zero, and on slopes it rarely is zero. You will get mid-air jumps and missed ground jumps.
The Full 2D Jump Script
This script handles coyote time, buffering, a configurable number of air jumps and variable jump height. It reads a Jump action from an InputActionReference, so it works with keyboard and gamepad.
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(Rigidbody2D))]
public class PlayerJump2D : MonoBehaviour
{
[SerializeField] InputActionReference jumpAction;
[SerializeField] float jumpVelocity = 14f;
[SerializeField] int airJumps = 1; // 1 = double jump
[SerializeField] float coyoteTime = 0.1f;
[SerializeField] float jumpBufferTime = 0.1f;
[SerializeField, Range(0f, 1f)] float jumpCutMultiplier = 0.5f;
[Header("Ground check")]
[SerializeField] LayerMask groundLayer;
[SerializeField] Vector2 groundCheckOffset = new Vector2(0f, -0.5f);
[SerializeField] Vector2 groundCheckSize = new Vector2(0.4f, 0.1f);
Rigidbody2D rb;
float coyoteTimer;
float bufferTimer;
int airJumpsLeft;
bool jumpReleased;
void Awake() => rb = GetComponent<Rigidbody2D>();
void OnEnable() => jumpAction.action.Enable();
void OnDisable() => jumpAction.action.Disable();
void Update()
{
bool grounded = IsGrounded();
if (grounded && rb.linearVelocity.y <= 0.01f)
{
coyoteTimer = coyoteTime;
airJumpsLeft = airJumps;
}
else
{
coyoteTimer -= Time.deltaTime;
}
if (jumpAction.action.WasPressedThisFrame())
bufferTimer = jumpBufferTime;
else
bufferTimer -= Time.deltaTime;
if (jumpAction.action.WasReleasedThisFrame())
jumpReleased = true;
if (bufferTimer > 0f)
{
if (coyoteTimer > 0f)
{
DoJump();
coyoteTimer = 0f;
}
else if (airJumpsLeft > 0)
{
DoJump();
airJumpsLeft--;
}
}
}
void FixedUpdate()
{
if (jumpReleased)
{
if (rb.linearVelocity.y > 0f)
rb.linearVelocity = new Vector2(rb.linearVelocity.x, rb.linearVelocity.y * jumpCutMultiplier);
jumpReleased = false;
}
}
void DoJump()
{
rb.linearVelocity = new Vector2(rb.linearVelocity.x, jumpVelocity);
bufferTimer = 0f;
jumpReleased = false;
}
bool IsGrounded()
{
Vector2 feet = (Vector2)transform.position + groundCheckOffset;
return Physics2D.OverlapBox(feet, groundCheckSize, 0f, groundLayer) != null;
}
void OnDrawGizmosSelected()
{
Gizmos.color = Color.green;
Gizmos.DrawWireCube((Vector2)transform.position + groundCheckOffset, groundCheckSize);
}
}
Notes on the choices:
linearVelocityis the Unity 6 name for what used to bevelocity. The old name is obsolete in Unity 6.- We set vertical velocity instead of adding force. Setting it means every jump reaches the same height, no matter how fast the player was falling when they double jumped.
- Input is read in
UpdatebecauseWasPressedThisFrameis tied to rendered frames. Reading it inFixedUpdatecan miss presses. - The
rb.linearVelocity.y <= 0.01fcheck stops the grounded state from refilling coyote time during the first frame of a jump, while the feet are still inside the ground box.
How Coyote Time Works
The coyoteTimer is reset to its full value every frame you are grounded, and counts down once you leave the ground. As long as it’s above zero, a jump counts as a ground jump. It’s named after the cartoon coyote who runs off a cliff and hangs in the air until he notices.
Two details matter:
- Zero it after jumping. Otherwise a player could jump, then press again within 0.1 seconds and get a second “ground” jump plus their air jump.
- A coyote jump doesn’t spend an air jump. That’s why the ground branch is checked first. Players who walk off a ledge and jump late still have their double jump available.
Around 0.08 to 0.15 seconds is typical. Longer than about 0.2 seconds starts to look like the player is running on air.
How Jump Buffering Works
When jump is pressed, bufferTimer is set to its full value. Every frame it counts down. While it’s positive, the script tries to jump. The moment the player lands, the coyote branch succeeds and the stored press fires.
The buffer is cleared inside DoJump, so one press only ever produces one jump. Without that, a single press could trigger a ground jump and then immediately an air jump on the next frame.
Buffering also matters for the double jump. If a player presses jump twice quickly, the second press is remembered for a moment instead of being lost because the first jump hasn’t left the ground yet.
Double Jump and Air Jumps
The airJumps field controls how many extra jumps you get in the air. Set it to 0 for no double jump, 1 for a classic double jump or higher for a triple jump. The counter refills on landing.
Common design tweaks:
- Weaker air jump. Use a separate
airJumpVelocityslightly lower than the ground jump. - Unlockable double jump. Keep
airJumpsat 0 and raise it when the player collects an upgrade. - Refill on events. Many games refill air jumps when you bounce on an enemy or touch a crystal. Just set
airJumpsLeft = airJumpsfrom that event. - Feedback. Play a puff of particles and a different sound for the air jump so the player knows they spent it. The game feel guide has more ideas.
Variable Jump Height
Holding jump for a high jump and tapping for a short hop gives players precise control. The script does this with a jump cut: when the button is released while still moving upward, vertical velocity is multiplied by jumpCutMultiplier. A value of 0.5 halves the remaining rise.
Another popular tweak is stronger gravity while falling, which makes jumps feel less floaty. Raise rb.gravityScale when rb.linearVelocity.y < 0 and set it back when rising or grounded. Cap fall speed with a max so the player never falls faster than your ground check and colliders can handle.
Doing the Same in 3D
The timers and logic are identical in 3D. Only the ground check and the velocity change differ.
With a Rigidbody: use Physics.CheckSphere or Physics.SphereCast at the feet and set rb.linearVelocity = new Vector3(v.x, jumpVelocity, v.z).
bool IsGrounded3D()
{
return Physics.CheckSphere(transform.position + Vector3.down * 0.9f, 0.3f, groundLayer,
QueryTriggerInteraction.Ignore);
}
With a CharacterController: you manage your own vertical velocity. Use controller.isGrounded (or your own sphere check, which is more stable), and on jump set verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity), which gives an exact apex height. Apply gravity each frame and pass the vertical velocity into controller.Move. Note that isGrounded only updates after a Move call, so keep a small downward velocity (like -2) while grounded. The CharacterController vs Rigidbody guide covers the trade-offs.
📚 Want to design better platformers?
Coyote time and buffering come straight from game feel design. Our list of the best game design and programming books includes titles that dig into why controls feel good.
Troubleshooting
The player can jump forever in the air
The ground check is hitting the player’s own collider. Put the player on a Player layer and make sure it isn’t in groundLayer. Draw the gizmo and check the box sits just below the feet.
Double jump fires instantly with one press
The buffer isn’t being cleared after the jump, or input is read in FixedUpdate and seen twice. Clear bufferTimer in DoJump and read input only in Update.
Jump height is random
You’re using AddForce while the player has existing vertical velocity. Set the velocity directly instead, or zero y before adding an impulse.
‘Rigidbody2D.velocity’ is obsolete
In Unity 6 the property was renamed to linearVelocity. Replace it and the warning goes away. Behaviour is the same.
Jump doesn’t work after a scene reload
The action was never enabled. Make sure the action is enabled in OnEnable, or use a PlayerInput component that enables its actions for you. The Input System troubleshooting guide covers the other common causes.
FAQ
What is a good coyote time value?
Start at 0.1 seconds. Platformers usually land between 0.06 and 0.15. Test it by running off ledges and jumping late on purpose.
Should I use AddForce or set velocity for jumps?
Setting vertical velocity gives consistent jump heights, which is what most platformers want. AddForce with ForceMode2D.Impulse works too if you zero vertical velocity first.
Does jump buffering work with gamepads?
Yes. It only cares about when the Jump action was pressed, so any bound device works.
How do I stop double jump on slopes?
If slopes cause the ground check to flicker, widen the check box slightly or use a short raycast down. Flickering ground checks refill air jumps at odd times.
Can I use this with a CharacterController?
Yes. Keep the coyote, buffer and air jump timers exactly as written and swap the velocity lines for your own vertical velocity variable.
Reader questions
Asked by readers of this article and answered by AI. Behavior can differ between Unity versions, so test in yours.




Leave a Reply