Making character motion appear fluid with sidescroller camera

I’m creating a sidescroller, so this means the user can navigate their character by pressing the left or right key. I update the location of my camera in the simpleUpdate function as:

private Vector3f camLocationUpdate = new Vector3f();

[java]@Override
public void simpleUpdate(float tpf) {
// Get the player’s coordinates and use them to position the camera
camLocationUpdate.setX(player.getLocalTranslation().getX());
float newYCamPos = previousYCamPos + (player.getLocalTranslation().getY() - previousYCamPos) * Math.min(0.7f, Math.abs(player.getLocalTranslation().getY() - previousYCamPos));
camLocationUpdate.setY(newYCamPos + 1.2f);
camLocationUpdate.setZ(player.getLocalTranslation().getZ() + 7f);
previousYCamPos = newYCamPos;
cam.setLocation(camLocationUpdate);[/java]

For the y component some easing is added for when the character jumps. Though somehow when the character moves left or right there is some jitter as if the character is lagging. Which is not the case. As you can see in the code above the camera is assigned the same x component as the character.

    [java]camLocationUpdate.setX(player.getLocalTranslation().getX());[/java]

The game runs at about 50 fps and if I don’t let the camera move with the character, the motion appears fluid. My question is now does anybody have an idea how to stop the jitter ?

Any logic which does not refer to tpf is suspicious from the start.

simpleUpdate() is called before anything else. If your player is moved by a control or app state then that will happen after this.

In general, putting this kind of logic in simpleUpdate is a bad idea.

…and especially since you are following a specific node, a Control would be a better way to do this and then you could make sure it runs after anything else updating the player position.

The player has a control which is a class which extends BetterCharacterControl. What do you recommend I do with that ? Maybe some more concrete info would help.

Ok it seems fixed. I took your advice @pspeed and made sure it runs after anything else.

Here is the order that things are called every frame:
simpleUpdate()
all appState.update() methods
all Spatials’ controls’ update methods
all appState.render() methods
all visible Geometry Controls’ render methods

Your best bet would be to add a control to the player after the BetterCharacterControl and update the camera in your control’s update method. Otherwise you will have jitter.

Edit: ninja’ed by OP… the best kind of ninja’ed to be.

@Ojtwist said: Ok it seems fixed. I took your advice @pspeed and made sure it runs after anything else.

I wrote a control for the camera to follow a player when it is moving across X axis a time ago. Take a look and let know if it fix your requiriments. It is old code and I didn’t tests it properly, so it may have issues

[java]
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.control.CameraControl;

public class SideCameraControl extends CameraControl {

private float offsetXAxis = 0;
private float stepXAxis = 0.001f;
private float maxOffsetXAxis = 0.1f; //0.08f;
private Vector3f previousSpatialTranslation = null;
private float previousDisplXAxis = 0;

public SideCameraControl(Camera cam) {
    super(cam, ControlDirection.CameraToSpatial);
}

public SideCameraControl(Camera camera, ControlDirection controlDir) {
    super(camera, controlDir);
}

@Override
protected void controlUpdate(float tpf) {
    if (spatial != null && getCamera() != null) {
        switch (getControlDir()) {
            case SpatialToCamera:
                final Vector3f spatialTranslation = spatial.getWorldTranslation();
                if (previousSpatialTranslation != null) {
                    final Vector3f camLocation = new Vector3f(getCamera().getLocation());
                    final float displXAxis = spatialTranslation.x - previousSpatialTranslation.x;

                    if (displXAxis != 0) {
                        if ((previousDisplXAxis > 0 && displXAxis < 0)
                                || (previousDisplXAxis < 0 && displXAxis > 0)) {
                            offsetXAxis = 0;
                        }
                        if (displXAxis > 0) {
                            offsetXAxis += stepXAxis;
                        } else {
                            offsetXAxis -= stepXAxis;
                        }
                        if (Math.abs(offsetXAxis) > maxOffsetXAxis) {
                            if (offsetXAxis > 0) {
                                offsetXAxis = maxOffsetXAxis;
                            } else {
                                offsetXAxis = -maxOffsetXAxis;
                            }
                            camLocation.x += displXAxis;
                            System.out.println("offset " + offsetXAxis + "\t\tdispl " + displXAxis + "\t\tcamLoc " + getCamera().getLocation().x + "\t\tnewLoc " + camLocation.x);
                            getCamera().setLocation(camLocation);
                        } else {
                            camLocation.x += displXAxis + offsetXAxis;
                            System.out.println("offset " + offsetXAxis + "\t\tdispl " + displXAxis + "\t\tcamLoc " + getCamera().getLocation().x + "\t\tnewLoc " + camLocation.x);
                            getCamera().setLocation(camLocation);

                        }
                        previousDisplXAxis = displXAxis;
                    }


                } else {
                    getCamera().setLocation(spatialTranslation);
                }

                previousSpatialTranslation = new Vector3f(spatialTranslation);
                break;

            case CameraToSpatial:
                super.controlUpdate(tpf);
                break;
        }
    }
}

public float getMaxOffsetXAxis() {
    return maxOffsetXAxis;
}

public void setMaxOffsetXAxis(float maxOffsetXAxis) {
    this.maxOffsetXAxis = maxOffsetXAxis;
}

public float getOffsetXAxis() {
    return offsetXAxis;
}

public void setOffsetXAxis(float offsetXAxis) {
    this.offsetXAxis = offsetXAxis;
}

public float getStepXAxis() {
    return stepXAxis;
}

public void setStepXAxis(float stepXAxis) {
    this.stepXAxis = stepXAxis;
}

}
[/java]

I tested my SideCameraControl (very odd name by the way) and it was a old version. This is the final version:

[java]
import java.io.IOException;

import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.CameraControl;
import com.jme3.scene.control.Control;

/**
*

  • @author H
    */
    public class SideCameraControl extends CameraControl {

    private static final String FIELD_NAME_STEP_X = “stepX”;
    private static final String FIELD_NAME_MAX_DISTANCE_X = “maxDistanceX”;

    private static final float DEFAULT_STEP_X = .01f;
    private static final float DEFAULT_MAX_DISTANCE_X = 3;

    private float stepX = SideCameraControl.DEFAULT_STEP_X;
    private float maxDistanceX = SideCameraControl.DEFAULT_MAX_DISTANCE_X;
    private Vector3f prevSpatialLoc = null;
    private float prevDisplX = 0;

    public SideCameraControl(final Camera cam) {
    super(cam, ControlDirection.SpatialToCamera);
    }

    @Override
    protected void controlUpdate(final float tpf) {

     if (this.spatial != null && getCamera() != null) {
    
         switch (getControlDir()) {
         case SpatialToCamera:
             final Vector3f spatialLoc = this.spatial.getWorldTranslation();
    
             final Vector3f camLoc = new Vector3f(getCamera().getLocation());
             if (this.prevSpatialLoc != null) {
                 final float displXAxis = spatialLoc.x - this.prevSpatialLoc.x;
    
                 if (displXAxis != 0) {
                     final float distance = spatialLoc.x - camLoc.x;
                     float offsetXAxis = 0;
    
                     if (this.prevDisplX > 0 && displXAxis < 0 || this.prevDisplX < 0 && displXAxis > 0) {
                         offsetXAxis = this.stepX * (displXAxis > 0 ? 2 : -2);
                     } else {
                         if (Math.abs(distance) < this.maxDistanceX) {
                             offsetXAxis = this.stepX * (displXAxis > 0 ? 1 : -1);
                         }
                     }
    
                     camLoc.x += displXAxis + offsetXAxis;
                     getCamera().setLocation(camLoc);
    
                     this.prevDisplX = displXAxis;
                 }
             } else {
                 camLoc.x = spatialLoc.x;
                 getCamera().setLocation(camLoc);
             }
    
             this.prevSpatialLoc = new Vector3f(spatialLoc);
             break;
    
         case CameraToSpatial:
             super.controlUpdate(tpf);
             break;
         }
     }
    

    }

    @Override
    public Control cloneForSpatial(final Spatial newSpatial) {
    final SideCameraControl control = (SideCameraControl) super.cloneForSpatial(newSpatial);
    control.setMaxDistanceX(this.maxDistanceX);
    control.setStepX(this.stepX);

     return control;
    

    }

    @Override
    public void read(final JmeImporter im) throws IOException {
    super.read(im);
    final InputCapsule ic = im.getCapsule(this);
    this.maxDistanceX = ic.readFloat(SideCameraControl.FIELD_NAME_MAX_DISTANCE_X,
    SideCameraControl.DEFAULT_MAX_DISTANCE_X);
    this.stepX = ic.readFloat(SideCameraControl.FIELD_NAME_STEP_X, SideCameraControl.DEFAULT_STEP_X);
    }

    @Override
    public void write(final JmeExporter ex) throws IOException {
    super.write(ex);
    final OutputCapsule oc = ex.getCapsule(this);
    oc.write(this.maxDistanceX, SideCameraControl.FIELD_NAME_MAX_DISTANCE_X,
    SideCameraControl.DEFAULT_MAX_DISTANCE_X);
    oc.write(this.stepX, SideCameraControl.FIELD_NAME_STEP_X, SideCameraControl.DEFAULT_STEP_X);
    }

    public float getStepX() {
    return this.stepX;
    }

    public void setStepX(final float stepX) {
    this.stepX = stepX;
    }

    public float getMaxDistanceX() {
    return this.maxDistanceX;
    }

    public void setMaxDistanceX(final float maxDistanceX) {
    this.maxDistanceX = maxDistanceX;
    }
    }
    [/java]

Set stepXAxis and maxDistance so fix your needs. You just need to add this control to your player. You can change the code to adjust camera to movements across Y axis if you want.