I have a game server which, for testing purposes, is updating once per second, or 1hz so I can correctly implement client-side prediction. Everything is running locally at the moment so there is no lag issues to deal with, but this setup replicates potential packet loss, or if the server somehow is behind on processing.

I am using Unity for the client. I have a tunnel where the position is updated on the server and then broadcast to each client each tick (once per second). The client simply then lerps between its current position, and the new position received from the server, and this seems to work nicely.

However, as I have set the tick to 1hz, there are unexpected results where the position seems to pulse, rather than smoothly transition. An example can be seen below.

enter image description here

It can perhaps be seen more clearly without lerping

enter image description here

I have tried constantly increasing the tunnel's position in the client, but if it's moved too far in the client, once the server's position is received, it is snapped back to that position which creates some weird rubber-banding like effects even though there is no lag.

How can I determine how much to move the tunnel in the client to create some seamless movement, even when the tick rate is so low? Baring in mind this movement is variable, so it can move faster/slower at times so the movement in the client cannot be a fixed number.

Yes, I can just increase the tick rate which gives smooth movement, however, there is no lag to account for as it's all local so I am just preparing for when I move the server to actual hosting.

Instead of lerping between the current position and the received position, should I be trying to lerp between the current position and the next received position?


        Vector3 currentPosition = transform.position;
        Vector3 newPosition = new Vector3(currentPosition.x, currentPosition.y, tunnelPositionZ);

        transform.position = Vector3.Lerp(currentPosition, newPosition, Time.deltaTime);

DMGregory's solution attempt:

If I decrease the 1.0f for the blend variable, the tunnel will move very slowly and then jolt forward when a snapshot is received. Decreasing the 1.0f to around 0.1f seems to align the speeds but there is still a noticeable jolt.

I'm using the same velocity for the currentPosition and newPosition because the velocity never changes.

    float dT = Time.time - latestSnapshot.arrivalTime;
    float blend = Mathf.Clamp01(dT / 1.0f);

    Vector3 currentPosition = transform.position;
    Vector3 currentVelocity = rigidbody.velocity;

    Vector3 newPosition = new Vector3(currentPosition.x, currentPosition.y, latestSnapshot.tunnelPositionZ);
    Vector3 newVelocity = new Vector3(currentVelocity.x, currentVelocity.y, latestSnapshot.tunnelVelocityZ);

    Vector3 velocity = Vector3.Lerp(currentVelocity, newVelocity, blend);

    Vector3 position = Vector3.Lerp(
               currentPosition + velocity * dT,
               newPosition + velocity * dT,

    transform.position = position;

enter image description here

  • \$\begingroup\$ Can you show us your lerp code? There are lots of ways to implement interpolation, some appropriate to this context, some not. \$\endgroup\$ – DMGregory Aug 26 '18 at 13:37
  • \$\begingroup\$ @DMGregory I have updated my OP. I'm more concerned with predicting the next position though as when the scenario becomes more complex I'm going to need such a feature. \$\endgroup\$ – jjmcc Aug 26 '18 at 13:48
  • \$\begingroup\$ Ah, I suspected you might be using Lerp with the pattern current = Lerp(current, target, blend);. This is good for chasing a continuously changing variable, but not for discrete periodic updates. (And the deltaTime correction used here is incorrect) Another user had a similar issue recently — does this previous Q&A help you at all? \$\endgroup\$ – DMGregory Aug 26 '18 at 14:02
  • \$\begingroup\$ @DMGregory I've had a go at implementing that and the lerping seems smoother and more consistent now, however it still finishes the lerp way before the next snapshot is received, so it just stops and does nothing. I've updated my OP with the example. One thing I have noticed, though, is that there is slow constant movement before a snapshot is received (when latestSnapshot.arrivalTime is 0), and then it starts to pulse when a snapshot is present. \$\endgroup\$ – jjmcc Aug 26 '18 at 18:52
  • \$\begingroup\$ @DMGregory The more I decrease the float value in the blend variable, the smoother the movement becomes. 0.01f seems to be nice for consistent movement, however I don't know if the client is now ahead of the server or vice-versa. \$\endgroup\$ – jjmcc Aug 26 '18 at 19:08

It looks like you're trying to separate the behaviour on the xy vs z axes, but you have not done so correctly.

float dT = Time.time - latestSnapshot.arrivalTime;
float blend = Mathf.Clamp01(dT / snapshotPeriod);

float velocity = Mathf.Lerp(

float oldEstimate = latestSnapshot.tunnelPositionZOnArrival + 
                    velocity * dT;

float newEstimate = latestSnapshot.tunnelPositionZ +
                    latestSnapshot.tunnelVeloctyZ * dT;

Vector3 position = transform.position;

position.z = Mathf.Lerp(

transform.position = position;

Note that the starting point for our oldEstimate is where we were when the snapshot was received, NOT where we are now. Using the current position instead of the historical position creates a feedback loop, and over-estimates forward progress when we add velocity * dT on top of it, producing the pulsing motion you observed.

When implemented correctly, projective velocity blending is C1 continuous (consistent velocity across snapshot transitions), and transitions naturally to dead reckoning prediction in the event that a snapshot is missed or arrives late.


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.