BetterCharacterControl is "flickering" (no fluently movements)

Hello monkeys,
I have a problem with the BetterCharacterControl. First of all, currently I am developing a small Multiplayer FPS-Game (in FirstPerson). The camera is in one of the eyes of the player. After I attached a weapon into the players Hand, I noticed, that the weapon is a little bit “flickering” (not sure, if this the right term for that). After I set the Position of the camera a little bit behind the player, I noticed that the whole Player is “flickering” and so the weapon has to be as well. So, in my opinion I made something wrong with the BetterCharacterControl. Is this a known issue, because I had this Problem in another project as well.

Anyway, I posted a short video, where you can see the effect quite well.
Here the link:
https://youtu.be/ZU2si0YwKGE

I hope somebody is familiar with this problem and knows a solution (if one does exist).

Greetings, Domenic
:grin:

What location to you use to set the camera location?

Usually I use

 cam.setLocation(eyeR.getWorldTranslation());

eyeR is a node from a bone which belongs to the skelleton of the mesh.

Why do you think, that the cam is responsible for that issue? I mean, if you have a look at the terrain or the water, you see the “movements” are fluently.

Yep. 1000000% of the solutions require seeing code, though.

Else we play 20 questions. When do you update the camera? How do you update the position? etc. etc… you do something wrong there… not always obvious to you what… but something is done at the wrong time. It would be unfair to make us guess.

The camera is updated from one position and the avatar/gun/whatever is updated from another.

These positions are different because you grab them at the wrong/different times. 5 minutes of debug logging would have probably indicated this, by the way.

Yeah, sorry for that!

void init() {
eyeR = (Node) playerNode.getChild("eye.R");    
....
}

@Override
public void update(float tpf) {
    cam.setLocation(eyeR.getWorldTranslation());
    characterControl.setViewDirection(cam.getDirection());
 ... 
}

The following is from the PlayerNavControl:

@Override
protected void controlUpdate(float tpf) {
    getWalkDirection().set(Vector3f.ZERO);
    if (walking) {
        if (up) {
            getWalkDirection().addLocal(cam.getDirection().multLocal(10f));
        }
        if (down) {
            getWalkDirection().addLocal(cam.getDirection().multLocal(3f).negate());
        }
        if (left) {
            getWalkDirection().addLocal(cam.getLeft().multLocal(3f));
        }
        if (right) {
            getWalkDirection().addLocal(cam.getLeft().multLocal(3f).negate());
        }
        getWalkDirection().setY(0); // dass der Spieler nicht in die "Luft" gehen kann
        spatial.getControl(BetterCharacterControl.class).setWalkDirection(getWalkDirection());
//        return;
    } else if (ladder) {
        // empty
    }
}

That’s pretty much it. That’s all I found, where the camera position is set.
The weird part is, that sometimes it works quite well and I don’t have any problems.
Here the link to a second video:

(By the way, the Videos aren’t listed, you only can wath them via the link)

Please ignore the capsule collision shape in the end, I know it’s to small. I corrigated it already. That’s not the Problem.

Is that done in an app state? Or?

Here is a way you can debug this issue.

In your application’s simpleUpdate() method put:
System.out.println(“------------------------FRAME-----------------------------”);

Then, where you are setting the camera location, print the world translation of the eye.

Then in your application’s simpleRender(), print the eye location again.

You will see that they are different, most likely.

Also of concern to you will be whether your player nav control is before or after your better character control in the spatial’s control list. In general, response to player input is best done in an app state so you don’t have to worry about that. App state ordering will always be consistent with respect to control updates.

So, actually they have the same location. :smiley: … and yes I inserted the output command at the right position!

-----------------FRAME-------------------
(123.98754, 6.3842144, -50.92374)
(123.98754, 6.3842144, -50.92374)
-----------------FRAME-------------------
(123.98754, 6.3842144, -50.92374)
(123.98754, 6.3842144, -50.92374)
-----------------FRAME-------------------
(124.11396, 6.3846016, -50.812214)
(124.11396, 6.3846016, -50.812214)
-----------------FRAME-------------------
(124.11396, 6.3846016, -50.812214)
(124.11396, 6.3846016, -50.812214)
-----------------FRAME-------------------
(124.10198, 6.386235, -50.81113)
(124.10198, 6.386235, -50.81113)
-----------------FRAME-------------------
(124.10198, 6.386235, -50.81113)
(124.10198, 6.386235, -50.81113)

The “update-Code” is done in an AppState, that’s right!

Even though you said… I have to ask. The second one is definitely in simple render?

If so then you have no problem. Everything is exactly where it should be and there is no jitter. Because you are setting the camera to where it will actually be rendered. Everything is fine and the jitter is imaginary. (I don’t really believe that… just saying.)

