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.

Practice - what you do

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

Prerequisite: All scripts below require the InputReader to be set up in your scene.

BufferedJump2D.cs

2D jump with input buffering. Press jump before landing and it executes on landing.

Setup:

  1. Add Rigidbody2D to your player
  2. Create a child "GroundCheck" at the feet
  3. 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:

  1. Add CharacterController to your player
  2. Add this script and configure in Inspector
  3. 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

Related

Glossary Terms