Jump Buffer not working as intended

using UnityEngine;

[RequireComponent (typeof(Controller))]
public class Jump : MonoBehaviour
{
    [SerializeField, Range(0f, 10f)] private float jumpHeight = 3f;
    [SerializeField, Range(0, 5)] private int maxAirJumps = 0;
    [SerializeField, Range(0f, 5f)] private float downwardMovementMultiplier = 3f;
    [SerializeField, Range(0f, 5f)] private float upwardMovementMultiplier = 1.7f;
    [SerializeField, Range(0f, 0.3f)] private float coyoteTime = 0.2f;
    [SerializeField, Range(0f, 0.3f)] private float jumpBufferTime = 0.2f;
    
    private Controller controller;
    private Rigidbody2D body;
    private Ground ground;
    private Vector2 velocity;

    private int jumpPhase;
    private float defaultGravityScale, jumpSpeed, coyoteCounter, jumpBufferCounter;

    private bool desiredJump, onGround, isJumping;

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Awake()
    {
        body = GetComponent<Rigidbody2D>();
        ground = GetComponent<Ground>();
        controller = GetComponent<Controller>();

        defaultGravityScale = 1f;
    }

    // Update is called once per frame
    void Update()
    {
        desiredJump |= controller.input.RetrieveJumpInput(this.gameObject);
    }

    private void FixedUpdate()
    {
        onGround = ground.OnGround;
        velocity = body.linearVelocity;

        if (onGround && Mathf.Abs(body.linearVelocity.y) <= 0.01f)
        {
            jumpPhase = 0;
            coyoteCounter = coyoteTime;
            isJumping = false;
        }
        else
        {
            coyoteCounter -= Time.deltaTime;
        }

        if (desiredJump)
        {
            desiredJump = false;
            jumpBufferCounter = jumpBufferTime;
        }
        else if(!desiredJump && jumpBufferCounter > 0)
        {
            jumpBufferCounter -= Time.deltaTime;
        }

        if(jumpBufferCounter > 0)
        {
            JumpAction();
        }

        if(controller.input.RetrieveHoldJumpInput(this.gameObject) && body.linearVelocity.y > 0f)
        {
            body.gravityScale = upwardMovementMultiplier;
        }
        else if(!controller.input.RetrieveHoldJumpInput(this.gameObject) || body.linearVelocity.y < 0f)
        {
            body.gravityScale = downwardMovementMultiplier;
        }
        else
        {
            body.gravityScale = defaultGravityScale;
        }

        body.linearVelocity = velocity;
    }

    private void JumpAction()
    {
        if (coyoteCounter > 0f || (jumpPhase < maxAirJumps && isJumping))
        {
            if (isJumping)
            {
                jumpPhase += 1;
            }

            jumpBufferCounter = 0;
            coyoteCounter = 0;
            jumpSpeed = Mathf.Sqrt(-2f * Physics2D.gravity.y * jumpHeight);
            isJumping = true;

            if(velocity.y > 0f)
            {
                jumpSpeed = Mathf.Max(jumpSpeed - velocity.y, 0f);
            }
            
            if (onGround)
            {
                velocity.y += jumpSpeed;
            }
            else
            {
                velocity.y = jumpSpeed;
            }
        }
    }
}

I intended to give some leniency in timing the jumps. So added jump buffer but the issue I am facing is when I set maxAirJump to 1 or higher value, perform the maximum jumps allowed and press jump just before landing it jumps, as intended, but if I continue tapping jump it does one less maxAirJump than intended, ie, if I set maxAirJump to 2 it will jump twice in air and just before landing if jump is triggered then in next sequence of jumps it will only do 1 maxAirJump.

Sounds like you wrote a bug… and that means… time to start debugging!

If you want my coyote-time jump-buffering example logic, check this out:

Coyote Time, Multi-Jumping, Jump-Buffering, and disconnecting input gathering from input processing:

My example is simple 2D but most likely the logic flow is identical to what you need.

By debugging you can find out exactly what your program is doing so you can fix it.

Use the above techniques to get the information you need in order to reason about what the problem is.

You can also use Debug.Log(...); statements to find out if any of your code is even running. Don’t assume it is.

Once you understand what the problem is, you may begin to reason about a solution to the problem.

Remember with Unity the code is only a tiny fraction of the problem space. Everything asset- and scene- wise must also be set up correctly to match the associated code and its assumptions.

Your code is overly complex, mostly because you have followed the “input in Update, forces in FixedUpdate” rule too hard.

It’s a good rule for continuous inputs (movement) as they need to apply force in sync with FixedUpdate in order to make sense.

But jumping and other impulse actions should just happen immediately in Update. You’re doing it once anyway, it’ll be based on and be applied to same physics update steps (see here), so you save yourself a bunch of data passing that’s not really helping.

There’s also an argument for coyote time living in Update - you’re trying to give the player some leniency, but unless your framerate is perfectly in sync with FixedUpdate and you’re not dropping frames, the amount of real time the player has to press jump after walking off the ledge will vary depending on how many FixedUpdates happen after walking off the ledge.

So, yeah, step 1 is to get rid of desiredJump and then just Jump in Update. And then to simplify further, fix your variable names and reduce the “should I jump” code down to this in Update;

if (onGround)
    lastOnGroundTime = Time.time;

if (jumpPressed) {
    if (onGround || Time.time - lastOnGroundTime <= coyoteTimeDuration) {
        doubleJumpsLeft = numDoubleJumps;
        Jump();
    }
    else if (doubleJumpsLeft > 0) {
        doubleJumpsLeft--;
        Jump();
    }
}

If you add in a cooldown between jumps (which you should probably do anyways to avoid clunky, glitchy behavior around up-slopes and rapid pressing), you can avoid dealing with the whole Mathf.Abs(body.linearVelocity.y) <= 0.01f) hack you have and the “I’m grounded for a few physics frame after I start jumping” problems.