Input Buffer
Remember button presses for a short window, executing them when possible. If you press jump just before landing, the jump happens when you land.
The Feel
Input buffering makes actions feel responsive and forgiving. Without it, players need frame-perfect timing. With it, they can queue up their next action and trust it will happen.
Exposed Variables
| Variable | Type | Default | What it does |
|---|---|---|---|
bufferTime |
float | 0.1 | How long to remember an input |
Tuning Guide
For precision gameplay:
bufferTime= 0.05 - 0.08
For accessible/casual:
bufferTime= 0.1 - 0.15
For fighting games:
bufferTime= 0.1 - 0.2 (longer for combo inputs)
Perspective Scripts
BufferedJump2D.cs
2D jump with input buffering. Press jump before landing and it executes on landing.
Setup:
- Add
Rigidbody2Dto your player - Create a child "GroundCheck" at the feet
- Add this script and configure in Inspector
BufferedJump2D.cs
using UnityEngine;
/// <summary>
/// BUFFERED JUMP 2D - Remember jump input and execute when possible
/// VG101 Code Bank - Requires InputReader from Input Setup
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public class BufferedJump2D : MonoBehaviour
{
[Header("Jump Feel")]
[Range(5f, 25f)]
[SerializeField] private float jumpForce = 12f;
[Range(20f, 80f)]
[SerializeField] private float gravity = 40f;
[Range(1f, 4f)]
[SerializeField] private float fallGravityMultiplier = 1.5f;
[Header("Input Buffer")]
[Tooltip("How long to remember a jump press")]
[Range(0.01f, 0.3f)]
[SerializeField] private float bufferTime = 0.1f;
[Header("Ground Detection")]
[SerializeField] private Transform groundCheckPoint;
[Range(0.05f, 0.5f)]
[SerializeField] private float groundCheckRadius = 0.2f;
[SerializeField] private LayerMask groundLayer;
[Header("Debug")]
[SerializeField] private bool showGroundCheck = true;
[SerializeField] private bool debugBuffer = false;
private Rigidbody2D rb;
private float verticalVelocity;
private bool isGrounded;
private float bufferTimer;
private bool jumpBuffered;
private void Awake()
{
rb = GetComponent<Rigidbody2D>();
rb.gravityScale = 0f;
}
private void OnEnable()
{
if (InputReader.Instance != null)
{
InputReader.Instance.OnJumpPressed += HandleJumpInput;
}
}
private void OnDisable()
{
if (InputReader.Instance != null)
{
InputReader.Instance.OnJumpPressed -= HandleJumpInput;
}
}
private void FixedUpdate()
{
CheckGrounded();
HandleBuffer();
ApplyGravity();
ApplyVelocity();
}
private void CheckGrounded()
{
isGrounded = Physics2D.OverlapCircle(
groundCheckPoint.position,
groundCheckRadius,
groundLayer
);
if (isGrounded && verticalVelocity < 0)
{
verticalVelocity = 0f;
}
}
private void HandleBuffer()
{
// Count down buffer timer
if (bufferTimer > 0)
{
bufferTimer -= Time.fixedDeltaTime;
}
else
{
jumpBuffered = false;
}
// Execute buffered jump when grounded
if (jumpBuffered && isGrounded)
{
ExecuteJump();
jumpBuffered = false;
bufferTimer = 0;
if (debugBuffer) Debug.Log("BUFFERED JUMP executed!");
}
}
private void HandleJumpInput()
{
if (isGrounded)
{
// Jump immediately
ExecuteJump();
}
else
{
// Buffer the input
jumpBuffered = true;
bufferTimer = bufferTime;
if (debugBuffer) Debug.Log("Jump BUFFERED...");
}
}
private void ExecuteJump()
{
verticalVelocity = jumpForce;
}
private void ApplyGravity()
{
if (!isGrounded)
{
float gravityThisFrame = gravity;
if (verticalVelocity < 0)
{
gravityThisFrame *= fallGravityMultiplier;
}
verticalVelocity -= gravityThisFrame * Time.fixedDeltaTime;
}
}
private void ApplyVelocity()
{
Vector2 velocity = rb.linearVelocity;
velocity.y = verticalVelocity;
rb.linearVelocity = velocity;
}
private void OnDrawGizmos()
{
if (showGroundCheck && groundCheckPoint != null)
{
Gizmos.color = isGrounded ? Color.green : Color.red;
Gizmos.DrawWireSphere(groundCheckPoint.position, groundCheckRadius);
}
}
}
BufferedJump3D.cs
3D jump with input buffering. Works for third-person, first-person, and isometric perspectives.
Setup:
- Add
CharacterControllerto your player - Add this script and configure in Inspector
- Integrate with your movement script or use built-in movement
BufferedJump3D.cs
using UnityEngine;
/// <summary>
/// BUFFERED JUMP 3D - Input buffer for all 3D perspectives
/// VG101 Code Bank - Requires InputReader from Input Setup
/// </summary>
[RequireComponent(typeof(CharacterController))]
public class BufferedJump3D : MonoBehaviour
{
[Header("Jump Feel")]
[Range(5f, 25f)]
[SerializeField] private float jumpForce = 10f;
[Range(20f, 80f)]
[SerializeField] private float gravity = 30f;
[Range(1f, 4f)]
[SerializeField] private float fallGravityMultiplier = 2f;
[Header("Input Buffer")]
[Tooltip("How long to remember a jump press")]
[Range(0.01f, 0.3f)]
[SerializeField] private float bufferTime = 0.1f;
[Header("Ground Detection")]
[SerializeField] private Vector3 groundCheckOffset = new Vector3(0, -0.9f, 0);
[SerializeField] private float groundCheckRadius = 0.3f;
[SerializeField] private LayerMask groundLayer;
[Header("Movement (Optional)")]
[SerializeField] private float moveSpeed = 8f;
[Range(0f, 1f)]
[SerializeField] private float airControl = 0.5f;
[SerializeField] private bool useBuiltInMovement = true;
[Header("Debug")]
[SerializeField] private bool showGroundCheck = true;
[SerializeField] private bool debugBuffer = false;
private CharacterController controller;
private Vector3 velocity;
private bool isGrounded;
private float bufferTimer;
private bool jumpBuffered;
private Transform cameraTransform;
private void Awake()
{
controller = GetComponent<CharacterController>();
cameraTransform = Camera.main?.transform;
}
private void OnEnable()
{
if (InputReader.Instance != null)
{
InputReader.Instance.OnJumpPressed += HandleJumpInput;
}
}
private void OnDisable()
{
if (InputReader.Instance != null)
{
InputReader.Instance.OnJumpPressed -= HandleJumpInput;
}
}
private void Update()
{
CheckGrounded();
HandleBuffer();
if (useBuiltInMovement)
{
HandleMovement();
}
ApplyGravity();
ApplyVelocity();
}
private void CheckGrounded()
{
Vector3 checkPosition = transform.position + groundCheckOffset;
isGrounded = Physics.CheckSphere(checkPosition, groundCheckRadius, groundLayer);
if (isGrounded && velocity.y < 0)
{
velocity.y = -2f;
}
}
private void HandleBuffer()
{
if (bufferTimer > 0)
{
bufferTimer -= Time.deltaTime;
}
else
{
jumpBuffered = false;
}
if (jumpBuffered && isGrounded)
{
ExecuteJump();
jumpBuffered = false;
bufferTimer = 0;
if (debugBuffer) Debug.Log("BUFFERED JUMP executed!");
}
}
private void HandleMovement()
{
Vector2 input = InputReader.Instance != null ? InputReader.Instance.MoveInput : Vector2.zero;
Vector3 forward = cameraTransform != null ? cameraTransform.forward : transform.forward;
Vector3 right = cameraTransform != null ? cameraTransform.right : transform.right;
forward.y = 0f;
right.y = 0f;
forward.Normalize();
right.Normalize();
Vector3 moveDirection = (forward * input.y + right * input.x);
float currentSpeed = isGrounded ? moveSpeed : moveSpeed * airControl;
velocity.x = moveDirection.x * currentSpeed;
velocity.z = moveDirection.z * currentSpeed;
if (moveDirection.magnitude > 0.1f)
{
Quaternion targetRotation = Quaternion.LookRotation(moveDirection);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, 10f * Time.deltaTime);
}
}
private void HandleJumpInput()
{
if (isGrounded)
{
ExecuteJump();
}
else
{
jumpBuffered = true;
bufferTimer = bufferTime;
if (debugBuffer) Debug.Log("Jump BUFFERED...");
}
}
private void ExecuteJump()
{
velocity.y = jumpForce;
}
private void ApplyGravity()
{
if (!isGrounded)
{
float gravityThisFrame = gravity;
if (velocity.y < 0)
{
gravityThisFrame *= fallGravityMultiplier;
}
velocity.y -= gravityThisFrame * Time.deltaTime;
}
}
private void ApplyVelocity()
{
controller.Move(velocity * Time.deltaTime);
}
public void SetHorizontalVelocity(Vector3 horizontalVelocity)
{
velocity.x = horizontalVelocity.x;
velocity.z = horizontalVelocity.z;
}
private void OnDrawGizmos()
{
if (showGroundCheck)
{
Vector3 checkPosition = transform.position + groundCheckOffset;
Gizmos.color = isGrounded ? Color.green : Color.red;
Gizmos.DrawWireSphere(checkPosition, groundCheckRadius);
}
}
}
Key Implementation Details
- Immediate when possible: If grounded, jump happens instantly
- Buffer when not possible: If airborne, store the input with a timer
- Check every frame: Once grounded + buffered, execute the jump
- Clear after use: Prevents accidental double activation
The Complete Forgiveness Package
| Technique | What it forgives |
|---|---|
| Coyote Time | Pressing jump slightly AFTER leaving edge |
| Input Buffer | Pressing jump slightly BEFORE landing |
| Both together | Nearly all "unfair" missed jumps |
Beyond Jump: What Else to Buffer
| Action | Why Buffer |
|---|---|
| Jump | Press before landing, executes on land |
| Attack | Queue next attack during current one |
| Dash | Press during jump, executes when able |
| Dodge | Press during hitstun, executes on recovery |
Common Issues
"Buffered input executes multiple times"
You're not clearing the buffer after execution. Set jumpBuffered = false after ExecuteJump().
"Buffer doesn't seem to work"
Make sure you're recording input even when grounded. Test with debugBuffer enabled.
Combine With
- Coyote Time - together they cover both timing error directions
- Hitstop - buffer inputs during freeze frames
- Dash - buffer dash during other actions
Why Teach This
Input buffering teaches that player intent matters more than precise timing. The videogame should try to understand what the player wanted to do.
Teaching Sequence
- Have students play and note when jumps "don't work"
- Add input buffering - watch failures disappear
- Extend to other actions (attacks, dashes)
- Discuss: where would buffering be unwanted?
The Complementary Pair
Teach with coyote time. Together they illustrate complete timing forgiveness:
- Coyote Time: Forgives pressing AFTER leaving platform
- Input Buffer: Forgives pressing BEFORE being able to
Assessment Questions
- When would you NOT want to buffer an input?
- How does buffer time affect combat rhythm?
- Why must immediate execution still work?
Heritage Notes
Input buffering became essential with fighting games in the 1990s. Street Fighter II's combo system required inputting moves during other moves' animations. Without buffering, combos would be nearly impossible.
The Intent Problem
Input buffering attempts to solve the intent problem: the player knows what they want to do, but the videogame only sees button presses. Timing errors don't mean the player changed their mind.
A generous buffer says: "I trust that you meant to do this, so I'll do it when I can."
When Buffering Is Wrong
- Defensive actions: Blocking/dodging often requires precise timing for skill expression
- Commitment actions: If commitment matters, buffering undermines it
- Context-sensitive: Same button, different actions - buffering can cause wrong action
Related
- Coyote Time - the complementary forgiveness technique
- Hitstop - buffer inputs during freeze
- Input Setup - required InputReader system