BetterCharacterControl Problems with High Frame Rate

Hello, I’ve been working on a scenario to test out the engine and I’ve run into a problem. If I let the frame rate run above ~200 fps the BetterCharacterControl objects seem to stop responding to method calls located in the input listener onAnalog(). I’m wondering if the frame rate is somehow outpacing the event report rate of the listener or if there is some acceleration value to BetterCharacterControl that is being reset with each call to setWalkDirection(). Any insights into what’s going on would be greatly appreciated. Here’s the relevant code:


private TestGame game;

public static void main(String[] args) {
    Main app = new Main();
    AppSettings cfg = new AppSettings(true);
    cfg.setFrameRate(300);
    cfg.setVSync(false);
    cfg.setFrequency(48);
    cfg.setResolution(960, 540);
    cfg.setFullscreen(false);
    cfg.setSamples(0);
    cfg.setTitle("Test Game");
    cfg.setUseJoysticks(true);
    app.setShowSettings(false);
    app.setSettings(cfg);
    app.start();
}

@Override
public void simpleInitApp() {
    game = new TestGame();
}

@Override
public void simpleUpdate(float tpf) {
    game.eventLoop();
}

@Override
public void simpleRender(RenderManager rm) {
    game.render();
}

private class TestGame implements ActionListener, AnalogListener {

    private final long epoch;
    private long AnalogInputTime;
    private final Vector3f player1StickDirection, player2StickDirection, isoVector;
    private final BulletAppState bulletAppState;
    private final Spatial testRoomModel, player1Model, player2Model;
    private final BetterCharacterControl player1Phys, player2Phys;

    private TestGame() {
        //init fields
        epoch = currentTimeMillis();
        AnalogInputTime = epoch;
        player1StickDirection = new Vector3f();
        player2StickDirection = new Vector3f();
        isoVector = new Vector3f();
        bulletAppState = new BulletAppState();
        //load models
        testRoomModel = assetManager.loadModel(
                "Models/test_room/test_room.j3o");
        player1Model = assetManager.loadModel(
                "Models/test_character/test_character.j3o");
        player2Model = player1Model.clone();
        //load materials
        Material normalMat = new Material(
                assetManager, "Common/MatDefs/Misc/ShowNormals.j3md");
        //assign materials
        testRoomModel.setMaterial(normalMat);
        player1Model.setMaterial(normalMat);
        player2Model.setMaterial(normalMat);
        //register physics with stateManager
        stateManager.attach(bulletAppState);
        //generate collision geometry
        CollisionShape testRoomCollision
                = CollisionShapeFactory.createMeshShape((Node) testRoomModel);
        //create physics control geometry
        RigidBodyControl testRoomPhys = new RigidBodyControl(
                testRoomCollision, 0);
        player1Phys = new BetterCharacterControl(2f, 6f, 1f);
        player2Phys = new BetterCharacterControl(2f, 6f, 1f);
        //configure physics
        player1Phys.setJumpForce(new Vector3f(0, 12f, 0));
        player1Phys.setGravity(new Vector3f(0, 1f, 0));
        player2Phys.setJumpForce(new Vector3f(0, 12f, 0));
        player2Phys.setGravity(new Vector3f(0, 1f, 0));
        //create and configure scene lighting
        DirectionalLight sun = new DirectionalLight();
        sun.setDirection(new Vector3f(-0.1f, -0.7f, -1.0f));
        //configure camera
        flyCam.setEnabled(false);
        cam.setLocation(new Vector3f(25, 25, 25));
        cam.lookAt(Vector3f.ZERO, Vector3f.ZERO);
        //configure joystick input
        Joystick[] joysticks = inputManager.getJoysticks();
        if (joysticks != null) {
            joysticks[0].getXAxis().assignAxis("right", "left");
            joysticks[0].getYAxis().assignAxis("forward", "backward");
            joysticks[0].getButton("2").assignButton("jump");
            joysticks[0].getButton("0").assignButton("reset");
            if (joysticks.length > 1) {
                joysticks[1].getXAxis().assignAxis("right2", "left2");
                joysticks[1].getYAxis().assignAxis("forward2",
                        "backward2");
                joysticks[1].getButton("2").assignButton("jump2");
                joysticks[1].getButton("0").assignButton("reset2");
                inputManager.addListener(this, "left", "right", "forward",
                        "backward", "left2", "right2", "forward2",
                        "backward2",
                        "jump", "jump2", "reset", "reset2");
            }
        } else {
            out.println("No joysticks detected!");
        }
        //register scene objects
        rootNode.attachChild(testRoomModel);
        rootNode.attachChild(player1Model);
        rootNode.attachChild(player2Model);
        rootNode.addLight(sun);
        //register physics
        bulletAppState.getPhysicsSpace().add(testRoomPhys);
        bulletAppState.getPhysicsSpace().add(player1Phys);
        bulletAppState.getPhysicsSpace().add(player2Phys);
        //bind physics to geometry
        testRoomModel.addControl(testRoomPhys);
        player1Model.addControl(player1Phys);
        player2Model.addControl(player2Phys);
        //position players
        player1Phys.warp(new Vector3f(-5, 16, 5));
        player2Phys.warp(new Vector3f(5, 16, -5));
    }

