[Complete] ForceCharacterControl

As promised i made a character control which can be influenced by force (call the applyCentralForce method)

PROS:

*apply forces manually when you want, the standard charactercontrol kinematics apply as always.

*character is slowing down due to friction (you can adjust that with setForceDamping() )

*character can stop moving on small forces (to eliminate small unwanted vibrations, use setMinimalForceAmount() )

CONS:

*does not work on BoxCollisionShape floors and maybe some other simple collision shapes (use terrain quads, that is HeightMapCollisionShaps, CompoundCollisionShapes and MeshCollisionShapes for floors ), due to the limitations of the standard CharacterControl

*do not overdo it with vertical velocities, you can cause an infixable state where the vertical velocity becomes greater than the maximum gravity that is applied!!



UPDATE v1.1:

*fixed the infixable state bug, where the character would never land

UPDATE v1.2:

*added correct setters & getters for walk direction and gravity

UPDATE v1.3

*removed negative velocity while on ground, better for floor collision







Down below a testclass and further down the actual ForceCharacterControl.



______________________________________________________________________________________________________

TestPhysics.java

Use W,A,S,D to move around, use U,H,J,K to give a force impulse

[java collapse=“true”]

/**

  • Copyright © 2009-2010 jMonkeyEngine
  • All rights reserved.

    *
  • Redistribution and use in source and binary forms, with or without
  • modification, are permitted provided that the following conditions are
  • met:

    *
    • Redistributions of source code must retain the above copyright
  • notice, this list of conditions and the following disclaimer.

    *
    • Redistributions in binary form must reproduce the above copyright
  • notice, this list of conditions and the following disclaimer in the
  • documentation and/or other materials provided with the distribution.

    *
    • Neither the name of ‘jMonkeyEngine’ nor the names of its contributors
  • may be used to endorse or promote products derived from this software
  • without specific prior written permission.

    *
  • THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  • "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
  • TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  • PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  • CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  • EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  • PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  • PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  • LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  • NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  • SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

    */



    import com.jme3.animation.AnimChannel;

    import com.jme3.animation.AnimControl;

    import com.jme3.animation.AnimEventListener;

    import com.jme3.animation.LoopMode;

    import com.jme3.bullet.BulletAppState;

    import com.jme3.app.SimpleApplication;

    import com.jme3.bounding.BoundingBox;

    import com.jme3.bullet.PhysicsSpace;

    import com.jme3.bullet.collision.PhysicsCollisionEvent;

    import com.jme3.bullet.collision.PhysicsCollisionListener;

    import com.jme3.bullet.collision.shapes.CapsuleCollisionShape;

    import com.jme3.bullet.collision.shapes.SphereCollisionShape;

    import com.jme3.bullet.control.CharacterControl;

    import com.jme3.bullet.control.RigidBodyControl;

    import com.jme3.bullet.util.CollisionShapeFactory;

    import com.jme3.effect.EmitterSphereShape;

    import com.jme3.effect.ParticleEmitter;

    import com.jme3.effect.ParticleMesh.Type;

    import com.jme3.input.ChaseCamera;

    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.DirectionalLight;

    import com.jme3.material.Material;

    import com.jme3.math.ColorRGBA;

    import com.jme3.math.FastMath;

    import com.jme3.math.Quaternion;

    import com.jme3.math.Vector2f;

    import com.jme3.math.Vector3f;

    import com.jme3.post.FilterPostProcessor;

    import com.jme3.post.filters.BloomFilter;

    import com.jme3.renderer.Camera;

    import com.jme3.renderer.queue.RenderQueue.ShadowMode;

    import com.jme3.scene.Geometry;

    import com.jme3.scene.Node;

    import com.jme3.scene.Spatial;

    import com.jme3.scene.shape.Box;

    import com.jme3.scene.shape.Sphere;

    import com.jme3.scene.shape.Sphere.TextureMode;

    import com.jme3.terrain.geomipmap.TerrainLodControl;

    import com.jme3.terrain.geomipmap.TerrainQuad;

    import com.jme3.terrain.heightmap.AbstractHeightMap;

    import com.jme3.terrain.heightmap.ImageBasedHeightMap;

    import com.jme3.texture.Texture;

    import com.jme3.texture.Texture.WrapMode;

    import com.jme3.util.SkyFactory;

    import java.util.ArrayList;

    import java.util.List;

    import jme3test.bullet.BombControl;

    import jme3tools.converters.ImageToAwt;



    /**

    *
  • @author normenhansen
  • @author nego

    */

    public class TestPhysics extends SimpleApplication implements ActionListener, PhysicsCollisionListener, AnimEventListener {

    public static final Quaternion YAW090 = new Quaternion().fromAngleAxis(FastMath.PI/2, new Vector3f(0,1,0));

    public static final Quaternion ROT_LEFT = new Quaternion().fromAngleAxis(FastMath.PI/32, new Vector3f(0,1,0));

    public static final Quaternion ROT_RIGHT = new Quaternion().fromAngleAxis(-FastMath.PI/32, new Vector3f(0,1,0));



    private BulletAppState bulletAppState;

    //character

    ForceCharacterControl character;

    Node model;

    //temp vectors

    Vector3f walkDirection = new Vector3f();

    //terrain

    TerrainQuad terrain;

    RigidBodyControl terrainPhysicsNode;

    //Materials

    Material matRock;

    Material matWire;

    Material matBullet;

    //animation

    AnimChannel animationChannel;

    AnimChannel shootingChannel;

    AnimControl animationControl;

    float airTime = 0;

    //camera

    boolean left = false, right = false, up = false, down = false, strafeEnabled=false;

    //ChaseCamera chaseCam;

    ChaseCamera chaseCam;

    //bullet

    Sphere bullet;

    SphereCollisionShape bulletCollisionShape;

    //explosion

    ParticleEmitter effect;

    //brick wall

    Box brick;

    float bLength = 0.8f;

    float bWidth = 0.4f;

    float bHeight = 0.4f;

    FilterPostProcessor fpp;



    public static void main(String[] args) {

    TestPhysics app = new TestPhysics();

    app.start();

    }



    @Override

    public void simpleInitApp() {

    bulletAppState = new BulletAppState();

    bulletAppState.setThreadingType(BulletAppState.ThreadingType.PARALLEL);

    stateManager.attach(bulletAppState);

    setupKeys();

    prepareBullet();

    prepareEffect();

    createLight();

    createSky();

    createTerrain();

    createWall();

    createCharacter();

    setupChaseCamera();

    setupAnimationController();

    setupFilter();

    }



    private void setupFilter() {

    FilterPostProcessor fpp = new FilterPostProcessor(assetManager);

    BloomFilter bloom = new BloomFilter(BloomFilter.GlowMode.Objects);

    fpp.addFilter(bloom);

    viewPort.addProcessor(fpp);

    }



    private PhysicsSpace getPhysicsSpace() {

    return bulletAppState.getPhysicsSpace();

    }



    private void setupKeys() {

    inputManager.addMapping(“wireframe”, new KeyTrigger(KeyInput.KEY_T));

    inputManager.addListener(this, “wireframe”);

    inputManager.addMapping(“CharLeft”, new KeyTrigger(KeyInput.KEY_A));

    inputManager.addMapping(“CharRight”, new KeyTrigger(KeyInput.KEY_D));

    inputManager.addMapping(“CharUp”, new KeyTrigger(KeyInput.KEY_W));

    inputManager.addMapping(“CharDown”, new KeyTrigger(KeyInput.KEY_S));

    inputManager.addMapping(“CharSpace”, new KeyTrigger(KeyInput.KEY_RETURN));

    inputManager.addMapping(“CharShoot”, new KeyTrigger(KeyInput.KEY_SPACE));

    inputManager.addMapping(“CharStrafe”, new MouseButtonTrigger(MouseInput.BUTTON_RIGHT));

    inputManager.addMapping(“CharPushBack”, new KeyTrigger(KeyInput.KEY_J));

    inputManager.addMapping(“CharPushForward”, new KeyTrigger(KeyInput.KEY_U));

    inputManager.addMapping(“CharPushLeft”, new KeyTrigger(KeyInput.KEY_H));

    inputManager.addMapping(“CharPushRight”, new KeyTrigger(KeyInput.KEY_K));

    inputManager.addListener(this, “CharLeft”);

    inputManager.addListener(this, “CharRight”);

    inputManager.addListener(this, “CharUp”);

    inputManager.addListener(this, “CharDown”);

    inputManager.addListener(this, “CharSpace”);

    inputManager.addListener(this, “CharShoot”);

    inputManager.addListener(this, “CharStrafe”);

    inputManager.addListener(this, “CharPushBack”);

    inputManager.addListener(this, “CharPushForward”);

    inputManager.addListener(this, “CharPushLeft”);

    inputManager.addListener(this, “CharPushRight”);



    }



    private void createWall() {

    float xOff = -144;

    float zOff = -40;

    float startpt = bLength / 4 - xOff;

    float height = 6.1f;

    brick = new Box(Vector3f.ZERO, bLength, bHeight, bWidth);

    brick.scaleTextureCoordinates(new Vector2f(1f, .5f));

    for (int j = 0; j < 15; j++) {

    for (int i = 0; i < 4; i++) {

    Vector3f vt = new Vector3f(i * bLength * 2 + startpt, bHeight + height, zOff);

    addBrick(vt);

    }

    startpt = -startpt;

    height += 1.01f * bHeight;

    }

    }



    private void addBrick(Vector3f ori) {

    Geometry reBoxg = new Geometry(“brick”, brick);

    reBoxg.setMaterial(matRock);

    reBoxg.setLocalTranslation(ori);

    reBoxg.addControl(new RigidBodyControl(1.5f));

    reBoxg.setShadowMode(ShadowMode.CastAndReceive);

    this.rootNode.attachChild(reBoxg);

    this.getPhysicsSpace().add(reBoxg);

    }



    private void prepareBullet() {

    bullet = new Sphere(32, 32, 0.4f, true, false);

    bullet.setTextureMode(TextureMode.Projected);

    bulletCollisionShape = new SphereCollisionShape(0.4f);

    matBullet = new Material(getAssetManager(), “Common/MatDefs/Misc/SolidColor.j3md”);

    matBullet.setColor(“Color”, ColorRGBA.Green);

    matBullet.setColor(“m_GlowColor”, ColorRGBA.Green);

    getPhysicsSpace().addCollisionListener(this);

    }



    private void prepareEffect() {

    int COUNT_FACTOR = 1;

    float COUNT_FACTOR_F = 1f;

    effect = new ParticleEmitter(“Flame”, Type.Triangle, 32 * COUNT_FACTOR);

    effect.setSelectRandomImage(true);

    effect.setStartColor(new ColorRGBA(1f, 0.4f, 0.05f, (float) (1f / COUNT_FACTOR_F)));

    effect.setEndColor(new ColorRGBA(.4f, .22f, .12f, 0f));

    effect.setStartSize(1.3f);

    effect.setEndSize(2f);

    effect.setShape(new EmitterSphereShape(Vector3f.ZERO, 1f));

    effect.setParticlesPerSec(0);

    effect.setGravity(-5f);

    effect.setLowLife(.4f);

    effect.setHighLife(.5f);

    effect.setInitialVelocity(new Vector3f(0, 7, 0));

    effect.setVelocityVariation(1f);

    effect.setImagesX(2);

    effect.setImagesY(2);

    Material mat = new Material(assetManager, “Common/MatDefs/Misc/Particle.j3md”);

    mat.setTexture(“Texture”, assetManager.loadTexture(“Effects/Explosion/flame.png”));

    effect.setMaterial(mat);

    effect.setLocalScale(100);

    rootNode.attachChild(effect);

    }



    private void createLight() {

    Vector3f direction = new Vector3f(-0.1f, -0.7f, -1).normalizeLocal();

    DirectionalLight dl = new DirectionalLight();

    dl.setDirection(direction);

    dl.setColor(new ColorRGBA(1f, 1f, 1f, 1.0f));

    rootNode.addLight(dl);

    }



    private void createSky() {

    rootNode.attachChild(SkyFactory.createSky(assetManager, “Textures/Sky/Bright/BrightSky.dds”, false));

    }



    private void createTerrain() {

    matRock = new Material(assetManager, “Common/MatDefs/Terrain/TerrainLighting.j3md”);

    matRock.setBoolean(“useTriPlanarMapping”, false);

    matRock.setBoolean(“WardIso”, true);

    matRock.setTexture(“AlphaMap”, assetManager.loadTexture(“Textures/Terrain/splat/alphamap.png”));

    Texture heightMapImage = assetManager.loadTexture(“Textures/Terrain/splat/mountains512.png”);

    Texture grass = assetManager.loadTexture(“Textures/Terrain/splat/grass.jpg”);

    grass.setWrap(WrapMode.Repeat);

    matRock.setTexture(“DiffuseMap”, grass);

    matRock.setFloat(“DiffuseMap_0_scale”, 64);

    Texture dirt = assetManager.loadTexture(“Textures/Terrain/splat/dirt.jpg”);

    dirt.setWrap(WrapMode.Repeat);

    matRock.setTexture(“DiffuseMap_1”, dirt);

    matRock.setFloat(“DiffuseMap_1_scale”, 16);

    Texture rock = assetManager.loadTexture(“Textures/Terrain/splat/road.jpg”);

    rock.setWrap(WrapMode.Repeat);

    matRock.setTexture(“DiffuseMap_2”, rock);

    matRock.setFloat(“DiffuseMap_2_scale”, 128);

    Texture normalMap0 = assetManager.loadTexture(“Textures/Terrain/splat/grass_normal.png”);

    normalMap0.setWrap(WrapMode.Repeat);

    Texture normalMap1 = assetManager.loadTexture(“Textures/Terrain/splat/dirt_normal.png”);

    normalMap1.setWrap(WrapMode.Repeat);

    Texture normalMap2 = assetManager.loadTexture(“Textures/Terrain/splat/road_normal.png”);

    normalMap2.setWrap(WrapMode.Repeat);

    matRock.setTexture(“NormalMap”, normalMap0);

    matRock.setTexture(“NormalMap_1”, normalMap2);

    matRock.setTexture(“NormalMap_2”, normalMap2);

    matWire = new Material(assetManager, “Common/MatDefs/Misc/WireColor.j3md”);

    matWire.setColor(“Color”, ColorRGBA.Green);



    AbstractHeightMap heightmap = null;

    try {

    heightmap = new ImageBasedHeightMap(ImageToAwt.convert(heightMapImage.getImage(), false, true, 0), 0.25f);

    heightmap.load();



    } catch (Exception e) {

    e.printStackTrace();

    }



    terrain = new TerrainQuad(“terrain”, 65, 513, heightmap.getHeightMap());

    List<Camera> cameras = new ArrayList<Camera>();

    cameras.add(getCamera());

    TerrainLodControl control = new TerrainLodControl(terrain, cameras);

    terrain.addControl(control);

    terrain.setMaterial(matRock);

    terrain.setModelBound(new BoundingBox());

    terrain.updateModelBound();

    terrain.setLocalScale(new Vector3f(2, 2, 2));



    terrainPhysicsNode = new RigidBodyControl(CollisionShapeFactory.createMeshShape(terrain), 0);

    terrain.addControl(terrainPhysicsNode);

    rootNode.attachChild(terrain);

    getPhysicsSpace().add(terrainPhysicsNode);

    }



    private void createCharacter() {

    CapsuleCollisionShape capsule = new CapsuleCollisionShape(1.5f, 2f);

    character = new ForceCharacterControl(capsule, 0.1f);

    model = (Node) assetManager.loadModel(“Models/Oto/Oto.mesh.xml”);

    model.setLocalScale(0.5f);

    model.addControl(character);

    character.setPhysicsLocation(new Vector3f(-140, 10, -10));

    rootNode.attachChild(model);

    getPhysicsSpace().add(character);

    }



    private void setupChaseCamera() {

    flyCam.setEnabled(false);

    // chaseCam = new PhysicalAutoRotateCamera(cam,model,inputManager,

    // bulletAppState.getPhysicsSpace());

    chaseCam = new ChaseCamera(cam,model,inputManager);







    }



    private void setupAnimationController() {

    animationControl = model.getControl(AnimControl.class);

    animationControl.addListener(this);

    animationChannel = animationControl.createChannel();

    shootingChannel = animationControl.createChannel();

    shootingChannel.addBone(animationControl.getSkeleton().getBone(“uparm.right”));

    shootingChannel.addBone(animationControl.getSkeleton().getBone(“arm.right”));

    shootingChannel.addBone(animationControl.getSkeleton().getBone(“hand.right”));

    }





    @Override

    public void simpleUpdate(float tpf) {

    System.out.println(tpf);

    Vector3f viewDir = character.getViewDirection().normalize();

    Vector3f leftDir = YAW090.mult(viewDir).normalize();

    viewDir.y = 0;

    walkDirection.set(0, 0, 0);

    if (left) {

    if (!strafeEnabled) {

    ROT_LEFT.multLocal(viewDir);

    } else {

    walkDirection.addLocal(leftDir.mult(0.2f));

    }

    }

    if (right) {

    if (!strafeEnabled) {

    ROT_RIGHT.multLocal(viewDir);

    } else {

    walkDirection.addLocal(leftDir.negate().mult(0.2f));

    }

    }

    if (up) {

    walkDirection.addLocal(viewDir.mult(0.2f));

    //character.getControllerId().setVelocityForTimeInterval(new javax.vecmath.Vector3f(0,0.5f,0), 1000f);

    }

    if (down) {

    walkDirection.addLocal(viewDir.negate().mult(0.1f));

    }

    if (!character.onGround()) {

    airTime = airTime + tpf;

    } else {

    airTime = 0;

    }

    if (walkDirection.length() == 0) {

    if (!“stand”.equals(animationChannel.getAnimationName())) {

    animationChannel.setAnim(“stand”, 1f);

    }

    } else {



    if (airTime > .3f) {

    if (!“stand”.equals(animationChannel.getAnimationName())) {

    animationChannel.setAnim(“stand”);

    }

    } else if (!“Walk”.equals(animationChannel.getAnimationName())) {

    animationChannel.setAnim(“Walk”, 0.7f);

    }

    }

    character.setViewDirection(viewDir);

    character.setWalkDirection(walkDirection);

    }



    public void onAction(String binding, boolean value, float tpf) {

    if (binding.equals(“CharLeft”)) {

    if (value) {

    left = true;

    } else {

    left = false;

    }

    } else if (binding.equals(“CharRight”)) {

    if (value) {

    right = true;

    } else {

    right = false;

    }

    } else if (binding.equals(“CharUp”)) {

    if (value) {

    up = true;

    } else {

    up = false;

    }

    } else if (binding.equals(“CharDown”)) {

    if (value) {

    down = true;

    } else {

    down = false;

    }

    } else if (binding.equals(“CharSpace”)) {

    character.jump();

    } else if (binding.equals(“CharShoot”) && !value) {

    bulletControl();

    } else if (binding.equals(“CharStrafe”)) {

    if (value) {

    strafeEnabled = true;

    } else {

    strafeEnabled = false;

    }

    } else if (binding.equals(“CharPushBack”)) {

    Vector3f pushForce = character.getViewDirection().negate().mult(5f);

    pushForce.y = 7.5f;

    character.applyCentralForce(pushForce);

    } else if (binding.equals(“CharPushForward”)) {

    Vector3f pushForce = character.getViewDirection().mult(5f);

    character.applyCentralForce(pushForce);

    } else if (binding.equals(“CharPushLeft”)) {

    Vector3f pushForce = YAW090.multLocal(character.getViewDirection().clone());

    pushForce.multLocal(5f);

    pushForce.y = 7.5f;

    character.applyCentralForce(pushForce);

    } else if (binding.equals(“CharPushRight”)) {

    Vector3f pushForce = YAW090.multLocal(character.getViewDirection().clone()).negate();

    pushForce.multLocal(5f);

    pushForce.y = 7.5f;

    character.applyCentralForce(pushForce);

    }

    }



    private void bulletControl() {

    shootingChannel.setAnim(“Dodge”, 0.1f);

    shootingChannel.setLoopMode(LoopMode.DontLoop);

    Geometry bulletg = new Geometry(“bullet”, bullet);

    bulletg.setMaterial(matBullet);

    bulletg.setShadowMode(ShadowMode.CastAndReceive);

    bulletg.setLocalTranslation(character.getPhysicsLocation().add(cam.getDirection().mult(2)));

    RigidBodyControl bulletControl = new BombControl(bulletCollisionShape, 1);

    bulletControl.setCcdMotionThreshold(0.1f);

    bulletControl.setLinearVelocity(cam.getDirection().mult(80));

    bulletg.addControl(bulletControl);

    rootNode.attachChild(bulletg);

    getPhysicsSpace().add(bulletControl);

    }



    public void collision(PhysicsCollisionEvent event) {

    if (event.getObjectA() instanceof BombControl) {

    final Spatial node = event.getNodeA();

    effect.killAllParticles();

    effect.setLocalTranslation(node.getLocalTranslation());

    effect.emitAllParticles();

    } else if (event.getObjectB() instanceof BombControl) {

    final Spatial node = event.getNodeB();

    effect.killAllParticles();

    effect.setLocalTranslation(node.getLocalTranslation());

    effect.emitAllParticles();

    }

    }



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

    if (channel == shootingChannel) {

    channel.setAnim(“stand”);

    }

    }



    public void onAnimChange(AnimControl control, AnimChannel channel, String animName) {

    }

    }



    [/java]







    _______________________________________________________________________________________________

    ForceCharacterControl.java v1.3



    [java collapse=“true”]

    /**
  • Copyright © 2009-2010 jMonkeyEngine
  • All rights reserved.

    *
  • Redistribution and use in source and binary forms, with or without
  • modification, are permitted provided that the following conditions are
  • met:

    *
    • Redistributions of source code must retain the above copyright
  • notice, this list of conditions and the following disclaimer.

    *
    • Redistributions in binary form must reproduce the above copyright
  • notice, this list of conditions and the following disclaimer in the
  • documentation and/or other materials provided with the distribution.

    *
    • Neither the name of ‘jMonkeyEngine’ nor the names of its contributors
  • may be used to endorse or promote products derived from this software
  • without specific prior written permission.

    *
  • THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  • "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
  • TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  • PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  • CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  • EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  • PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  • PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  • LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  • NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  • SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

    */

    import com.jme3.bullet.PhysicsSpace;

    import com.jme3.bullet.PhysicsTickListener;

    import com.jme3.bullet.collision.shapes.CollisionShape;

    import com.jme3.bullet.control.CharacterControl;

    import com.jme3.math.FastMath;

    import com.jme3.math.Vector3f;

    import com.jme3.scene.Spatial;

    import com.jme3.scene.control.Control;



    /**
  • A special CharacterControl which can handle received forces.
  • <p>
  • This class extends {@link CharacterControl} and emulates the application
  • of central forces.
  • Implements {@link PhysicsTickListener}, which is necessary to time the
  • emulation with the rest of the physics system.

    *
  • @author nego
  • @version 1.3

    */

    public class ForceCharacterControl extends CharacterControl implements PhysicsTickListener {



    /**
  • The desired direction, set by the user, updated on each PhysicsTick.

    *
  • Not to be mistaken with walk direction, which is set as the sum
  • of move and force direction.

    */

    protected Vector3f moveDirection;

    /**
  • The calculated linear velocity, handled internally, updated on each
  • Physicstick.

    */

    protected Vector3f forceDirection;

    /**
  • The linear damping, which is applied at each PhysicsTick, reduces
  • the linear velocity gradually, while the character is on the ground.

    *

    */

    protected float forceDamping;

    /**
  • The minimal force amount, expressed as velocity.length().
  • If the current velocitys length is lower as this value,
  • the current velocity is reset to zero.

    *

    */

    protected float minimalForceAmount;

    /**
  • The characters gravity.

    *

    */

    protected float gravity;

    /**
  • Temporary value, used for expressing the distance to move between
  • each PhysicsTick.

    */

    private Vector3f distanceToMove;

    /**
  • Temporary value, used for expressing the dampened linear velocity,
  • which will be applied on the next PhysicsTick.

    */

    private Vector3f updatedForceDirection;

    /**
  • Temporary value, identifies if this was already in the air before.
  • Useful for reseting the vertical component of the velocity upon
  • landing.

    *

    */

    protected boolean wasInAir;



    /**
  • The default constructor. Initializes basic values.

    *

    */

    public ForceCharacterControl() {

    super();

    this.moveDirection = new Vector3f(0, 0, 0);

    this.distanceToMove = new Vector3f(0, 0, 0);

    this.forceDamping = 0.9f;

    this.forceDirection = new Vector3f(0, 0, 0);

    this.updatedForceDirection = new Vector3f(0, 0, 0);

    this.wasInAir = false;

    this.minimalForceAmount = 2f;

    this.gravity = this.getControllerId().getGravity();

    }



    /**
  • Another constructor which invokes the superclasses constructor
  • with the specified arguments

    *
  • @param CollisionShape The CollisionShape to be used for physics calculations.
  • @param stepHeigt The Heigth a Character can step up.

    */

    public ForceCharacterControl(CollisionShape shape, float stepHeight) {

    super(shape, stepHeight);

    this.moveDirection = new Vector3f(0, 0, 0);

    this.distanceToMove = new Vector3f(0, 0, 0);

    this.forceDamping = 0.9f;

    this.forceDirection = new Vector3f(0, 0, 0);

    this.updatedForceDirection = new Vector3f(0, 0, 0);

    this.wasInAir = false;

    this.minimalForceAmount = 2f;

    this.gravity = this.getControllerId().getGravity();

    }



    /**
  • Another constructor which invokes the superclasses constructor
  • with the specified arguments and sets the linear damping

    *
  • @param CollisionShape The CollisionShape to be used for physics calculations.
  • @param stepHeigt The Heigth a Character can step up.
  • @param linearDamping The amount of linear damping to be applied.

    */

    public ForceCharacterControl(CollisionShape shape, float stepHeight, float linearDamping) {

    super(shape, stepHeight);

    this.moveDirection = new Vector3f(0, 0, 0);

    this.distanceToMove = new Vector3f(0, 0, 0);

    this.forceDamping = linearDamping;

    this.forceDirection = new Vector3f(0, 0, 0);

    this.updatedForceDirection = new Vector3f(0, 0, 0);

    this.wasInAir = false;

    this.minimalForceAmount = 2f;

    this.gravity = this.getControllerId().getGravity();

    }



    /**
  • Set the force damping being applied per second.
  • A value of 0.5f will reduce the force by a half each second.
  • Values greater than that will reduce the force even further,
  • values smaller than that will lower the force dampening.

    *
  • Defaults to 0.8f

    *
  • @return forceDamping The force Damping being applied, greater than 0f and less than 1f

    */

    public float getForceDamping() {

    return forceDamping;

    }



    /**
  • Set the force damping being applied per second.
  • A value of 0.5f will reduce the force by a half each second.
  • Values greater than that will reduce the force even further,
  • values smaller than that will lower the force dampening.

    *
  • Defaults to 0.8f

    *
  • @param forceDamping The force Damping being applied, greater than 0f and less than 1f

    */

    public void setForceDamping(float forceDamping) {

    this.forceDamping = forceDamping;

    }



    /**
  • Gets the minimal amount of force to be applied.
  • If the total current force length is under the minimal force amount,
  • the force will be reset to zero.
  • Useful for eliminating too small force movements.

    *
  • Defaults to 2f.

    *
  • @return minimalForceAmount The minimal amount of force to be applied.

    */

    public float getMinimalForceAmount() {

    return minimalForceAmount;

    }



    /**
  • Sets the minimal amount of force to be applied.
  • If the total current force length is under the minimal force amount,
  • the force will be reset to zero.
  • Useful for eliminating too small force movements.

    *
  • Defaults to 2f.

    *
  • @param minimalForceAmount The minimal amount of force to be applied.

    */

    public void setMinimalForceAmount(float minimalForceAmount) {

    this.minimalForceAmount = minimalForceAmount;

    }



    /**
  • Gets the force currently applied to this.

    *
  • @return forceDirection The force that is currently applied to the character.

    */

    public Vector3f getForceDirection() {

    return forceDirection;

    }



    /**
  • Check if there is a force applied currently.
  • Note that the force must be above the minimal force amount.

    *
  • @return boolean True if there is a force applied above the treshold, false otherwise.

    */

    public boolean isForceApplied() {

    return (forceDirection.length() >= minimalForceAmount);

    }



    /**
  • Utility method for cloning this object to another Spatial.
  • Useful for controlling many Spatials with the same setup.

    *
  • @param spatial The Spatial which will receive the newly created Control.
  • @return control The newly created Control, with the same field values as this.

    */

    @Override

    public Control cloneForSpatial(Spatial spatial) {

    ForceCharacterControl control = new ForceCharacterControl(collisionShape, stepHeight, forceDamping);

    control.setCcdMotionThreshold(getCcdMotionThreshold());

    control.setCcdSweptSphereRadius(getCcdSweptSphereRadius());

    control.setCollideWithGroups(getCollideWithGroups());

    control.setCollisionGroup(getCollisionGroup());

    control.setFallSpeed(getFallSpeed());

    control.setGravity(getGravity());

    control.setJumpSpeed(getJumpSpeed());

    control.setMaxSlope(getMaxSlope());

    control.setPhysicsLocation(getPhysicsLocation());

    control.setUpAxis(getUpAxis());

    control.setApplyPhysicsLocal(isApplyPhysicsLocal());



    control.setForceDamping(getForceDamping());

    control.setMinimalForceAmount(getMinimalForceAmount());



    control.setSpatial(spatial);

    return control;

    }



    /**
  • Set the {@link PhysicsSpace} of this.

    *
  • Overriden to add/remove this as a PhysicsTickListener.

    *
  • @param physicsSpace The PhysicsSpace which this will be located in.

    */

    @Override

    public void setPhysicsSpace(PhysicsSpace physicsSpace) {



    if (physicsSpace == null) {

    if (this.getPhysicsSpace() != null) {

    this.getPhysicsSpace().removeTickListener(this);

    }

    } else {

    if (this.getPhysicsSpace() == physicsSpace) {

    return;

    }

    physicsSpace.addTickListener(this);

    }



    super.setPhysicsSpace(physicsSpace);

    }



    /**
  • Method wich applies a given velocity to this Control, which degrades on ground.
  • <p>
  • The given linear Velocity is gradually applied via PhysicsTickListener.
  • This operation is cummulative and increases the current velocity.
  • Note that the velocity is reset, when it reaches minimal force amount.
  • Note that the input velocity has to be greater than the minimal force amount.

    *
  • @param linearVelocity The initial velocity to be applied, degrades due to linear damping on ground.

    */

    public void applyCentralForce(Vector3f linearVelocity) {

    if (linearVelocity.length() >= this.minimalForceAmount) {

    this.forceDirection.addLocal(linearVelocity);

    if (this.forceDirection.getY() >= gravity - FastMath.ZERO_TOLERANCE) {

    this.forceDirection.setY ( gravity - FastMath.ZERO_TOLERANCE );

    }

    }

    }



    /**
  • Masking method, sets the walk direction for the character, as in {@link PhysicsCharacter}.
  • <p>
  • Internally is the desired user walk direction added to force direction
  • and is then called as an paramater (walkDirection + forceDirection) to
  • setWalkDirection from PhysicsCharacter.
  • Note that due to adding of "forces", the character is still able to counteract
  • an amount of force.

    *
  • @param vec The walk direction the user wants to go, applied continously, until canceled with a zero Vector3f.

    */

    @Override

    public void setWalkDirection(Vector3f vec) {

    this.moveDirection = vec;

    }



    /**
  • Masking method, gets the walk direction for the character, as in {@link PhysicsCharacter}.
  • <p>
  • Internally is the desired user walk direction added to force direction
  • and is then called as an paramater (walkDirection + forceDirection) to
  • setWalkDirection from PhysicsCharacter.
  • Note that due to adding of "forces", the character is still able to counteract
  • an amount of force.

    *
  • @return walkDirection The walk direction the user wants to go, applied continously, until canceled with a zero Vector3f.

    */

    @Override

    public Vector3f getWalkDirection () {

    return this.moveDirection;

    }



    /**
  • Internal method, should NOT be called, this method calculates the distance
  • to move within the next physics tick.
  • <br>
  • This distance is calculated with the formula distance = (initialVelocity +
  • finalVelocity) * timeDelta / 2. The force damping is applied as newVelocity =
  • oldVelocity * (1-forceDamping) ^ timeDelta. TimeDelta is the rate at which
  • this PhysicsTick occurs, usually 1/60.
  • The desired move direction is added to this force direction and is then
  • applied via super.setWalkDirection (moveDirection+forceDirection).
  • <p>
  • Note that the force dampening is only applied on ground, as the underlying
  • {@link KinematicCharacterController} is handling vertical deacceleration while
  • this is in the air.
  • Note that this method checks for a minimal velocity, set by setMinimalForceAmount(
),
  • and sets the velocity to zero, once the velocity is smaller than that specified
  • treshold in order to avoid minimal movements (which can produce stuttering).
  • Note that once this reaches ground, and the vertical velocity is negative,
  • the vertical velocity is reset, to avoid some conflicts with the floor.
  • Note that once this reaches ground, after being in the air, the vertical velocity
  • is set to zero, to avoid bouncing.

    /

    public void prePhysicsTick(PhysicsSpace space, float f) {

    //if there is a force above the treshold, apply force+movement

    if (forceDirection.length() > 0) {



    //old check: if (onGround() || forceDirection.y<this.gravity
    f)

    //eliminates odd behaviour on BoxCollisionShape floor

    //but prevents correct behaviour when an upforce and downforce

    //is applied simultaniously to this control



    //we are on ground, apply linear dampening

    if (onGround()) {

    //we have landed, reset vertical component to avoid bouncing

    if (wasInAir) {

    forceDirection.setY(0f);

    wasInAir = false;

    }

    //we are on ground, no use of negative vertical velocity

    if (forceDirection.getY() < 0f) {

    forceDirection.setY(0f);

    }



    //calculate the final velocity for the current delta time

    float decreasingFactor = FastMath.pow(1f - forceDamping, f);

    //create new Vector for updatedForceDirection

    updatedForceDirection = forceDirection.mult(decreasingFactor);



    //calculate the force distance to move on next physic tick

    distanceToMove = forceDirection.add(updatedForceDirection);

    distanceToMove.multLocal(0.5f);

    distanceToMove.multLocal(f);



    //reset the new force direction if its < minimalForceamount

    if (updatedForceDirection.length() < minimalForceAmount) {

    updatedForceDirection.set(0, 0, 0);

    }

    //update the force direction for the next calculations

    //point to the Vector of the (old) updatedForceDirection

    forceDirection = updatedForceDirection;

    } else {

    //in air, no dampening

    distanceToMove = forceDirection.mult(f);

    }

    super.setWalkDirection(distanceToMove.add(moveDirection));

    //else apply movement only

    } else {

    super.setWalkDirection(moveDirection);

    }

    }



    /**
  • Internal method, should NOT be called, this method checks if the character
  • has been in air.

    */

    public void physicsTick(PhysicsSpace space, float f) {

    //only do something if we have a sufficient force

    if (forceDirection.length() > 0) {

    if (!onGround()) {

    wasInAir = true;

    }

    }

    }



    /**
  • Sets the gravity for this, as in {@link PhysicsCharacter}.

    *
  • @param value The gravity to set.

    */

    @Override

    public void setGravity(float value) {

    super.setGravity(value);

    this.gravity = this.getControllerId().getGravity();

    }

    }







    [/java]
3 Likes

Hey, cool. Gonna try this later. Thanks!

EDIT: found the error, it had something to do with my code, 8)

EIDT2: updated it to v1.1 → the character should land properly now

EDIT3: minor update 1.2

EDIT4: minor update 1.3

This is just awesome :smiley: Will try this out asap. Thanks!



Q: Does this character moves with the moving platform that it is standing on? Or it remains stationary?

The character behaves exactly like the normal CharacterControl. In fact the ForceCharacterControl does nothing without implementing the collisions between the ForceCharacterControl and another CollisionObject

It’s a little bit outdated now, do you have a more recent version or haven’t you changed it anymore?

Don’t know how I didn’t find this 6 months ago. I’ve been meaning to try something similar


Probably this should be made a library plugin


Why does it build upon the current character control? That means it gets all the issues the current character control has, and now you have to use forces to control it which most people actually don’t like.



Instead it should build the character based on regular rigid bodies, and make it controllable by forces.

There’s a tutorial on how to do this properly here: http://csis.pace.edu/~benjamin/projects/virtualworldprojects/OgreOdeWalkingCharacter.pdf

Its intended for ODE but should work just fine with bullet. I used this method myself actually, it worked pretty well.