How do I determine how far to move an object in the client when using client-prediction?

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.

It can perhaps be seen more clearly without lerping

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?

Lerp:

        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,
blend);

transform.position = position;


• Can you show us your lerp code? There are lots of ways to implement interpolation, some appropriate to this context, some not. Commented Aug 26, 2018 at 13:37
• @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. Commented Aug 26, 2018 at 13:48
• 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? Commented Aug 26, 2018 at 14:02
• @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. Commented Aug 26, 2018 at 18:52
• @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. Commented Aug 26, 2018 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(
latestSnapshot.tunnelVelocityZOnArrival,
latestSnapshot.tunnelVelocityZ,
blend);

float oldEstimate = latestSnapshot.tunnelPositionZOnArrival +
velocity * dT;

latestSnapshot.tunnelVeloctyZ * dT;

Vector3 position = transform.position;

position.z = Mathf.Lerp(
oldEstimate,

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.