Input Buffering and Coyote Time: Small Systems That Make Platformers Feel Great
Learn how jump input buffering and coyote time work, how to implement them safely, and how they interact with physics ticks, moving platforms, wall jumps and accessibility.
# Input Buffering and Coyote Time: Small Systems That Make Platformers Feel Great
Platformer controls often fail for reasons players cannot see.
A character may look like they were standing on the platform when the physics engine had already marked them airborne. A jump press may arrive just before landing, when the game technically cannot jump yet. The player experiences both cases as:
> **"I pressed jump. Why didn't it happen?"**
Two small systems help reconcile strict simulation timing with human timing:
- **input buffering** remembers a recent input until the action becomes valid;
- **coyote time** keeps a recently valid action available for a short grace period after the state changes.
They are not mandatory for every platformer, and they do not have universal correct durations. They are design tools.
## Input Buffering
Input buffering stores the fact that an input happened recently.
Example:
1. player presses Jump shortly before landing;
2. the game records the press time;
3. the character becomes grounded;
4. if the stored jump is still inside the buffer window, the game executes it;
5. the buffered input is consumed.
Without a buffer, that press is discarded because Jump was invalid at the exact sample where it occurred.
## Store Timestamps, Not Only Countdown Booleans
A timestamp makes the intent easier to reason about:
```csharp
[SerializeField] private float jumpBufferSeconds = 0.12f;
private float lastJumpPressedAt = float.NegativeInfinity;
public void OnJumpPressed()
{
lastJumpPressedAt = Time.time;
}
private bool HasBufferedJump()
{
return Time.time - lastJumpPressedAt <= jumpBufferSeconds;
}
private void ConsumeJumpBuffer()
{
lastJumpPressedAt = float.NegativeInfinity;
}
```
This representation makes it obvious when the input happened and prevents the same press from being reused after consumption.
## Buffer Windows Are Game-Specific
Do not copy a universal `100–150 ms` number.
The useful window depends on:
- character speed;
- jump cadence;
- animation timing;
- physics tick rate;
- camera zoom;
- level geometry;
- target audience;
- competitive precision;
- accessibility goals.
A very generous buffer can make actions trigger later than the player expected. A very short buffer may be indistinguishable from no buffer.
Tune with playtesting.
## Buffer More Than Jump When It Helps
Input buffering is useful for many actions:
- attacks during recovery;
- combo follow-ups;
- dodge after a committed animation;
- interaction just before entering range;
- reload immediately after firing;
- menu confirmations during transitions.
Each action can have its own rules.
Do not create one global buffer duration and apply it to every command.
## Coyote Time
Coyote time extends action eligibility briefly after leaving a valid state.
For jumping:
1. character is grounded;
2. character walks off an edge;
3. grounded state becomes false;
4. jump remains allowed for a short grace period.
The mechanic is commonly called coyote time after the cartoon trope of a character hanging beyond a ledge before falling.
## Track the Last Valid Ground Time
```csharp
[SerializeField] private float coyoteSeconds = 0.10f;
private float lastGroundedAt = float.NegativeInfinity;
private void RecordGroundedState(bool isGrounded)
{
if (isGrounded)
lastGroundedAt = Time.time;
}
private bool HasCoyoteTime()
{
return Time.time - lastGroundedAt <= coyoteSeconds;
}
```
This works well when the rule is "jump is valid if grounded now or grounded recently."
## Consume Coyote Time After Jumping
Without explicit consumption, one jump could satisfy the grace-period condition repeatedly.
```csharp
private void ConsumeGroundGrace()
{
lastGroundedAt = float.NegativeInfinity;
}
```
Call it when a jump succeeds.
## Combine Buffering and Coyote Time
The systems complement each other:
- coyote time helps a **late** jump press;
- input buffering helps an **early** jump press.
A simplified controller rule becomes:
```text
recent jump press
AND
recent valid ground state
→ execute jump
```
## A More Complete Unity Example
This example assumes input actions call `OnJumpPressed()` and `OnJumpReleased()`. It keeps input detection separate from the physics action.
```csharp
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D))]
public class ResponsiveJumpController : MonoBehaviour
{
[Header("Jump")]
[SerializeField] private float jumpVelocity = 14f;
[SerializeField] private float jumpBufferSeconds = 0.12f;
[SerializeField] private float coyoteSeconds = 0.10f;
[SerializeField, Range(0f, 1f)] private float jumpCutMultiplier = 0.5f;
[Header("Ground Check")]
[SerializeField] private Transform groundCheck;
[SerializeField] private Vector2 groundCheckSize = new(0.6f, 0.12f);
[SerializeField] private LayerMask groundMask;
private Rigidbody2D body;
private float lastJumpPressedAt = float.NegativeInfinity;
private float lastGroundedAt = float.NegativeInfinity;
private bool jumpReleaseQueued;
private void Awake()
{
body = GetComponent();
}
private void Update()
{
bool grounded = Physics2D.OverlapBox(
groundCheck.position,
groundCheckSize,
0f,
groundMask
) != null;
if (grounded)
lastGroundedAt = Time.time;
}
private void FixedUpdate()
{
if (CanConsumeJump())
Jump();
if (jumpReleaseQueued)
{
jumpReleaseQueued = false;
if (body.linearVelocityY > 0f)
{
Vector2 velocity = body.linearVelocity;
velocity.y *= jumpCutMultiplier;
body.linearVelocity = velocity;
}
}
}
public void OnJumpPressed()
{
lastJumpPressedAt = Time.time;
}
public void OnJumpReleased()
{
jumpReleaseQueued = true;
}
private bool CanConsumeJump()
{
bool buffered = Time.time - lastJumpPressedAt <= jumpBufferSeconds;
bool groundedRecently = Time.time - lastGroundedAt <= coyoteSeconds;
return buffered && groundedRecently;
}
private void Jump()
{
Vector2 velocity = body.linearVelocity;
velocity.y = jumpVelocity;
body.linearVelocity = velocity;
lastJumpPressedAt = float.NegativeInfinity;
lastGroundedAt = float.NegativeInfinity;
}
}
```
Depending on the Unity version/API profile, `Rigidbody2D.velocity` may be used instead of `linearVelocity`; use the property appropriate to the project version.
## Why Separate Input From Physics?
A common bug occurs when `GetButtonDown()` is read only in the fixed physics loop.
Input events can happen between physics ticks. Depending on engine/input architecture, a short press can be missed.
A safer pattern is:
- record button edges in the input/update layer;
- store the event;
- consume it in the simulation/physics layer when appropriate.
Modern input systems may provide event callbacks that make this even cleaner.
## Ground Detection Is Part of the Feel
Coyote time cannot fix a poor ground check.
Ground detection needs to handle:
- slopes;
- moving platforms;
- thin ledges;
- one-way platforms;
- wall contacts;
- uneven terrain;
- changing gravity if the game supports it.
A single ray from the character center may fail near platform edges because the player's feet visually overlap a surface while the ray misses it.
Depending on the controller, use:
- shape casts;
- overlap boxes/capsules;
- collision contact data;
- engine character-controller grounding state.
## Moving Platforms
When standing on a moving platform, jump behavior may depend on platform velocity.
Possible rules:
- inherit vertical platform velocity;
- inherit horizontal velocity;
- detach with current world velocity;
- cap inherited motion.
Coyote time should usually preserve the same logical ground relationship briefly after leaving.
## Wall Jumps Need Their Own Grace Rules
If the game has wall jumps, you may want:
- wall coyote time;
- wall-jump input buffering;
- separate left/right wall eligibility;
- cooldown before reattaching to the same wall.
Do not reuse ground coyote state blindly.
Example conceptual state:
```text
last_grounded_at
last_left_wall_at
last_right_wall_at
last_jump_pressed_at
```
Then choose the jump type from the most relevant recent valid state.
## Ladders, Ledges, and Traversal
The same concept generalizes.
You can buffer:
- climb input before a ladder enters range;
- mantle input just before a ledge query succeeds;
- dash input until recovery ends.
But each buffer should preserve player intent rather than execute an obsolete command seconds later.
## Variable Jump Height
Variable jump height is separate from buffering/coyote time.
A common technique is **jump cut**: releasing Jump during upward movement reduces vertical velocity.
```csharp
if (jumpReleased && velocity.y > 0f)
velocity.y *= jumpCutMultiplier;
```
This creates a range of jump heights between a tap and a hold.
The multiplier is a design parameter, not universally `0.5`.
## Apex Behavior
Some platformers alter gravity or acceleration near the apex.
Possible goals:
- give more time for horizontal correction;
- emphasize a floaty style;
- improve readability;
- create a sharper arcade arc.
Reducing gravity near zero vertical velocity is one option, but not a universal quality improvement.
A precision game may prefer a sharp, consistent ballistic arc.
## Fall Gravity
Many platformers use stronger downward gravity than upward gravity to create a fast, readable fall.
Conceptually:
```csharp
float gravityScale = velocity.y < 0f
? fallGravityMultiplier
: riseGravityMultiplier;
```
Again, this is a style choice.
## Jump Apex Input Assist
Some games increase horizontal acceleration or control near the apex.
This can help players make fine landing adjustments.
But too much apex assistance can make momentum feel inconsistent.
Expose the rule in tuning data and playtest it.
## Input Buffer Queues vs. Single Action Slots
A jump buffer often needs only the **latest jump press**.
Combat systems may need a true queue:
```text
Light Attack
Light Attack
Heavy Attack
```
If multiple commands can be buffered, define:
- maximum queue length;
- whether duplicate commands collapse;
- which actions replace previous actions;
- expiration time per command;
- cancellation conditions.
Do not accidentally build an unlimited command queue that executes stale inputs.
## Buffer Priority
If Jump and Dash are both buffered when the character lands, which executes first?
Possible policies:
- latest input wins;
- fixed action priority;
- both can execute if compatible;
- entering grounded state consumes only jump-related input.
Document the rule so edge cases are predictable.
## Accessibility
Timing forgiveness can also be an accessibility feature.
Consider exposing options for:
- larger jump buffer;
- larger coyote window;
- slower game speed;
- hold-to-repeat interactions;
- reduced precision requirements.
That does not mean every game needs user-facing sliders for these values. It means timing windows can be part of accessibility design rather than hidden sacred constants.
## Competitive Games
In competitive games, timing forgiveness affects balance.
A larger buffer may:
- make combos easier;
- increase reversal consistency;
- change execution skill requirements;
- affect network prediction interactions.
Do not evaluate buffers only as "feel improvements." They are game rules.
## Networked Input Buffers
For online games, local buffering and network buffering/prediction solve different problems.
A local input buffer stores **player intent** until gameplay accepts it.
Network prediction/reconciliation handles **remote authority and latency**.
You may need both.
Do not let the client claim an impossible coyote jump purely because its local clock says it was valid; authoritative multiplayer needs a defined server/prediction policy.
## Debugging Timing Windows
Add debug visualization.
For example, display:
```text
Grounded: false
Time since grounded: 0.064
Coyote window: 0.100
Time since jump press: 0.041
Buffer window: 0.120
Can jump: true
```
This makes "sometimes jump doesn't work" bugs far easier to diagnose.
## Test Boundaries Deliberately
Test:
- jump exactly at edge departure;
- jump just inside/outside coyote window;
- jump just before landing;
- hold/release during buffered landing;
- walking off moving platforms;
- landing and immediately walking off again;
- pause/slow motion if timers use scaled time;
- low frame rates;
- different physics tick rates.
Timing assists often fail at boundaries rather than during normal play.
## Common Mistakes
### Universal Timing Values
`0.12` seconds is an example, not a design standard.
### Buffer Never Consumed
One press triggers multiple actions.
### Coyote Time Not Invalidated After Jump
The player gets an unintended second jump.
### Input Read Only in Physics Tick
Short presses can be missed.
### Weak Ground Check
Coyote time masks but does not fix grounding bugs.
### Stale Buffered Inputs
A button press executes long after player intent has changed.
### Treating Forgiveness as Cheating
It is part of control design, like hitboxes, acceleration, and camera behavior.
## A Practical Tuning Process
1. **Build the strict version first.** Understand the raw failure cases.
2. **Add debug timing visualization.**
3. **Add a small buffer and coyote window.**
4. **Test edge cases at several frame/physics rates.**
5. **Watch new players.** Note early/late presses.
6. **Tune separately.** Buffer and coyote time solve different timing errors.
7. **Check interaction with wall jumps, moving platforms, dashes, and variable jump.**
8. **Expose accessibility options if useful for the game.**
## The Goal
Input buffering and coyote time are valuable because they translate **recent player intent** into a more forgiving simulation boundary.
Use them intentionally, consume them correctly, and tune them around the game's movement model rather than copying another platformer's numbers.
The result should not feel like the game is secretly playing for the user. It should feel like the character did what the player reasonably expected.