Little interpolation example


#1

Hi,



i’m trying to do an interpolation system for my multiplayer games.

It is really simple and every advice is welcome :slight_smile:

If you want to try to run this example copy this code:



GameClient:

[java]

package it.mygame.simpletest;



import com.jme3.app.SimpleApplication;

import com.jme3.asset.TextureKey;

import com.jme3.cinematic.events.MotionTrack;

import com.jme3.input.controls.ActionListener;

import com.jme3.input.controls.KeyTrigger;

import com.jme3.light.DirectionalLight;

import com.jme3.material.Material;

import com.jme3.math.ColorRGBA;

import com.jme3.math.Vector2f;

import com.jme3.math.Vector3f;

import com.jme3.network.Client;

import com.jme3.network.Message;

import com.jme3.network.MessageListener;

import com.jme3.network.Network;

import com.jme3.scene.Geometry;

import com.jme3.scene.shape.Box;

import com.jme3.system.AppSettings;

import com.jme3.texture.Texture;

import com.jme3.texture.Texture.WrapMode;

import java.io.IOException;

import java.util.LinkedList;

import java.util.List;

import java.util.concurrent.Callable;

import java.util.logging.Level;

import java.util.logging.Logger;



/**

  • @author Alessio

    */

    public class GameClient extends SimpleApplication implements

    MessageListener, ActionListener {



    private Geometry localPlayer;

    private Geometry interpolatedPlayer;

    private Geometry serverPlayer;



    private InterpolationControl interpolationControl;



    private Client client;

    private MotionTrack motionControl;



    private long DELAY = 250L;

    private long lastDelay = DELAY + (long) Math.random() * 100;

    private List<UpdateMessage> messageQueue = new LinkedList<UpdateMessage>();



    private boolean server = true;

    private boolean local = true;

    private boolean interpolated = true;



    public static void main(String[] args) {

    GameClient client = new GameClient();

    AppSettings settings = new AppSettings(true);

    settings.setFrameRate(-1);

    client.setSettings(settings);

    client.start();

    }



    @Override

    public void simpleInitApp() {

    GameServer.serializeAllMessages();

    initFloorAndPlayers();

    initKeys();

    initNetwork();



    cam.getLocation().setY(5);

    interpolationControl = new InterpolationControl();

    interpolationControl.setSpatial(interpolatedPlayer);

    interpolatedPlayer.addControl(interpolationControl);

    }



    @Override

    public void simpleUpdate(float tpf) {

    // Check if there are messages that have to be processed

    UpdateMessage um;

    if (!messageQueue.isEmpty()) {

    um = messageQueue.get(0);

    } else {

    um = null;

    }

    // Simulate network latency

    if (um != null) {

    if (System.currentTimeMillis() >= um.time + lastDelay) {

    messageQueue.remove(0);

    // If latency has passed update states

    localPlayer.setLocalRotation(um.newState.rotation);

    localPlayer.setLocalTranslation(um.newState.position);

    // Update interpolation control

    interpolationControl.addState(um.newState);

    // Generate a delta latency to simulate network viaribility

    lastDelay = DELAY + (long) Math.random() * 100;

    }

    }

    }



    public void messageReceived(Object source, Message m) {

    if (m instanceof UpdateMessage) {

    final UpdateMessage um = (UpdateMessage) m;

    this.enqueue(new Callable<Void>(){

    public Void call() throws Exception {

    messageQueue.add(um);

    // Update immediatly remote player to see his most up-to-date state

    serverPlayer.setLocalRotation(um.newState.rotation);

    serverPlayer.setLocalTranslation(um.newState.position);

    return null;

    }

    });

    }

    }



    private void initFloorAndPlayers() {

    // Init floor

    Box floor = new Box(Vector3f.ZERO, 10f, 0.1f, 5f);

    floor.scaleTextureCoordinates(new Vector2f(3, 6));

    Material floor_mat = new Material(assetManager,

    "Common/MatDefs/Misc/Unshaded.j3md");

    TextureKey key3 = new TextureKey("Textures/Terrain/Pond/Pond.jpg");

    key3.setGenerateMips(true);

    Texture tex3 = assetManager.loadTexture(key3);

    tex3.setWrap(WrapMode.Repeat);

    floor_mat.setTexture("ColorMap", tex3);



    Geometry floor_geo = new Geometry("Floor", floor);

    floor_geo.setMaterial(floor_mat);

    floor_geo.setLocalTranslation(0, -0.1f, 0);

    this.rootNode.attachChild(floor_geo);



    // Init local non-interpolated player

    Box b = new Box(Vector3f.ZERO, 0.5f, 0.5f, 0.5f);

    localPlayer = new Geometry("Box", b);

    Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");

    mat.setColor("Color", ColorRGBA.Blue);

    localPlayer.setMaterial(mat);

    localPlayer.getLocalTranslation().setY(0.5f);

    rootNode.attachChild(localPlayer);



    // init interpolated player

    interpolatedPlayer = new Geometry("Box2", b);

    Material mat2 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");

    mat2.setColor("Color", ColorRGBA.Green);

    interpolatedPlayer.setMaterial(mat2);

    interpolatedPlayer.getLocalTranslation().setY(0.5f);

    rootNode.attachChild(interpolatedPlayer);



    // iniit remote player

    serverPlayer = new Geometry("Box3", b);

    Material mat3 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");

    mat3.setColor("Color", ColorRGBA.Yellow);

    serverPlayer.setMaterial(mat3);

    serverPlayer.getLocalTranslation().setY(0.5f);

    rootNode.attachChild(serverPlayer);



    DirectionalLight light = new DirectionalLight();

    light.setDirection(new Vector3f(0, -1, 0));

    light.setColor(ColorRGBA.White);

    rootNode.addLight(light);

    }



    private void initNetwork() {

    try {

    client = Network.connectToServer("127.0.0.1", 65530, 65531);

    client.addMessageListener(this);

    client.start();

    } catch (IOException ex) {

    Logger.getAnonymousLogger().log(Level.SEVERE, "It is not possible"
  • " to connect the server due to {0}", ex.getMessage());

    ex.printStackTrace();

    System.exit(1);

    }

    }



    @Override

    public void destroy() {

    client.close();

    super.destroy();

    }



    private void initKeys() {

    // Enable - disable displaying of remote player

    inputManager.addMapping("server", new KeyTrigger(keyInput.KEY_P));

    // Enable - disable displaying of local player

    inputManager.addMapping("local", new KeyTrigger(keyInput.KEY_O));

    // Enable - disable displaying of interpolated player

    inputManager.addMapping("interpolated", new KeyTrigger(keyInput.KEY_I));

    // Add - remove some delay

    inputManager.addMapping("delay_up", new KeyTrigger(keyInput.KEY_L));

    inputManager.addMapping("delay_down", new KeyTrigger(keyInput.KEY_K));

    // Adjust interpolation factor

    inputManager.addMapping("interp_factor_up", new KeyTrigger(keyInput.KEY_U));

    inputManager.addMapping("interp_factor_down", new KeyTrigger(keyInput.KEY_Y));



    inputManager.addListener(this,

    "server",

    "local",

    "interpolated",

    "delay_up",

    "delay_down",

    "interp_factor_up",

    "interp_factor_down");

    }



    public void onAction(String name, boolean isPressed, float tpf) {

    if (name.equals("server") && !isPressed) {

    server = !server;

    if (server) {

    rootNode.attachChild(serverPlayer);

    } else {

    rootNode.detachChild(serverPlayer);

    }

    }



    if (name.equals("local") && !isPressed) {

    local = !local;

    if (local) {

    rootNode.attachChild(localPlayer);

    } else {

    rootNode.detachChild(localPlayer);

    }

    }



    if (name.equals("interpolated") && !isPressed) {

    interpolated = !interpolated;

    if (interpolated) {

    rootNode.attachChild(interpolatedPlayer);

    } else {

    rootNode.detachChild(interpolatedPlayer);

    }

    }



    if (name.equals("delay_up") && !isPressed) {

    DELAY += 25;

    System.out.println("Min Delay:t" + DELAY);

    }



    if (name.equals("delay_down") && !isPressed) {

    DELAY -= 25;

    System.out.println("Min Delay:t" + DELAY);

    }



    if (name.equals("interp_factor_up") && !isPressed) {

    interpolationControl.interpFactor += 5.0f;

    System.out.println("interp factor:t" + interpolationControl.interpFactor);

    }



    if (name.equals("interp_factor_down") && !isPressed) {

    interpolationControl.interpFactor -= 5.0f;

    if (interpolationControl.interpFactor < 0){

    interpolationControl.interpFactor = 0.0f;

    }

    System.out.println("interp factor:t" + interpolationControl.interpFactor);

    }

    }



    }

    [/java]



    GameSerrver:

    [java]

    package it.mygame.simpletest;



    import com.jme3.animation.LoopMode;

    import com.jme3.app.SimpleApplication;

    import com.jme3.cinematic.MotionPath;

    import com.jme3.cinematic.events.MotionTrack;

    import com.jme3.material.Material;

    import com.jme3.math.ColorRGBA;

    import com.jme3.math.FastMath;

    import com.jme3.math.Quaternion;

    import com.jme3.math.Vector3f;

    import com.jme3.network.ConnectionListener;

    import com.jme3.network.HostedConnection;

    import com.jme3.network.Message;

    import com.jme3.network.MessageListener;

    import com.jme3.network.Network;

    import com.jme3.network.Server;

    import com.jme3.network.serializing.Serializer;

    import com.jme3.scene.Geometry;

    import com.jme3.scene.shape.Box;

    import com.jme3.system.JmeContext;

    import java.io.IOException;

    import java.util.logging.Level;

    import java.util.logging.Logger;



    /**

    *
  • @author Alessio

    */

    public class GameServer extends SimpleApplication implements

    ConnectionListener, MessageListener<HostedConnection> {



    private boolean start = false;

    private MotionTrack motionControl;



    private float syncFreq = 1/5; // sync 10 times per second

    private float dropRate = (float) 5/10; // 30 % packet loss

    private float syncTime = 0.0f;



    private Geometry newPlayer;



    private Server server;

    private static final Logger logger =

    Logger.getLogger(GameServer.class.getName());



    public static void main(String[] args) {

    GameServer server = new GameServer();

    server.start(JmeContext.Type.Headless);

    }



    @Override

    public void simpleInitApp() {

    serializeAllMessages();

    initNetwork();

    initPlayer();



    // Create the track of the simulated remote player

    MotionPath path = new MotionPath();

    path.addWayPoint(new Vector3f(8, 0.5f, 0));

    path.addWayPoint(new Vector3f(3, 0.5f, 5));

    path.addWayPoint(new Vector3f(-5.0f, 0.5f, -5.0f));

    path.addWayPoint(new Vector3f(-5.0f, 0.5f, 5));

    path.addWayPoint(new Vector3f(5, 0.5f, -5.0f));

    path.addWayPoint(new Vector3f(5, 0.5f, 0));

    path.addWayPoint(new Vector3f(8, 0.5f, 0));



    motionControl = new MotionTrack(newPlayer,path);

    motionControl.setDirectionType(MotionTrack.Direction.PathAndRotation);

    motionControl.setRotation(new Quaternion().fromAngleNormalAxis(-FastMath.HALF_PI, Vector3f.UNIT_Y));

    motionControl.setInitialDuration(10f);

    motionControl.setSpeed(1.5f);

    motionControl.setLoopMode(LoopMode.Loop);

    }



    @Override

    public void simpleUpdate(float tpf) {

    if (start) {

    syncTime += tpf;

    if(syncTime >= syncFreq) {

    // Simulate the packet loss

    float x = FastMath.nextRandomFloat();

    if (x < dropRate) {

    State newState = new State(newPlayer.getLocalTranslation(),

    newPlayer.getLocalRotation());

    server.broadcast(new UpdateMessage(true, newState,

    System.currentTimeMillis()));

    }

    syncTime = 0.0f;

    }

    }

    }



    public void messageReceived(HostedConnection source, Message m) {

    }



    private void initNetwork() {

    try {

    server = Network.createServer(65530, 65531);

    server.addConnectionListener(this);

    server.start();

    logger.log(Level.ALL, "Server ready");

    } catch (IOException ex) {

    logger.log(Level.SEVERE, "It is not possible to start a network"
  • " server due to {0}", ex.getMessage());

    ex.printStackTrace();

    System.exit(1);

    }

    }



    private void initPlayer() {

    // Inizializzo il giocatore

    Box b = new Box(Vector3f.ZERO, 1, 1, 1);

    newPlayer = new Geometry("player", b);

    Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");

    mat.setColor("Color", ColorRGBA.Blue);

    newPlayer.setMaterial(mat);

    rootNode.attachChild(newPlayer);

    }



    public static void serializeAllMessages() {

    Serializer.registerClass(UpdateMessage.class);

    Serializer.registerClass(State.class);

    }



    public void connectionAdded(Server server, HostedConnection conn) {

    start = true;

    motionControl.play();

    System.out.println("Client connected");

    }



    public void connectionRemoved(Server server, HostedConnection conn) {



    }



    }

    [/java]



    State:

    [java]

    package it.mygame.simpletest;



    import com.jme3.math.Quaternion;

    import com.jme3.math.Vector3f;

    import com.jme3.network.serializing.Serializable;



    /**

    *
  • @author Alessio

    */

    @Serializable

    public class State {



    public Vector3f position;

    public Quaternion rotation;



    public State() {}



    public State(Vector3f position, Quaternion rotation) {

    this.position = position;

    this.rotation = rotation;

    }



    }

    [/java]



    UpdateMessage

    [java]

    package it.mygame.simpletest;



    import com.jme3.network.AbstractMessage;

    import com.jme3.network.serializing.Serializable;



    /**

    *
  • @author Alessio

    */

    @Serializable

    public class UpdateMessage extends AbstractMessage {



    public long time;

    public State newState;



    public UpdateMessage(){}



    public UpdateMessage(boolean tcp, State state, long time) {

    super(tcp);

    this.newState = state;

    this.time = time;

    }



    }

    [/java]



    InterpolationControl:

    [java]

    package it.mygame.simpletest;



    import com.jme3.export.JmeExporter;

    import com.jme3.export.JmeImporter;

    import com.jme3.math.FastMath;

    import com.jme3.math.Quaternion;

    import com.jme3.math.Vector3f;

    import com.jme3.renderer.RenderManager;

    import com.jme3.renderer.ViewPort;

    import com.jme3.scene.Spatial;

    import com.jme3.scene.control.Control;

    import java.io.IOException;

    import java.util.LinkedList;

    import java.util.List;



    /**

    *
  • @author Alessio

    */

    public class InterpolationControl implements Control {



    private List<State> intStateList =

    new LinkedList<State>();



    private Vector3f currentTranslation = Vector3f.ZERO;

    private Vector3f targetTranslation = Vector3f.ZERO;

    private Vector3f startTranslation = Vector3f.ZERO;



    private Quaternion currentRotation = Quaternion.IDENTITY;

    private Quaternion targetRotation = Quaternion.IDENTITY;

    private Quaternion startRotation = Quaternion.IDENTITY;



    private float currentScale = 0.0f;

    public float interpFactor = 45.0f;

    private Spatial player;



    public boolean refresh = true;



    public void update(float tpf) {



    if (currentScale > 0.95f && intStateList.size() > 0) {

    State nextTargetState = intStateList.remove(0);

    // Set new target position

    startTranslation = currentTranslation;

    targetTranslation = nextTargetState.position;

    // Set new target rotation

    startRotation = currentRotation;

    targetRotation = nextTargetState.rotation;

    // reset the scale

    currentScale = 0.0f;

    }



    // Update the scale

    currentScale += tpf * interpFactor;



    // Interpolate start position to target position

    currentTranslation = FastMath.interpolateLinear(

    currentScale,

    startTranslation,

    targetTranslation);



    // Interpolate start rotation to target rotation

    currentRotation.slerp(startRotation, targetRotation, currentScale);



    player.setLocalTranslation(currentTranslation);

    player.setLocalRotation(currentRotation);

    }



    public void addState(State newState) {

    intStateList.add(newState);

    }



    //

//

public void setSpatial(Spatial spatial) {
player = spatial;
}

public Control cloneForSpatial(Spatial spatial) {
throw new UnsupportedOperationException("Not supported yet.");
}

public void setEnabled(boolean enabled) {
throw new UnsupportedOperationException("Not supported yet.");
}

public boolean isEnabled() {
throw new UnsupportedOperationException("Not supported yet.");
}

public void render(RenderManager rm, ViewPort vp) {
}

public void write(JmeExporter ex) throws IOException {
throw new UnsupportedOperationException("Not supported yet.");
}

public void read(JmeImporter im) throws IOException {
throw new UnsupportedOperationException("Not supported yet.");
}

}
[/java]