…unless the thing that is jittering is somehow being updated in another way.

Edit: see if you can put together a simple test case that illustrates the issue so that we don’t get soda-straw views of the code. This can work… something is updating something at the wrong time.

Actually, can you show me the actual println code in context?

Sorry, but what do you mean exactly?

EDIT:
Ok, now I got you :smiley:
This is called from the “Main”

@Override
public void simpleUpdate(float tpf) {
    System.out.println("-----------------FRAME-------------------");
}

@Override
public void simpleRender(RenderManager rm) {
    PlayerState playerState = stateManager.getState(PlayerState.class);
    if (playerState != null && playerState.isInitialized()) {
        System.out.println(playerState.getEyeRNode().getWorldTranslation());
    }
}  

This is in the PlayerState class:

@Override
public void update(float tpf) {
    cam.setLocation(eyeR.getWorldTranslation());
    System.out.println(eyeR.getWorldTranslation());
...
}

Do you want something like a “demo” project which contains, let’s say only the guy, one Terrain and the relevant code for that. Is this, what you want?

The position of the model is set in the physics control, depending on when you set the location of the camera from the eye that might happen afterwards (that is after you set the cam position). Try setting the cam position off of the physics position (which is later used to set the model position) or make sure the setting of the cam position happens after the physics position is applied (e.g. by making a Control on the model that is added after the physics control).

Setting the camera location in an AppState’s render() method is sufficient for making sure that all controls have been run.

…but it doesn’t look like it will fix the issue in this case which makes me wonder if the object seen jittering is something else. Like maybe the thing setting that object’s position is also done at the wrong time or something.

Preferably one class (perhaps with an inner class or two) that illustrates the problem without a lot of other cruft.

Okay, I had for 2 days no Internet.
Anyway, here is the Main class:

package mygame;

import com.jme3.app.SimpleApplication;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.control.BetterCharacterControl;
import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;

public class Main extends SimpleApplication {

private BulletAppState bulletAppState;
private Node eyeR;
private BetterCharacterControl characterControl;
private Spatial playerModel;

public static void main(String[] args) {
    Main app = new Main();
    app.start();
}

@Override
public void simpleInitApp() {
    bulletAppState = new BulletAppState();
    stateManager.attach(bulletAppState);


    initTerrain();
    initLight();
    initPlayer();
    initInput();

}

private void initTerrain() {
    Spatial terrain = assetManager.loadModel("Scenes/TestScene.j3o");
    terrain.setLocalTranslation(0, 0, 0);
    bulletAppState.getPhysicsSpace().addAll(terrain);
    rootNode.attachChild(terrain);
}

private void initLight() {
    AmbientLight ambient = new AmbientLight();
    ambient.setColor(ColorRGBA.White);
    rootNode.addLight(ambient);

    DirectionalLight sun = new DirectionalLight();
    sun.setDirection((new Vector3f(-0.5f, -0.5f, -0.5f)).normalizeLocal());
    sun.setColor(ColorRGBA.White);
    rootNode.addLight(sun);
}

private void initPlayer() {
    characterControl = new BetterCharacterControl(1.3f, 7.5f, 80);
    playerModel = assetManager.loadModel("Models/Characters/DefaultCharacterNew.j3o");
    playerModel.setLocalTranslation(0, 0, 0);
    playerModel.scale(4f);
    playerModel.addControl(characterControl);

    Node playerNode = (Node) playerModel;

    characterControl.setGravity(new Vector3f(0, 10, 0));
    characterControl.setJumpForce(new Vector3f(0, 400, 0));
    characterControl.warp(new Vector3f(0, 3, 0));

    // add to physicSpace and to rootNode
    bulletAppState.getPhysicsSpace().addAll(playerModel);
    rootNode.attachChild(playerModel);

    // add controls     
    playerModel.addControl(new PlayerNavControl(cam));
    playerModel.addControl(new PlayerAnimControl());
    
    eyeR = (Node) playerNode.getChild("eye.R");
}

private void initInput() {
    inputManager.addMapping("Up", new KeyTrigger(KeyInput.KEY_W));
    inputManager.addMapping("Down", new KeyTrigger(KeyInput.KEY_S));
    inputManager.addMapping("Left", new KeyTrigger(KeyInput.KEY_A));
    inputManager.addMapping("Right", new KeyTrigger(KeyInput.KEY_D));
    inputManager.addMapping("Jump", new KeyTrigger(KeyInput.KEY_SPACE));
    inputManager.addMapping("Esc", new KeyTrigger(KeyInput.KEY_ESCAPE));
    inputManager.addMapping("Space", new KeyTrigger(KeyInput.KEY_SPACE));
    inputManager.addMapping("ButtonRight", new MouseButtonTrigger(MouseInput.BUTTON_RIGHT));
    inputManager.addMapping("ButtonLeft", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));

