In my Unity online multiplayer game players move very fast and I currently use Lerp to interpolate between their last position and current position:

transform.position = Vector3.Lerp(transform.position, this.correctPlayerPos, Time.deltaTime * smoothingFactor);

smoothingFactor is currently around 35f but I don't like the result. If I lower smoothingFactor, movements will be smoother but players will lag as well. If I increase it to like 40, it will be more precise but laggy.

How can I solve this?


There are two common patterns for use of Lerp. The one you're using is:

current = Lerp(current, target, sharpness)
(where sharpness is a constant between 0 and 1)

Note that there's a feedback loop here. The value of current is both an input and an output, so the value of current we use as input in the next frame is the output from the last frame.

This is good for when you want an easing effect: when the target is updated to something far from current, you get a fast initial movement, which slows down as the current value approaches the target. This looks nice in UI transitions and simple camera follow scripts.

In the context of interpolation for multiplayer though, it's probably not an ideal choice. Imagine another player running at a constant velocity across my screen. Their speed will spike each time I get a new update from the server (when target jumps ahead) and then slow to a crawl as they get close to the last update position. Even if I'm getting frequent updates, the constant increases & decreases in speed make it look jerky and low-performance.

Instead you probably want the other common Lerp pattern:

current = Lerp(previous, next, progress)
(where progress varies linearly from 0 to 1 over the expected duration of the move)

Here you store the position of the player in the most recent update from the server (next), and the position they were in when it was received (previous), and interpolate between them over one server update interval.

Note that neither of the inputs previous or next are modified by the Lerp, which prevents any feedback loop changing the behaviour of the formula over time.

Instead, the character moves with a constant velocity throughout the Lerp. In fact, if you let progress go past 1 in the event of a late or missing update packet from the server, then this also seamlessly handles a simple linear prediction, so the character won't stop and hesitate waiting for new server input (but at a risk of slight overshoot). This will generally look much smoother for this type of use.

  • \$\begingroup\$ if you let progress go past 1 in the event of a late or missing update packet from the server, then this also seamlessly handles a simple linear prediction - in some cases, it's not a late or missing update but the character just stopped moving and the server doesn't send any new data. \$\endgroup\$
    – Sergey
    Jul 4 '19 at 17:08
  • \$\begingroup\$ If you use a prediction scheme, then the server would be obligated to send a "stop" message in that case, rather than using the convention "no data = stop". \$\endgroup\$
    – DMGregory
    May 6 at 13:50

Thank you. This has been a very good explanation, regarding the progress, and the different ways of interpolation.

As I understood the issue: Having a constant interpolation factor between 0 and 1; Where lower values produce smoother but much delayed results, and higher values produce more accurate but choppy results.

What the progress calculation below does is treat interpolation as a variable, that increases and decreases when it needs to, handled by how far one is away from the Previous Position Received to the Next Position Received

This is the way I handled it. - Since my issue was, in a 'constant' interpolation factor where movement was smooth, the jump for example was extremely delayed, making the player seem like he's strafe-jumping in mid-air. This is where the 'progress' method comes in handy. Upon approaching the 'Next Position' say when the player lands, previously he was too 'laggy' to land accurately on the ground but rather drop half way from the jump before jumping again. So as the player drops down closer to the 'next position', the progress calculation gets closer to 100%, resulting in the interpolation toward the end of the jump get accelerated, smoothing the movement.

See below, I'm using Photon Unity Networking:

void Update () {
    clientPredictionValue = nManager.clientPredVal;
    velocityPredictionValue = nManager.velocityPredVal;

    if (photonView.isMine) {
    else {

        //Progress Calculation for Interpolation value
        if (prevPosition == Vector3.zero) {
            prevPosition = realPosition;
        currentDistance = Vector3.Distance (tr.position, realPosition);
        fullDistance = Vector3.Distance (prevPosition, realPosition);
        if (fullDistance != 0) {
            progress = currentDistance / fullDistance;
        prevPosition = realPosition;

        if (clientPredictionValue != 0) {
            progress *= clientPredictionValue;

        syncTime += Time.deltaTime;
        tr.rotation = Quaternion.Lerp(tr.rotation, realRotation, syncTime/syncDelay);
        tr.position = Vector3.Lerp (tr.position, realPosition, progress);

public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info){

    if (stream.isWriting) {
        //This is OUR player. We need to send our actual position to the network.

        stream.SendNext (tr.position);
        stream.SendNext (tr.rotation);
        stream.SendNext (pMovement.playerVelocity);
        stream.SendNext (anim.GetFloat ("AimAngle"));
        stream.SendNext (anim.GetFloat ("HorizontalMovement"));
        stream.SendNext (anim.GetFloat ("VerticalMovement"));
        stream.SendNext (anim.GetBool ("Jump"));
        //This is someone else's player. We need to receive their position (as of a few
        //milliseconds ago, and update our version of THAT player.

        // Right now, "RealPosition" holds the other's position at the LAST frame.
        // Instead of simply updating "realPosition" and continuing to lerp,
        // we MAY want to set our transform.position immediately to this old "realPosition"
        // and then update realPosition

        realPosition = (Vector3)stream.ReceiveNext ();
        realRotation = (Quaternion)stream.ReceiveNext ();
        playerVelocity = (Vector3)stream.ReceiveNext ();
        anim.SetFloat ("AimAngle", (float)stream.ReceiveNext ());
        anim.SetFloat ("HorizontalMovement", (float)stream.ReceiveNext ());
        anim.SetFloat ("VerticalMovement", (float)stream.ReceiveNext ());
        anim.SetBool ("Jump", (bool)stream.ReceiveNext ());

        syncTime = 0f;
        syncDelay = Time.time - lastSynchronizationTime;
        lastSynchronizationTime = Time.time;

        //Velocity Prediction
        realPosition = realPosition + playerVelocity * velocityPredictionValue * syncDelay;
  • \$\begingroup\$ clientPredictionValue = nManager.clientPredVal; velocityPredictionValue = nManager.velocityPredVal; How you are calculationg and finding these values?? \$\endgroup\$ Oct 5 '18 at 3:45
  • \$\begingroup\$ syncDelay = Time.time - lastSynchronizationTime; - if the player has been stationary for some time and the server hasn't been sending any new data, syncDelay will grow up to a huge number which doesn't seem to make any sense. \$\endgroup\$
    – Sergey
    Jul 5 '19 at 7:20
  • \$\begingroup\$ Due to the fact that the server stops sending data when the object remains in place, the syncDelay variable cannot show the actual interval between receiving data. \$\endgroup\$
    – Sergey
    Jul 5 '19 at 7:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.