How do I smoothly and realistically interpolate positions?

Started by
6 comments, last by oliii 8 years, 1 month ago

Hey!

I have a new player position coming every 50ms. This helped me achieve the smoothest interpolation:


newPos.x = MathUtils.lerp(getPosX(), interpolateToX, delta * 6f);
newPos.y = MathUtils.lerp(getPosY(), interpolateToY, delta * 6f);

However, this one feels too floaty and unrealistic. Especially, when character jumps, he slows down the closer he gets to the ground. I understand why this is happening. How could I interpolate smoothly but still realistically? Maybe somehow make time parameter dependant on distance?

Game I'm making - GANGFORT, Google Play, iTunes

Advertisement

Depending on the situation, you maybe could get away with simulating gravity and only interpolating the horizontal movement. I have no idea if this is true, but Guild wars 2 feels like it does this for proxy player characters (with occasional corrections).

It seems to me that you are currently overwriting the position in each interpolation step. How about saving the old and target position and calculate the current position by interpolating between those two.

So instead of


position = lerp(position, targetPosition, delta)

do


position = lerp(oldPosition, targetPosition, t)

Where t is a counter that is reset on every position update and that is increased according to elapsed time (make sure that it is clamped to [0,1]).

When a new position update arrives, it will become the new target position and the previous targetPosition will become the oldPosition.

Simmie is on it -- your current function is an exponential decay that updates each time a new position is received.
This will cause the movement to be large when the player has far to go, but small when the player is near, which means it will move stuttery if your frame rate is much higher than your network update rate.
A good way of testing these things is to set network update rate to something low (like 2 per second) and see how it goes.

There are also questions about whether you interpolate "in the past" (so that the position is accurate, but late) or "in the future" (so that the position is approximate, but up-to-date.)
In general, you want to calculate "targetPosition" from the reported position and the reported velocity to aim somewhere slightly in the future.

I wrote pretty good description of this, with a sample application and some code, here: http://www.mindcontrol.org/~hplus/epic/
enum Bool { True, False, FileNotFound };

Yeah, you need to keep a short history of the control points (position, orientation, but also timestamps). Two points are enough for linear interpolation. The character position / orientation is basically an interpolation / extrapolation of the path defined by the control points, and the current time on the character update.

To keep it simple, you can just do linear interpolation / extrapolation. Then you can expand to other more interesting algorithms.

Everything is better with Metal.

The character position / orientation is basically an interpolation / extrapolation of the path defined by the control points, and the current time on the character update.


That's not quite enough when you forward extrapolate, because a new update may show you that you interpolated "wrong" and a naive interpolator will then snap/jump the entity to the new position.
I prefer to do something like:
- when receiving update, calculate where the entity would be 100 ms from now, based on position/velocity
- now, subtract the current entity position from the projected entity position -- this is the movement vector for the next 100 ms
- divide by 100 ms to get the speed of movement

That way you correct mis-predictions by changing velocity, rather than snapping position, which ends up being smoother. The remote entity will be "chasing" the correct position at all times, and will be in the right spot about 99% of the time, and not too far off the rest of the time.

This is when forward extrapolating. If doing accurate/deterministic simulation, you have to instead do it in the "past" and thus you end up getting "lag behind" instead of "position discrepancies." Pick one of those two poisons, and design your game around it :-)
enum Bool { True, False, FileNotFound };

https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking

https://code.google.com/archive/p/nuclear-bomberman/wikis/Interpolation.wiki

After reading these 2 articles, I was able to implement mine.

An invisible text.

- when receiving update, calculate where the entity would be 100 ms from now, based on position/velocity
- now, subtract the current entity position from the projected entity position -- this is the movement vector for the next 100 ms
- divide by 100 ms to get the speed of movement


Yeah, snapping to extrapolation will generate some pretty bad jitter.

Have you used anything like this before?
// generic rigid body physics.
// simple linear / velocity vectors.
struct PhysicalBody
{
    Vector position;    // 3D position.
    Vector velocity;    // 3D velocity vector.
};

// a network update. Contains 
// the remote state of the physical body,
// as well as the time it was sent from. 
struct ControlPoint
{
    PhysicalBody body;  // body position and velocity.
    float t;            // time from when the body position was sent.
};

// calculate body position, extrapolating from a control point.
PhysicalBody extrapolate(ControlPoint cp, float t)
{
    PhysicalBody body;

    float dt = (t - cp.t);                          // extrapolation time.
    body.position = cp.position + cp.velocity * dt; // extrapolate (predict) using the body velocity.
    
    return body;
}

// interpolate between two bodies.
PhysicalBody interpolate(PhysicalBody a, PhysicalBody b, float u)
{
    PhysicalBody c;

    c.position = a.position + (b.position - a.position) * u; // simple linear interpolation.
    c.velocity = a.velocity + (b.velocity - a.velocity) * u; // simple linear interpolation.
    
    return c;
}


ControlPoint cp[2];         // the control points we used to extrapolate the remote player position.
float network_dt = 0.1f;    // network update period (100ms, or whatever your send rate is).
float snap_speed = 0.9f;    // how fast we snap to the new extrapolation. something sensible between [0.5f, 1.5f].
PhysicalBody player;        // the predicted remote player position.
float time;                 // the current time. 

// it's time to update the player position
// using two control points that we will extrapolate from.
void extrapolate_player()
{
    // extrapolate from the two control points.
    PhysicalBody a = extrapolate(cp[0], time);              // extrapolate to current time using the oldest control point.
    PhysicalBody b = extrapolate(cp[1], time);              // extrapolate to current time using the latest control point.

    // blend the two positions together. 
    // Once we're about ready to receive a new update 
    // (depending on snap_speed), we will be following 
    // the most up-to-date extrapolation.
    float blend_dt = network_dt * snap_speed;               // the network update rate, assuming it's constant. Add +/-10% in extra.
    float u = m_blend_timer.get_elapsed_time() / blend_dt;  // the blend factor between the two extrapolations.
    u = clamp(u, 0.0f, 1.0f);                               // clamp u in range [0.0f, 1.0f].
    
    // the new player position, 
    // blended between two extrapolations.
    player = interpolate(a, b, u);
}

// we received our first network update. 
// we can calculate extrapolations from here.
void receive_first_network_update(ControlPoint new_cp)
{
    // set both control points to the same value.
    cp[0] = new_cp;
    cp[1] = new_cp;

    // restart the blending.
    m_blend_timer.start();

    // use arbitrary value.
    network_dt = 0.1;
}

// network sent us a new update.
// update exrapolation cache.
void receive_new_network_update(ControlPoint new_cp)
{
    // measure the network update rate. 
    // although if the update rate is constant, network_dt could just be left alone.
    network_dt = (new_cp.t - cp[1].t); 

    // use the current body position and time 
    // as the oldest extrapolation path.
    cp[0].body = player;
    cp[0].t = time;

    // use the new control point as the latest extrpolation path.
    cp[1] = new_cp;

    // restart the blending.
    m_blend_timer.start();
}

NOTE : like all network games, the time between machines should be synchronised to a reasonable degree. all times (local current time, and netowrk update times) should refer to the same 'clock'.

Everything is better with Metal.

This topic is closed to new replies.

Advertisement