    inputManager.addListener(new ActionListener() {
        public void onAction(String name, boolean isPressed, float tpf) {
            if (name.equals("Up")) {
                playerModel.getControl(PlayerNavControl.class).up = isPressed;
            }
            if (name.equals("Down")) {
                playerModel.getControl(PlayerNavControl.class).down = isPressed;
            }
            if (name.equals("Left")) {
                playerModel.getControl(PlayerNavControl.class).left = isPressed;
            }
            if (name.equals("Right")) {
                playerModel.getControl(PlayerNavControl.class).right = isPressed;
            }
        }
    }, new String[]{"Up", "Down", "Left", "Right"});
}

@Override
public void simpleUpdate(float tpf) {
    cam.setLocation(eyeR.getWorldTranslation());  
    characterControl.setViewDirection(cam.getDirection());
}

@Override
public void simpleRender(RenderManager rm) {
}
}

Player Anim:

package mygame;

import com.jme3.animation.AnimChannel;
import com.jme3.animation.AnimControl;
import com.jme3.animation.AnimEventListener;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.AbstractControl;

/**
*

  • @author Domenic

  • Handling animations for the player’s spatial
    */
    public class PlayerAnimControl extends AbstractControl implements AnimEventListener {

    private AnimControl animControl;
    private AnimChannel armsChannel;

    public PlayerAnimControl() {
    }

    @Override
    public void setSpatial(Spatial spatial) {
    super.setSpatial(spatial);
    if (spatial != null) {
    animControl = spatial.getControl(AnimControl.class);
    animControl.addListener(this);

         armsChannel = animControl.createChannel();
         armsChannel.setAnim("HoldM16Arms");
     }
    

    }

    @Override
    protected void controlUpdate(float tpf) {
    }

    @Override
    protected void controlRender(RenderManager rm, ViewPort vp) {
    }

    @Override
    public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) {

    }

    @Override
    public void onAnimChange(AnimControl control, AnimChannel channel, String animName) {
    // empty, not used
    }
    }

    package mygame;

import com.jme3.bullet.control.BetterCharacterControl;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.control.AbstractControl;

/**
*

  • @author Domenic

  • Rules the walking of the player
    */
    public class PlayerNavControl extends AbstractControl {

    public boolean up, down, left, right;
    public boolean walking = true, ladder = false;
    private Vector3f walkDirection;
    private Camera cam;

    public PlayerNavControl(Camera cam) {
    this.cam = cam;
    walkDirection = new Vector3f(0, 0, 0);
    }

    @Override
    protected void controlUpdate(float tpf) {
    getWalkDirection().set(Vector3f.ZERO);
    if (walking) {
    if (up) {
    getWalkDirection().addLocal(cam.getDirection().multLocal(10f));
    }
    if (down) {
    getWalkDirection().addLocal(cam.getDirection().multLocal(3f).negate());
    }
    if (left) {
    getWalkDirection().addLocal(cam.getLeft().multLocal(3f));
    }
    if (right) {
    getWalkDirection().addLocal(cam.getLeft().multLocal(3f).negate());
    }
    getWalkDirection().setY(0); // dass der Spieler nicht in die “Luft” gehen kann
    spatial.getControl(BetterCharacterControl.class).setWalkDirection(getWalkDirection());
    // return;
    } else if (ladder) {
    // empty
    }
    }

    @Override
    protected void controlRender(RenderManager rm, ViewPort vp) {
    }

    /**

    • @return the walkDirection
      */
      public Vector3f getWalkDirection() {
      return walkDirection;
      }
      }

use tripple backticks for code blocks. that are just two additional lines and it would look way much nicer

and

Is exactly what you dont want to do.
Move that .setLocation to either simpleRender() or better in an app state (then you have a seperate file for your camera and can enable/disable it anytime).

Imagine it like this:

for (Control c : controls)
 c.update(tpf);

for (Control c: controls)
 c.render(tpf);

Now you never know how the updates are ordered. What happens is that you set the camera before you move the player which leads to that jittering.

When you’re at render, it definintely happened already.
(Though I don’t know why moving the camera in the render doesn’t mess up things :P)

Hey, it worked actually !
I inserted the cam.setLocation(eyeR.getWorldTranslation()); in the render method of my PlayerState class (this is an app state).

Thank you all for your great help !!! :grinning:

1 Like

Note: don’t do this… counterintuitively, simpleRender() is called after render. So it’s too late for setting the camera location to affect anything.

1 Like