Variable Jump Height script not working as intended

Hi all. I am attempting to get variable jump height working for a platformer, where the longer a player holds a button the higher they jump, but it’s not working as intended.

The jump height in editor preview is almost double the jump height in the build version of the game. The only thing I can think of that is affecting it is that the editor has a framerate of around 148fps, while the build is locked to 60fps. But I am using Time.deltaTime in my jump script, so it should be framerate independent (as far as I know). Here is the relevant code:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour {

//The players rigidbody
Rigidbody rb;

//The jump key
KeyCode jump = KeyCode.W;

//Is the player jumping
bool jumping = false;

//The players max jump height
float jumpHeightMax = 0.8f;

//Player Start method
void Start () {
  rb = GetComponent<Rigidbody>();
}

//Player Update method
void Update(){
  PlayerMovement();
}

//Movement
void PlayerMovement(){
  if (Input.GetKeyDown(jump) && !jumping)
  {
    StartCoroutine(Jump());
  }
}

//Jump
IEnumerator Jump(){
  rb.velocity = Vector2.zero;
  float timer = 0;

  while (Input.GetKey(jump) && timer < jumpHeightMax)
  {
    float portionCompleted = timer / jumpHeightMax;
    Vector2 thisFrameJumpVector = Vector3.Lerp(Vector3.up, Vector3.zero, portionCompleted);
    rb.AddForce(thisFrameJumpVector, ForceMode.Impulse);
    timer += Time.deltaTime;
    jumping = true;
    yield return null;
  }
}
}

Here is a video showing the issue:

Any and all input is welcome, thank you!

A few things to note: First, using deltaTime only approximates frame independence (when used correctly), it doesn’t achieve it perfectly. If you really want frame independence, you run your mechanics on an update cycle that on average runs at exactly the rate you desire. This might mean that on some visually rendered frames, the mechanics update cycle iterates multiple times, one time, or zero times. This is what physics engines typically do, and is why Unity has FixedUpdate() and Time.fixedDeltaTime.

Second, I’m pretty sure that ForceMode.Impulse is generally only appropriate when you add a force once, instantaneously. Not when you’re repeatedly adding a force over multiple frames. In that case, you want ForceMode.Force (the default), and you also want to do this within FixedUpdate(), not Update(), so that it properly applies the force reliably every iteration of the physics cycle. Or when using coroutines as you are, you want to yield return new WaitForFixedUpdate() instead of null. You might be able to get away with using the normal update cycle and ForceMode.Impulse, but you’ll need to scale the amount of impulse by the duration of the frame. You said you’re using deltaTime, but you’re not actually using it where it would affect physics, only time. You’d need to multiply your jump vector by deltaTime (and further by some constant based on desired game feel) to approximate frame-independent physics. (If you go with the go with fixed-rate updates, this issue disappears altogether.)

Finally, the method you’re using to manage the jump physics looks awkward to me. A noble try, and I know getting good feeling jump physics can be tough, but I think you’ll end up having a hard time tweaking it to feel just right, even if you get it sorta working. For me, I’ve had the best luck using AddForce(Vector3.up, ForceMode.VelocityChange) on the very first frame of the jump, doing nothing on the following frames as long as the jump button is held, and if the jump button is released while the rigid body’s vertical velocity is still positive, I instantly clamp the vertical velocity to be no greater than some very small positive value. This produces a predictable parabolic jump arc that can be easily tweaked by changing the velocity change value, and a high degree of control for the player to stop the jump early if they wish. And you might naively choose to clamp the vertical velocity to 0 when they cancel a jump early, but in my experience, that actually feels too aggressive. Some small positive value, chosen according to taste, seems to be ideal.

The following (just written, untested) attempts to achieve the effect just described, and doesn’t bother with coroutines at all. You’ll have to play around to figure out good values for jumpStrength and cancelJumpMaxVelocity.

void PlayerMovement()
{
  if (Input.GetKeyDown(jump) && !jumping)
  {
    // Instantly push the avatar upward.
    rb.AddForce(Vector3.up * jumpStrength, ForceMode.VelocityChange);
    jumping = true;
  }
  else if (jumping)
  {
    if (!Input.GetKey(jump))
    {
      // Cancel the jump early.
      var v = rb.velocity;
      v.y = Mathf.Min(v.y, cancelJumpMaxVelocity);
      rb.velocity = v;
      jumping = false;
    }
    else if (rb.velocity.y <= cancelJumpMaxVelocity)
    {
      // The jump has now progressed beyond the point where it can be canceled early.
      jumping = false;
    }
  }
}