Search Unity

[INFO] How to properly lerp between movement updates

Discussion in 'Multiplayer' started by TwoTen, Jul 27, 2017.

  1. TwoTen

    TwoTen

    Joined:
    May 25, 2016
    Posts:
    1,168
    Hello!
    I am a fairly active guy over here at the Networking section. I try to help out. And more often that I would like, I see a common mistake.

    Most people setup their multiplayer games as follows:
    Movement get's calculated on client.
    Client sends the input to server
    (Optional) Server verifies if the distance is realistic and maybe even does physics checks.
    Server sends position to other clients
    Clients store the position they got from server as a Vector3,
    and each Update. They lerp towards that position. Usually with some int multiplied with Time.deltatime for the t parameter.

    The problem with this is:
    To explain this and make it easy to understand. Lets remove the Time.deltaTime and pretend that the Update runs 30 times a second.

    If our int called LerpSpeed is then something like 0.25.
    Everytime update is called. We get 25% closer. In theory, this means we never actually reach the target. We just get 25% closer every time. And the first few iterations of the Update is going to move the player more than the later ones. You can start to see the problem?

    (I even found a open PR on the HLAPI where this was present)
    https://bitbucket.org/Unity-Technol...networktransform-interpolation-when-used/diff

    Solution:
    The solutions are fairly simple.
    One option is to always have two values stored. The target, and the position when the lerp started. Then lerp from the position where we started, to the target position.

    Option number two is to use a very handy method.
    Transform.MoveTowards. It will move towards the target with the same speed all the time.
    Using that you can use MoveTowards from current position to targetPosition. And your t can be a LerpSpeed * Time.deltaTime.

    And if you send position at a fixed interval. You can even do more clever things:
    You can basically calculate the distance between currentPosition and targetPosition when you get it. And then you can calculate the LerpSpeed (You would do this each time you get a update from Server) so that the MoveTowars will have reached it's destination aproximatly when the next update arrives.

    There is also a rotation equivilant for this. Quaterion.RotateTowards, works the same way.


    And for the more advanced people who would like to combine Interpolation and Extrapolation. You could calculate the direction vector from currentPos and targetPos, and use MoveTowards but the targetPos is the direction (Basically extended). Ofcourse to do this you would also have to notify the clients when somebody stops, otherwise people would just fly all over the place. But that's for you guys to look into ;)


    Hope you found this helpful,
    Thanks - TwoTen
     
    Last edited: Jul 27, 2017
  2. angusmf

    angusmf

    Joined:
    Jan 19, 2015
    Posts:
    261
    Since we were discussing this on the PR anyway, might as well talk here and share with others.

    You've explained the difference between lerp and MoveTowards, but your explanation of why one is more appropriate doesn't immediately make sense to me. Unless you know or calculate the velocity, you don't know how much to move the object on each frame to achieve a smooth look or to reach the target at the "right" time (which as you point out here is really extrapolation anyway.) Your example code in the PR factored in send rate, but I'm not sure why since you don't know the velocity (edit: I think I get it now. Assuming a static send rate, a you said, deltaTime / rate sort of gives you a velocity?) Neither method guarantees you will be at the target at the "right" time. Interpolation on each frame will cause an easing effect as long as it's a sane system and you recycle the position. The main difference here is that one method gives a constant rate, which I argued is not appropriate since you have no idea what your positional delta is when you set that rate. Applying deltaTime to t makes sense and is missing from the PR. Lerp has the additional property of easing out of the motion which may be undesirable, but this calls for a small t anyway. It might also make sense to do both (ease in and out), which can be done by applying sin to t when lerping. https://forum.unity.com/threads/a-smooth-ease-in-out-version-of-lerp.28312/

    I'd like a better explanation of why the PR is "wrong" in any case, so here we are on the forum. My best guess is that you can't really do a perfect job on this. Assuming you are using some easing when moving the master copy, it's probably best to use the same method of moving the transform on the client copy as you do on the master, in which case using the network send rate as a factor could be useful!

    BTW, regarding what lerp (linear interpolation) means or "must" be used for: I believe linear simply refers to the fact that the two points of interpolation could represent a line. The output is never (except when t =0 or 1?) linear.

    Edit:

    After re-reading a lot, based on your sample code in the PR, my new understanding is this:
    Dividing by send rate potentially gives you a rough rate of change, v. If so, you can apply this to your easing function, whichever you use. That's the really important thing here, if I got it right. For lerp, t = v. For MoveTowards t = v * positional delta. MoveTowards has the advantage (?) that the integration of the original velocity curve will be made of straight lines instead of the sloping lines of lerp. If you also know that v is increasing or decreasing, lerp would give you a better fit to a curve. Sin Lerp would make a wavy approximation.
    Snapping to a minimum delta would be a useful optimisation, but it's the only thing I can see you automatically get by using MoveTowards. I can make a straight line myself...
     
    Last edited: Feb 28, 2018
  3. TwoTen

    TwoTen

    Joined:
    May 25, 2016
    Posts:
    1,168
    This might make things a bit more clear:
     
    Deleted User likes this.
  4. angusmf

    angusmf

    Joined:
    Jan 19, 2015
    Posts:
    261
    So I did quite a bit of testing on this. I implemented:
    - your MoveTowards solution (I think...still never saw your implementation because I couldn't see your video on my phone)
    - the same solution using Lerp (Does require changing a few params)
    -the solution in the PR
    -the solution in the PR but * fixedDeltaTime, which is a very small .02 in my case.

    First thing to note is that smoothness itself was not significantly affected most of time, even at my low send rate of 9. Reducing the rate to 1 or 2 made things much more noticeable, though. There were occasional glaring glitches, however, when using the PR-based solutions at 9 sends/s. Implemented with the changes below, Lerp and MoveTowards might as well have been identical as far as my aging eyes can see, even at 9 sends, which is even lower than cheap/old animation.

    An easing function is "properly" used to move a value in steps from a start to a finish (0 to 1) by a delta (t), which TwoTen points out is a scale that can be applied to the distance between two position syncs. The different easing functions will require different inputs, but they are all derived from the same things.

    Two major things are missing from the PR solution to implement "proper" easing, which my testing shows is objectively improved as TwoTen promises. The "Calculated delta" we feed to the easing function must:
    a) be incremented each frame until a send is received, then zeroed. In other words, t += [Calculated delta], not t = ...
    b) be divided by the send rate, since a "unit of sent" is what you are trying to scale. In other words again, t += [Partially Calculated delta] / network send rate

    Additionally, you must either ignore the interpolation factor, or leave the default of 1.0f.

    My take:
    Code (CSharp):
    1. if (m_InterpolateMovement > 0)
    2.             {
    3.                 m_PositionDelta += Time.fixedDeltaTime * m_InterpolateMovement * m_DistanceSinceSync / GetNetworkSendInterval();
    4.                 transform.position = Vector3.MoveTowards(transform.position, m_TargetSyncPosition, m_PositionDelta);
    5.             }
    6.  
    7.  
    8.             if (m_InterpolateMovement > 0)
    9.             {
    10.                 m_PositionDelta += Time.fixedDeltaTime * m_InterpolateMovement / GetNetworkSendInterval();
    11.                 transform.position = Vector3.Lerp(m_LastSyncPosition, m_TargetSyncPosition, m_PositionDelta);
    12.             }
    Thanks again for the discussion (and the video, sorry I didn't watch it.) You've wondered why people use Lerp the "wrong" way. I think it's two things. First, it gets you rather close without having to really think (or know) about transformations. Just apply the formula you see repeated every day on the forums. Which is the second thing. Poor code that "works" is really hard to stamp out. It spreads, actually. "Learning to Unity" from the internet is a lot like learning on the job. A lot of what folks tell you is crap for a variety of reasons.
     
    Last edited: Mar 1, 2018
  5. TwoTen

    TwoTen

    Joined:
    May 25, 2016
    Posts:
    1,168
    https://github.com/TwoTenPvP/Unity-LerpShowcase
    The video shows that project. Shows the different lerp methods. But the video explains their flaws and how to solve them.
    https://github.com/TwoTenPvP/Unity-LerpShowcase/blob/master/Assets/Controller.cs
     
    captainfuzzyfeet likes this.
  6. angusmf

    angusmf

    Joined:
    Jan 19, 2015
    Posts:
    261
    Fixed https://bitbucket.org/Unity-Technol...plement-transform-interpolation-based-on/diff

    The object now generally arrives where it's meant to be on time with no stutters or "fuzzy" animation.

    I opted not to use MoveTowards (I effed up a little bit by leaving the cached distance calculation in, but at least that will be handy if anyone wants to change to MoveTowards) as the motion looked subjectively worse (sudden changes in velocity) at very low send rates. Lerp gave more of an impression of interia, which is appropriate in my case. As mentioned above, I also tested at 9 sends, which is still very low, and the difference between the two easing methods is imperceptible to me.


    On to the next one...
     
    Last edited: Mar 1, 2018
  7. TwoTen

    TwoTen

    Joined:
    May 25, 2016
    Posts:
    1,168
    If you implement both methods properly as in the example on the github repo I linked there is no difference between the two. I'll be commenting some improvements on your PR
     
  8. angusmf

    angusmf

    Joined:
    Jan 19, 2015
    Posts:
    261
    Unless moving the interpolation to Update changes anything, this is demonstrably untrue.

    Maybe this will help you explain it to me...

     
    Last edited: Mar 1, 2018
  9. TwoTen

    TwoTen

    Joined:
    May 25, 2016
    Posts:
    1,168
    Moving it to update just means more smoothness. It will be smoothed out and be moved every frame rather than every physics update.
     
  10. angusmf

    angusmf

    Joined:
    Jan 19, 2015
    Posts:
    261
    Exactly, it doesn't change anything. However Lerp and MoveTowards produce two different curves. MoveTowards has _no_ curve, while Lerp accelerates at the end. This is directly reflected in the motion, as the only times they will be at the same position will be start and end. I still haven't tried your project, but do whatever you have to to set your virtual send rate low enough to "see" the difference. Or watch the video I linked above.
     
  11. TwoTen

    TwoTen

    Joined:
    May 25, 2016
    Posts:
    1,168
    No, the lerp will move at a constant rate. T is just a value between 0-1. We increase it by the same amount every frame to make it reach 1 when we are about to get the next update.

    The project I created allows you to set low sendrates (such as 0-1 sends per second) and also customize the rate at which the lerp occurs (to simulate different framerates). You will see that the MoveTowards and Lerp will produce the same result. And it makes sense. If T increases with an equal amount at all times. Then we get an equal % closer each time aswell
     
  12. angusmf

    angusmf

    Joined:
    Jan 19, 2015
    Posts:
    261
    Ok...I guess I messed something up in testing. I'll try again.
     
    Dunngeon1 likes this.
  13. TwoTen

    TwoTen

    Joined:
    May 25, 2016
    Posts:
    1,168
    Basically. The lerp function gives the position at a certain percentage between two points (the percentage being t). Thus, we make a counter that counts fast enough to reach the number 1 (100%) as the next packet is about to arrive.
     
  14. angusmf

    angusmf

    Joined:
    Jan 19, 2015
    Posts:
    261
    This must have been a testing mistake on my part and I was seeing incorrect behavior when I thought I was testing MoveTowards. The reason I see the interial effect at all is because I intentionally Lerp the position of the object on the server while recycling the transform position. It ends up behaving like SmoothStep, since the input is effectively squared. I may have stolen that idea from some unity tutorial years ago. It's probably an animation ("tweening") technique. Anyway, that's how I wound up trying to argue that Lerp and MoveTowards implement two different systems...
     
  15. AurimasBlazulionis

    AurimasBlazulionis

    Joined:
    Aug 13, 2013
    Posts:
    209
    You would also want to interpolate between 3 points, because due to latency fluctuations a packet might come later than you finish interpolating making the game feel a bit jittery.
     
  16. angusmf

    angusmf

    Joined:
    Jan 19, 2015
    Posts:
    261
    Lol. Will update the PR with this as soon as I can get to it.

    Anything else??? :) Is there a simple missed packet strategy? My instinct is to start numbering the sync events and go from there.
     
    Last edited: Mar 4, 2018
  17. TwoTen

    TwoTen

    Joined:
    May 25, 2016
    Posts:
    1,168
    If you miss a packet in the current implementation. Nothing has to be done. If however you start to keep a buffer. You might want to implement a form of slowdown in order to catch up and refill the buffer once a packet is lost. You don't need to number the packets so long you use a Unreliable Ordered method. Or possibly a unreliable state update. Then they will automatically be dropped if out of order which is probably what you want. I think it's to messy to try and reorder them in order to keep a pool of packets.