Some explainations:
On server a cube moves over a motion track in a loop and this movements are sampled and sent to client i with a frequency = GameServer.syncFreq. Because i run this example in a LAN i need to simulate packet loss with a GameServer.dropRate possibility.

On client is simulated a latency delaying the processing of the messages using a queue. At start this latency is set to deafult to 250 ms and varies of another 100 ms to simulate variability of network. For simplicity it is assumed that packets arrives in order (in UDP it is not guranteed but it is simple to check priority simply comparing the last message time and the arriving message. if the arriving message has a time lower than the last in the queue it is rejected)

On the client there are 3 entities:
The server player -> is the state of the player on the server side so the most up - to - date state.
The local player -> is the state where the delay is applied but is not interpolated
The interpolated player -> is the interpolated version of local client.

The InterpolationControl does all the job:
here there is a list that hold the target states that are followed by the interpolated player and when a state is reached the target state is updated to the next one.
There is also a variable that i call interpolation factor. I foud this variable perchance trying more and more to reach a more suitable visual result. In practice, for what i've seen, this is a rate that helps to smooth he cube in different forms:
In order to compensate the packat drop this value must be relatively low, (on my machine 30) and it results in a smooth move also when many packet are lost but the distance beetween the local player and interpolated player is greater.
In order to reduce this distance this factor can be setted to a value like 60 but if many packates are lost, it will swap (less than the local player) to the next position.

There are some inputs that help seeing the differences beetween some values:
P -> enable/disable server state rendering
O -> enable/disable local state rendering
I -> enable/disable interpolated state rendering
L -> increase base latency
K -> decrease base latency
U -> increase interpolation factor
Y -> decrease interpolation factor

i'm sure you have some of you can help me to improve this simple system :)
Thanks

#2

Looks cool. However you seems to claim 5/10 is 30 % in the comment after the dropRate variable :stuck_out_tongue:


#3

HAhaha yes sorry, i’ve made many chages to the code varying those value all XD

Did you try it ?