    void eventLoop() {
        //ensure player stops if analog input stops
        if (currentTimeMillis() - AnalogInputTime > .2) {
            player1Phys.setWalkDirection(Vector3f.ZERO);
            player2Phys.setWalkDirection(Vector3f.ZERO);
        }
    }

    void render() {
    }

    @Override
    public void onAction(String name, boolean isPressed, float tpf) {
        if (isPressed) {
            switch (name) {
                case "jump":
                    player1Phys.jump();
                    break;
                case "jump2":
                    player2Phys.jump();
                    break;
                case "reset":
                    player1Phys.warp(new Vector3f(0, 15, 0));
                    break;
                case "reset2":
                    player2Phys.warp(new Vector3f(0, 15, 0));
                    break;
            }
        }
    }

    @Override
    public void onAnalog(String name, float value, float tpf) {
        AnalogInputTime = currentTimeMillis();
        //player (1, 2) abstraction
        Vector3f stickDirection;
        Spatial playerModel;
        BetterCharacterControl playerPhys;
        if (name.contains("2")) {
            stickDirection = player2StickDirection;
            playerPhys = player2Phys;
            playerModel = player2Model;
        } else {
            stickDirection = player1StickDirection;
            playerPhys = player1Phys;
            playerModel = player1Model;
        }
        switch (name) {
            case "left":
            case "left2":
                stickDirection.x = -value;
                break;
            case "right":
            case "right2":
                stickDirection.x = value;
                break;
            case "forward":
            case "forward2":
                stickDirection.z = value;
                break;
            case "backward":
            case "backward2":
                stickDirection.z = -value;
                break;
        }
        //'dead-zone' input threshold
        float deadZone = .005f;
        float magnitude = stickDirection.length();
        if (magnitude < deadZone) {
            return;
        }
        stickDirection = stickDirection.normalize().mult(
                (magnitude - deadZone) / (1
                - deadZone));
        //cartesian to isometric coordinate conversion
        CoordPair isoPair = CoordPair.cartToIso(stickDirection.x,
                stickDirection.z);
        isoVector.set(isoPair.x, 0, isoPair.y);
        //prevent passing -0 to physics methods
        if (isoVector.x == 0 || isoVector.x == -0 && isoVector.z == 0
                || isoVector.z == -0) {
            playerPhys.setWalkDirection(Vector3f.ZERO);
        } else {
            playerPhys.setWalkDirection(isoVector.normalize().mult(5));
            playerPhys.setViewDirection(isoVector);
            playerModel.lookAt(isoVector, Vector3f.UNIT_Y);
        }
    }
}}

Note that if you want to make the code runnable you need these imports:

import com.jme3.app.SimpleApplication;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.control.BetterCharacterControl;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.bullet.util.CollisionShapeFactory;
import com.jme3.input.Joystick;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.AnalogListener;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.system.AppSettings;
import static java.lang.System.*;

And this static nested class:

private static class CoordPair {
    //immutable object

    final float x, y;

    CoordPair(float x, float y) {
        this.x = x;
        this.y = y;
    }

    static CoordPair cartToIso(float x, float y) {
        return new CoordPair((2 * y + x) / 2, (2 * y - x) / 2);
    }

    static CoordPair isoToCart(float x, float y) {
        return new CoordPair(x - y, (x + y) / 2);
    }

    CoordPair getCartToIso() {
        return cartToIso(x, y);
    }

    CoordPair getIsoToCart() {
        return isoToCart(x, y);
    }
}

You’ll also need stand-in models for the room and player, and at least one controller (mappings are written for xbox 360).