I love this topic!
Firstly, I want to briefly talk about a protocol on top of UDP I’ve created that I think is useful.
Secondly, I made a simple networked game (using UDP) all in a few hours last night with interpolation and extrapolation - I’ll post some code.
A few concepts:
- Multiple “channels” can be setup to differentiate between reliability and order.
- A channel is marked as unreliable or reliable, and ordered or unordered.
- A command is sent through a channel, has a retry count, a priority, has a determinable size in bytes, and can be read/written to a ByteBuffer
- A packet is formatted [protocolId][sequence][ack][ackuence][command id+channel][command data][command id+channel][command data…]
Where sequence is the remote packet sequence, ack is the last packet sequence received by the remote address, ackuence is 32-bits which stores which of the past 32 packets have been received by the remote address.
The strength of this system lies in the amount of data you send that you don’t care about. The more of that there is, the better this system performs over TCP (plus TCP acks one at a time, this does 32).
This is a library I’ve developed for my game engine, I’m going to have it available separately however.
Anyway, onto my experimentation last night!
The world consists of ships with position, velocity, direction:
public class Player extends SteerSprite
{
public byte id;
public String name;
public long lastUpdateTime;
public float ping;
public float time;
public Tween<Vec2f> tweenPosition = new Tween<Vec2f>( new Vec2f(), new Vec2f() );
public Tween<Vec2f> tweenVelocity = new Tween<Vec2f>( new Vec2f(), new Vec2f() );
public Tween<Vec2f> tweenDirection = new Tween<Vec2f>( new Vec2f(), new Vec2f() );
public Vec2f position = new Vec2f();
public Vec2f velocity = new Vec2f();
public Vec2f direction = new Vec2f();
}
When I get the command that someone has joined, I do this:
Player p = new Player();
p.id = joined.id;
p.name = joined.name;
p.lastUpdateTime = System.currentTimeMillis();
p.time = 0;
p.ping = INTERVAL_CLIENT * 0.001f; // expected (INTERVAL_CLIENT = 50 which means 20 packets per second)
When I get the command that someone has updated, I do this:
long lastUpdateTime = p.lastUpdateTime;
p.lastUpdateTime = System.currentTimeMillis();
p.time = 0;
p.ping = (p.lastUpdateTime - lastUpdateTime) * 0.001f;
// important for smoothness, the beginning of the tween should start in the players current position
p.tweenPosition.start.set( p.position );
p.tweenDirection.start.set( p.direction );
p.tweenVelocity.start.set( p.velocity );
// the most recent position, velocity, and direction
p.tweenPosition.end.set( update.x, update.y );
p.tweenDirection.end.set( update.dx, update.dy );
p.tweenVelocity.end.set( update.vx, update.vy );
And most importantly, here’s my update code:
p.time += gameState.seconds; // deltatime
if ( p.time <= p.ping )
{
// Interpolate
p.tweenPosition.set( p.position, p.time / p.ping ); // basically p.position = (p.tweenPosition.end - p.tweenPosition.start) * (p.time / p.ping ) + p.tweenPosition.start
p.tweenDirection.set( p.direction, p.time / p.ping );
p.tweenVelocity.set( p.velocity, p.time / p.ping );
}
else
{
// Extrapolate
Vec2f subp = Vec2f.sub( p.tweenPosition.end, p.tweenPosition.start );
Vec2f subd = Vec2f.sub( p.tweenDirection.end, p.tweenDirection.start );
Vec2f subv = Vec2f.sub( p.tweenVelocity.end, p.tweenVelocity.start );
p.position.add( subp, gameState.seconds ); // deltatime
p.direction.add( subd, gameState.seconds );
p.velocity.add( subv, gameState.seconds );
}
Results in perfectly smooth movement! However, the state of the ship you see on screen is out of date… but its worth the smoothness.
Hopefully this is helpful to someone…