Search Unity

Can I get an "Expert's" Explanation of FixedUpdate() vs Update()?

Discussion in 'Physics' started by n_gon, Oct 14, 2021.

  1. n_gon

    n_gon

    Joined:
    Oct 8, 2020
    Posts:
    10
    I don't mean to be condescending, but the amount of bad information I'm seeing on FixedUpdate() is...well it's a lot. The most common claim I'm seeing is that FixedUpdate is "Fixed" and therefore frame rate independent. This is...nonesense. It is impossible for a game loop to be frame rate independent, no computer is stable enough to not 'miss the train'. In short, I've tried looking online but there's too much incorrect information for me to sort through and I'm not sure where a reliable source might be. I'm not sure if anyone here feels like they have an extremely solid understanding and not a speculative understanding (or a link to a good resource?)

    I have noticed that I get some weird behavior if I have my movement code in Update(). With a fairly standard method of applying input vectors to my velocity and updating a character controller's position (CharacterController.Move(delta)), I get very weird results inside Update(). When I hold "W" my character will stall for a few seconds and I can see that their Y Velocity is like +0.005ish for 1-20 seconds (totally random) and then all of the sudden it just starts working properly until I stop moving and try to start moving in which case it stalls for 1-20 seconds again (again, COMPLETELY random amount of time) before it starts moving faster than .005ish. Can anyone explain why this code works if I put it in FixedUpdate() but not Update()? Obviously I could just accept that it works in FixedUpdate and move on but...I need to understand why it works.

    Movement code is below, m_InMoveVector is set when the Input System Package detects a change in Input (either when "W" is pressed down or when "W" is released, for instance). I have checked and it does not appear to be on a delay. If I print the value of m_InMoveVector the magnitude is '1' before the character starts moving noticeably (when its stalled) so I don't believe that's the culprit. In fact, I've tried printing the entire Time.deltaTime * m_InputAcceleration * m_InMoveVector.x and it appears to be (nearly) constant regardless of whether my character is stalled.


    Vector2 velocity2D = new Vector2(m_Velocity.x, m_Velocity.z);

    // If we are moving faster than we should be (within a tolerance of 1%)
    if (velocity2D.magnitude > m_InMoveVector.magnitude * m_MovementSpeed * 1.01f)
    {
    //If our deceleration is greater than our remaining velocity, this means we should set velocity to 0,0 to avoid overshooting
    if ((velocity2D.normalized * m_InputDeceleration * Time.deltaTime).magnitude > velocity2D.magnitude)
    velocity2D = Vector2.zero;
    else
    velocity2D = velocity2D - velocity2D.normalized * m_InputDeceleration * Time.deltaTime;
    }
    else
    {
    float timeScale = Mathf.Clamp(Time.deltaTime * m_InputTurningBoost, 0f, 1f);
    //Apply change to direction based only on turning boost
    velocity2D = velocity2D + (m_InMoveVector.normalized * velocity2D.magnitude - velocity2D) * timeScale;

    velocity2D = velocity2D + m_InMoveVector * Time.deltaTime * m_InputAcceleration;
    velocity2D = velocity2D.normalized * Mathf.Clamp(velocity2D.magnitude, 0f, m_MovementSpeed);
    }

    //Debug.Log(velocity2D.x);
    m_Velocity = new Vector3(velocity2D.x, m_Velocity.y, velocity2D.y);


    edit: I've also noticed in the past with simpler movement code (velocity either equals movement speed or zero), the only way to not get camera stutter is to have both my movements AND the camera updating in Update(). If the Camera is in FixedUpdate, I have a bad stutter. If the Camera movement is updating in Update() and the character movement is updating in FixedUpdate(), I get some bad artifacts since they're updating at different times.
     
    Last edited: Oct 14, 2021
  2. Peeling

    Peeling

    Joined:
    Nov 10, 2013
    Posts:
    443
    Not saying I'm an 'expert', but I think I can answer your questions.

    First question:

    "The most common claim I'm seeing is that FixedUpdate is "Fixed" and therefore frame rate independent. This is...nonesense."

    Actually, it's not. It just doesn't work the way you are imagining.

    Suppose you set Unity's fixed update interval to a leisurely 0.1 seconds, and further suppose you are running a simple scene that easily runs at a capped 60fps. Here's what will happen:

    Frame 0: FixedUpdate(T=0) Update(T=0) Render(T=0)
    Frame 1: Update(T=1/60) Render(T=1/60)
    Frame 2: Update(T=2/60) Render(T=2/60)
    Frame 3: Update(T=3/60) Render(T=3/60)
    Frame 4: Update(T=4/60) Render(T=4/60)
    Frame 5: Update(T=5/60) Render(T=5/60)
    Frame 6: FixedUpdate(T=6/60) Update(T=6/60) Render(T=6/60)
    Frame 7: Update(T=7/60) Render(T=7/60)
    Frame 8: Update(T=8/60) Render(T=8/60)

    Any rigidbodies that have 'interpolate' or 'extrapolate' enabled will smoothly blend their positions between frames 0 and 6.

    Now suppose you go to a super intense scene where you're only getting 5fps. Here's what will happen:

    Frame 0: FixedUpdate(T=0) Update(T=0) Render(T=0)
    ... busy rendering lots of splody stuff
    Frame 12: FixedUpdate(T=6/60) FixedUpdate(T=12/60) Update(T=12/60) Render(T=12/60)
    ... busy rendering lots of splody stuff
    Frame 24: FixedUpdate(T=18/60) FixedUpdate(T=24/60) Update(T=24/60) Render(T=24/60)

    In other words, the 'fixed' nature of fixed update is not that they occur at fixed intervals in real time, but that the simulated time advances by a fixed interval each fixed update, and Unity executes as many such fixed updates as are necessary to 'catch up'.
     
  3. Peeling

    Peeling

    Joined:
    Nov 10, 2013
    Posts:
    443
    Second part, as to why your input behaves in a flaky manner:

    First, the obvious questions:
    • Have you checked to make sure that your deceleration code isn't kicking in on alternate frames?
    • How is m_Velocity actually used? Can it change outside of the code above?
    • Have you tried commenting out each of the lines in the acceleration code to see if the behaviour changes?
    Second, gazing deep into my crystal ball, and based on the fact you say it works in FixedUpdate, my intuition is as follows:

    By default, Unity runs with a very high/uncapped framerate in the editor, unrelated to monitor refresh rate. If your scene is very simple, framerate will be sky-high, which means Time.deltaTime will be very small.

    You say the problem manifests when moving from a standstill - okay, that means velocity is also very small.

    Everything you add to velocity2D is multiplied by Time.deltaTime, directly or indirectly. So you're also only adding very small numbers.

    Then, you normalise that very small vector and multiply it by its original magnitude. Depending on how normalisation is done and how magnitudes are calculated, that could introduce a LOT of rounding error.

    I think what's most likely happening is that rounding errors are preventing velocity2D from growing until you get a random 'slow' frame that increases Time.deltaTime and gets the velocity over that 'quantum noise' threshold, allowing it to grow.
     
  4. Edy

    Edy

    Joined:
    Jun 3, 2010
    Posts:
    2,510
    Here's a convenient guide on Update() vs. FixedUpdate():

    https://forum.unity.com/threads/update-vs-fixed-update-vs-late-update.307235/#post-5114657

    About the code, surely something is wrong in that math. I'd recommend you to reduce it to the simplest example possible, even using justo one dimension, until you get that working properly. Then add and test other features, one at a time.

    Camera code is recommended in LateUpdate(). It runs in the same cycle as Update(), but after all these have been executed in all the scripts. This way the camera will provide smooth motion no matter the display frame rate.

    Achieving consistent movement along all combinations of devices / display rates / vsync etc requires moving the character in FixedUpdate(). Frame interpolation should be implemented in Update() so the motion looks smooth at high display rates. Physics elements include the Interpolation option that does it.
     
    Last edited: Oct 15, 2021
  5. n_gon

    n_gon

    Joined:
    Oct 8, 2020
    Posts:
    10
    On the deceleration/acceleration code, it *should* be good. It's the same code I use in Unreal Engine and it works if I have it in fixedupdate. The only issue is if it's in Update(). I understand physics update should be used in FixedUpdate, but regardless I can't figure out why there would be an issue at all in Update() in this case.

    What I'm figuring out now is if essentially I have this in my loop:



    Vector2 velocity2D = new Vector2(m_Velocity.x, m_Velocity.z);

    velocity2D = velocity2D + adjustedInMoveVector * Time.deltaTime * m_InputAcceleration;
    velocity2D = velocity2D.normalized * Mathf.Clamp(velocity2D.magnitude, 0f, m_MovementSpeed);
    Debug.Log(velocity2D.x + " " + Time.deltaTime * m_InputAcceleration * adjustedInMoveVector.x + " " + m_Velocity.x);

    m_Velocity = new Vector3(velocity2D.x, m_Velocity.y, velocity2D.y);


    The debug log actually seems to indicate that velocity is zero multiple frames in a row when its stalling. velcoity2d.x is incremented to the exact value of the incrementing amount (because it's starting at 0,0,0 because velocity is 0,0,0).

    All I can think of...are Vector3's in Unity horribly imprecise? The incrementing value is about ~0.02 which is not an extremely tiny number....


    EDIT: To clarify...I get the same behavior where velocity gets stuck at 0 with just 3 lines of code.

    Vector2 velocity2D = new Vector2(m_Velocity.x, m_Velocity.z);         Debug.Log(m_Velocity.x);         
    velocity2D += new Vector2(.02f, 0f);
    m_Velocity = new Vector3(velocity2D.x, 0f, velocity2D.y);


    If I set 0.02f to a higher value like 1f it works fine. But at 0.02 it just stalls and the thing NEVER moves.
     
    Last edited: Oct 15, 2021
  6. Peeling

    Peeling

    Joined:
    Nov 10, 2013
    Posts:
    443
    Then the problem lies outside those lines of code. Try logging m_Velocity.x after it's been updated. If it's 0.02, then the only way it can be zero again on the next update is if it's been modified elsewhere.
     
    Last edited: Oct 15, 2021
  7. n_gon

    n_gon

    Joined:
    Oct 8, 2020
    Posts:
    10
    Solved!

    The problem was actually outside of the code I provided, in the Update() function...
            Vector3 delta = m_Velocity * Time.deltaTime;       
    Vector3 oldPos = transform.position;
    m_CharacterController.Move(delta);
    // Collision may cause disparities with our desired movement... Update velocity to reflect actual movement. m_Velocity = (transform.position - oldPos) / Time.deltaTime;


    This code is actually VERY necessary for a movement controller... You need to update the velocity to match the actual movement or collisions will mess up your acceleration code.

    HOWEVER, Unity has a field in their character controller called "Max Move Distance" which DEFAULTS to 0.001 despite the fact that the docs specifically state that this should be left at 0.00(...000) for most cases. So...essentially the move function was called but 99% of my frames were too fast and made it so the move distance was lower than the minimum move distance and then velocity was updating to '0' because the .move function didnt actually move anything. I had thought that I tried commenting this out, but apparently I had either not or was having another issue at the time, which is why I missed it being the potential problem. My apologies for posting a problem and not posting the correct part of the code that was causing the problem.

    I can't imagine this isn't an oversight - I actually found another post from someone who had similar trouble a while back.

    https://forum.unity.com/threads/psa-your-minimum-move-distance-may-be-too-high.483167/ (the moderator's replies in this case are actually extremely inaccurate)

    In all honesty...this field should be completely removed. With proper movement code, there is NO reason whatsoever to have a minimum move distance. It is just going to make your controller MORE clunky even if it doesn't just make it not work altogether. Either way, considering the docs actually state specifically that this should almost always be '0', they should at least consider changing the default to '0' from '0.001'. No one writing controllers is going to expect the move function to have an exception at low distances...no matter how you write your controller if it scales with deltaTime (which it always should), there is a framerate that this minimum distance value is going to glitch out your code.