Choppy movement (again)

Hi all

Been a long while since I worked on my space shooter game, RealLife and all that. So I come back to it yesterday and start looking into what needs to be worked on next - turns out I never got around to fixing the very jittery movement in the game. Basically what I'm doing is pumping out entity data from the server every tenth of a second, entity data being position, speed, angle etc. On the client side I've implemented a Controller that interpolates the movement for each client frame using the position and speed gained from the last update from the server.



First I thought the choppy movement was caused by the "transition" between each update from the server, but when lowering the "server frame rate" to one per 5 seconds, it was easy to see that the client movement was just as choppy through the entire movement. Doesn't seem to be the interpolator's fault either, as adding some debug printouts showed that the interpolator's update() method was run at pretty constant 50 FPS, with a fairly even delay between each call. I also tried completely overriding the network part on the client side, only using static positions and speeds with the same effect.



I've made a mistake earlier in what order you're supposed to update stuff in the BaseGame.update() method, I'm guessing this is something similar. Here's my update method, with some parts (that wouldn't make much sense without the rest of the code anyways) commented out.



Any ideas on this are most welcome…



   protected synchronized void update(float interpolation) {
      timer.update();
      long time = timer.getTime();
      double timeSinceLastFrame = (double) (time - timeLastFrame) / timer.getResolution();
      timeLastFrame = time;
      input.update((float) timeSinceLastFrame);
      
      if (serverFrame > oldFrame) {
// An update from the server has been received since last time update() was called, do stuff like refreshing all entities' positions, speeds etc
      }
      
      rootNode.updateGeometricState((float) timeSinceLastFrame, true);
      Entity ent = entities.get(shipId);
      if (ent != null && ent.getModel() != null) {
         Vector3f targetPos = new Vector3f(ent.getModel().getWorldTranslation());
         float mult = Math.min(100f, ent.getSpeed().length());
         targetPos.x += FastMath.cos(ent.getDir() - FastMath.HALF_PI) * mult;
         targetPos.y += FastMath.sin(ent.getDir() - FastMath.HALF_PI) * mult;
         if (camPos == null) {
            camPos = new Vector3f(targetPos.x, targetPos.y, 250);
         } else {
            camPos.x += (targetPos.x - camPos.x) * 0.05f;
            camPos.y += (targetPos.y - camPos.y) * 0.05f;
            camPos.z = 250;
         }
         
         viewPos.x = camPos.x;
         viewPos.y = camPos.y;
         viewPos.z = 0;
         
         cam.setLocation(camPos);
         
         if (time > lastClientMsg + 75) {
            float dir = oldDir;
            float dx = display.getScreenCoordinates(ent.getModel().getWorldTranslation()).x;
            float dy = display.getScreenCoordinates(ent.getModel().getWorldTranslation()).y;
            Angle a = new Angle(dx, dy, mouse.getHotSpotPosition().x, mouse.getHotSpotPosition().y);
            if (!MouseInput.get().isButtonDown(1)) {
               dir = a.getAngle();
            }
            boolean forward = KeyBindingManager.getKeyBindingManager().isValidCommand("Forward", true);
            boolean backward = KeyBindingManager.getKeyBindingManager().isValidCommand("Backward", true);
            boolean left = KeyBindingManager.getKeyBindingManager().isValidCommand("Left", true);
            boolean right = KeyBindingManager.getKeyBindingManager().isValidCommand("Right", true);
            boolean fire = MouseInput.get().isButtonDown(0);
            connection.addMessage(new ClientMessage(forward, backward, left, right, fire, dir, a.getAngle()));
            lastClientMsg = time;
            oldDir = dir;
         }
      }
      try {
         Thread.sleep(10);
      } catch (InterruptedException e) {
         System.out.println("wtf");
      }
   }



And here's my interpolator...


//imports here...

public class TimedSpatialController extends Controller {
   private Node node;
   private Vector3f startPos;
   private Vector3f endPos;
   private float startDir;
   private float endDir;
   
   private float totTime;
   private float curTime = 0;
   private float addX;
   private float addY;
   private float addDir;
   private Quaternion quat = new Quaternion();
   private static Vector3f rotVector = new Vector3f(0,0,1);
   
   public TimedSpatialController(Node node) {
      this.node = node;
      node.addController(this);
      setActive(false);
   }
   public TimedSpatialController(Node node, Vector3f start, Vector3f end, float startDir, float endDir, float time) {
      this.node = node;
      reinit(start, end, startDir, endDir, time);
      node.addController(this);
   }
   
   public void reinit(Vector3f start, Vector3f end, float startDir, float endDir, float time) {
      this.startPos = start;
      this.endPos = end;
      this.startDir = startDir;
      this.endDir = endDir;
      this.totTime = time;
      curTime = 0;
      
      addX = end.x - start.x;
      addY = end.y - start.y;
      
      float a = startDir;
      float o = endDir;
      float diff = Math.abs(a - o);
      if (diff <= Math.PI) {
         addDir = o - a;
      } else {
         if (a > o) {
            a -= FastMath.TWO_PI;
         } else {
            o -= FastMath.TWO_PI;
         }
         diff = Math.abs(a - o);
         addDir = o - a;
      }
      setActive(true);
   }
   
   public void release() {
      node.removeController(this);
      setActive(false);
   }
   
   public void update(float time) {
      if (!isActive()) {
         return;
      }
      
      curTime += time;
      
      float dTime = curTime / totTime;
      quat.fromAngleAxis(startDir + addDir * dTime, rotVector);
      node.setLocalTranslation(new Vector3f(startPos.x + addX * dTime, startPos.y + addY * dTime, 0));
      node.setLocalRotation(quat);
   }
}


Bump



I've been working on this project a little on and off, doing some other stuff that needs to be done. I just cannot find what's causing the weird, jittery movement. Is there anyone out there that have any ideas what so ever?

Could you explain the jitter?  Is it choppy translation or rotation?

Without looking too closely at the code, I notice some conversions to and from Quaternions.  Choppy movement caused by numerical inaccuracies was what originally triggered my patch to issue 229 (https://jme.dev.java.net/issues/show_bug.cgi?id=229).



That bug has been fixed in current CVS, so you might try just updating to the latest CVS jME and see if your choppiness goes away.  Maybe the fix is easy. :slight_smile:

So, is there some kind of collected pieces of wisdom what all has changed between each cvs?

Right now I can't compile my project since CloneCreator and ParticleManager seem to be lost to us. Also the method Geometry.getModelBound() is strangely absent… Any ideas on this?

CloneCreator has been removed as well JmeBinaryReader and writer in favor of the JmeImporter and JmeExporter interfaces. CloneImportExport is CloneCreator's replacement, while BinaryImporter and BinaryExporter are the replacement for reader and writer. There currently isn't an XML reader/writer replacement.



Geometry getModelBound is the same as getWorldBound.



Particles have just gone a slight reorg with some name changes, look at ParticleMesh and ParticleFactory. The TestParticleSystem should help you convert.

Hmm, for now I've just commented out the particle code and load the models from disk each time requested, just to get the thing running.



The MD3 loader doesn't seem to like the model I'm using for the player ship, it gets quite corrupted:

http://giex.dynu.com/badmd3.png

Zoomed in a bit, the texture is badly applied and there seems to be a piece of the tail fin missing:

http://giex.dynu.com/badmd3-2.png



Any ideas if there's something wrong with the model or my way of loading it?



URL texpath = new File(System.getProperty("user.dir") + "/data/" + path + "/").toURL();
URL modelpath = new File(System.getProperty("user.dir") + "/data/" + p + ".3ds").toURL();

FormatConverter converter = new MaxToJme();
converter.setProperty("texurl", texpath);
converter.setProperty("bound", "box");

ByteArrayOutputStream bo = new ByteArrayOutputStream();
converter.convert(modelpath.openStream(), bo);

return (Node) BinaryImporter.getInstance().load(bo.toByteArray());



And here's the model I'm using:
http://giex.dynu.com/jet.md3
http://giex.dynu.com/jet.jpg

In the old JmeBinaryReader, there was a method to set path to textures (jbr.setProperty("texurl", texpath)), is there something similar in BinaryImporter that I'm missing?