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
- 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
- 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
- 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
- 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
- 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
- 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.gravityf)
//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]