/* * Copyright (c) 2009-2018 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. */ package com.jme3.bullet.control; import com.jme3.bullet.PhysicsSpace; import com.jme3.bullet.PhysicsTickListener; import com.jme3.bullet.collision.PhysicsRayTestResult; import com.jme3.bullet.collision.shapes.CapsuleCollisionShape; import com.jme3.bullet.collision.shapes.CollisionShape; import com.jme3.bullet.collision.shapes.CompoundCollisionShape; import com.jme3.bullet.objects.PhysicsRigidBody; import com.jme3.export.InputCapsule; import com.jme3.export.JmeExporter; import com.jme3.export.JmeImporter; import com.jme3.export.OutputCapsule; import com.jme3.math.FastMath; import com.jme3.math.Quaternion; import com.jme3.math.Vector3f; import com.jme3.renderer.RenderManager; import com.jme3.renderer.ViewPort; import com.jme3.scene.Spatial; import com.jme3.util.TempVars; import com.jme3.util.clone.JmeCloneable; import java.io.IOException; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; /** * This class is intended to replace the CharacterControl class. *
* A rigid body with cylinder collision shape is used and its velocity is set * continuously. A ray test is used to test whether the character is on the * ground. *
* The character keeps their own local coordinate system which adapts based on * the gravity working on the character so they will always stand upright. *
* Motion in the local X-Z plane is damped. *
* This class is shared between JBullet and Native Bullet. * * @author normenhansen */ public class BetterCharacterControl extends AbstractPhysicsControl implements PhysicsTickListener, JmeCloneable { protected static final Logger logger = Logger.getLogger(BetterCharacterControl.class.getName()); protected PhysicsRigidBody rigidBody; protected float radius; protected float height; /** * mass of this character (>0) */ protected float mass; /** * relative height when ducked (1=full height) */ protected float duckedFactor = 0.6f; /** * local up direction, derived from gravity */ protected final Vector3f localUp = new Vector3f(0, 1, 0); /** * Local absolute z-forward direction, derived from gravity and UNIT_Z, * updated continuously when gravity changes. */ protected final Vector3f localForward = new Vector3f(0, 0, 1); /** * Local left direction, derived from up and forward. */ protected final Vector3f localLeft = new Vector3f(1, 0, 0); /** * Local z-forward quaternion for the "local absolute" z-forward direction. */ protected final Quaternion localForwardRotation = new Quaternion(Quaternion.DIRECTION_Z); /** * a Z-forward vector based on the view direction and the local X-Z plane. */ protected final Vector3f viewDirection = new Vector3f(0, 0, 1); /** * spatial location, corresponds to RigidBody location. */ protected final Vector3f location = new Vector3f(); /** * spatial rotation, a Z-forward rotation based on the view direction and * local X-Z plane. * * @see #rotatedViewDirection */ protected final Quaternion rotation = new Quaternion(Quaternion.DIRECTION_Z); protected final Vector3f rotatedViewDirection = new Vector3f(0, 0, 1); protected final Vector3f walkDirection = new Vector3f(); protected final Vector3f jumpForce; /** * X-Z motion damping factor (0→no damping, 1=no external forces, * default=0.9) */ protected float physicsDamping = 0.9f; protected final Vector3f scale = new Vector3f(1, 1, 1); protected final Vector3f velocity = new Vector3f(); protected boolean jump = false; protected boolean onGround = false; protected boolean ducked = false; protected boolean wantToUnDuck = false; /** * No-argument constructor needed by SavableClassUtil. Do not invoke * directly! */ public BetterCharacterControl() { jumpForce = new Vector3f(); } /** * Instantiate an enabled control with the specified properties. *
* The final height when ducking must be larger than 2x radius. The
* jumpForce will be set to an upward force of 5x mass.
*
* @param radius the radius of the character's collision shape (>0)
* @param height the height of the character's collision shape
* (>2*radius)
* @param mass the character's mass (≥0)
*/
public BetterCharacterControl(float radius, float height, float mass) {
this.radius = radius;
this.height = height;
this.mass = mass;
rigidBody = new PhysicsRigidBody(getShape(), mass);
jumpForce = new Vector3f(0, mass * 5, 0);
rigidBody.setAngularFactor(0);
}
/**
* Update this control. Invoked once per frame during the logical-state
* update, provided the control is added to a scene graph. Do not invoke
* directly from user code.
*
* @param tpf the time interval between frames (in seconds, ≥0)
*/
@Override
public void update(float tpf) {
super.update(tpf);
rigidBody.getPhysicsLocation(location);
//rotation has been set through viewDirection
applyPhysicsTransform(location, rotation);
}
/**
* Render this control. Invoked once per view port per frame, provided the
* control is added to a scene. Should be invoked only by a subclass or by
* the RenderManager.
*
* @param rm the render manager (not null)
* @param vp the view port to render (not null)
*/
@Override
public void render(RenderManager rm, ViewPort vp) {
super.render(rm, vp);
}
/**
* Callback from Bullet, invoked just before the physics is stepped.
*
* @param space the space that is about to be stepped (not null)
* @param tpf the time per physics step (in seconds, ≥0)
*/
public void prePhysicsTick(PhysicsSpace space, float tpf) {
checkOnGround();
if (wantToUnDuck && checkCanUnDuck()) {
setHeightPercent(1);
wantToUnDuck = false;
ducked = false;
}
TempVars vars = TempVars.get();
Vector3f currentVelocity = vars.vect2.set(velocity);
// Attenuate any existing X-Z motion.
float existingLeftVelocity = velocity.dot(localLeft);
float existingForwardVelocity = velocity.dot(localForward);
Vector3f counter = vars.vect1;
existingLeftVelocity = existingLeftVelocity * physicsDamping;
existingForwardVelocity = existingForwardVelocity * physicsDamping;
counter.set(-existingLeftVelocity, 0, -existingForwardVelocity);
localForwardRotation.multLocal(counter);
velocity.addLocal(counter);
float designatedVelocity = walkDirection.length();
if (designatedVelocity > 0) {
Vector3f localWalkDirection = vars.vect1;
//normalize walkdirection
localWalkDirection.set(walkDirection).normalizeLocal();
//check for the existing velocity in the desired direction
float existingVelocity = velocity.dot(localWalkDirection);
//calculate the final velocity in the desired direction
float finalVelocity = designatedVelocity - existingVelocity;
localWalkDirection.multLocal(finalVelocity);
//add resulting vector to existing velocity
velocity.addLocal(localWalkDirection);
}
if(currentVelocity.distance(velocity) > FastMath.ZERO_TOLERANCE) rigidBody.setLinearVelocity(velocity);
if (jump) {
//TODO: precalculate jump force
Vector3f rotatedJumpForce = vars.vect1;
rotatedJumpForce.set(jumpForce);
rigidBody.applyImpulse(localForwardRotation.multLocal(rotatedJumpForce), Vector3f.ZERO);
jump = false;
}
vars.release();
}
/**
* Callback from Bullet, invoked just after the physics has been stepped.
*
* @param space the space that was just stepped (not null)
* @param tpf the time per physics step (in seconds, ≥0)
*/
public void physicsTick(PhysicsSpace space, float tpf) {
rigidBody.getLinearVelocity(velocity);
}
/**
* Move the character somewhere. Note the character also warps to the
* location of the spatial when the control is added.
*
* @param vec the desired character location (not null)
*/
public void warp(Vector3f vec) {
setPhysicsLocation(vec);
}
/**
* Makes the character jump with the set jump force.
*/
public void jump() {
//TODO: debounce over some frames
if (!onGround) {
return;
}
jump = true;
}
/**
* Alter the jump force. The jump force is local to the character's
* coordinate system, which normally is always z-forward (in world
* coordinates, parent coordinates when set to applyLocalPhysics)
*
* @param jumpForce the desired jump force (not null, unaffected)
*/
public void setJumpForce(Vector3f jumpForce) {
this.jumpForce.set(jumpForce);
}
/**
* Access the jump force. The default is 5 * character mass in Y direction.
*
* @return the pre-existing vector (not null)
*/
public Vector3f getJumpForce() {
return jumpForce;
}
/**
* Check if the character is on the ground. This is determined by a ray test
* in the center of the character and might return false even if the
* character is not falling yet.
*
* @return true if on the ground, otherwise false
*/
public boolean isOnGround() {
return onGround;
}
/**
* Toggle character ducking. When ducked the characters capsule collision
* shape height will be multiplied by duckedFactor to make the capsule
* smaller. When unducking, the character will check with a ray test if it
* can in fact unduck and only do so when its possible. You can check the
* state of the unducking by checking isDucked().
*
* @param enabled true→duck, false→unduck
*/
public void setDucked(boolean enabled) {
if (enabled) {
setHeightPercent(duckedFactor);
ducked = true;
wantToUnDuck = false;
} else {
if (checkCanUnDuck()) {
setHeightPercent(1);
ducked = false;
} else {
wantToUnDuck = true;
}
}
}
/**
* Check if the character is ducking, either due to user input or due to
* unducking being impossible at the moment (obstacle above).
*
* @return true if ducking, otherwise false
*/
public boolean isDucked() {
return ducked;
}
/**
* Alter the height multiplier for ducking.
*
* @param factor the factor by which the height should be multiplied when
* ducking (≥0, ≤1)
*/
public void setDuckedFactor(float factor) {
duckedFactor = factor;
}
/**
* Read the height multiplier for ducking.
*
* @return the factor (≥0, ≤1)
*/
public float getDuckedFactor() {
return duckedFactor;
}
/**
* Alter the character's the walk direction. This parameter is framerate
* independent and the character will move continuously in the direction
* given by the vector with the speed given by the vector length in m/s.
*
* @param vec The movement direction and speed in m/s
*/
public void setWalkDirection(Vector3f vec) {
walkDirection.set(vec);
}
/**
* Read the walk velocity. The length of the vector defines the speed.
*
* @return the pre-existing vector (not null)
*/
public Vector3f getWalkDirection() {
return walkDirection;
}
/**
* Alter the character's view direction. Note this only defines the
* orientation in the local X-Z plane.
*
* @param vec a direction vector (not null, unaffected)
*/
public void setViewDirection(Vector3f vec) {
viewDirection.set(vec);
updateLocalViewDirection();
}
/**
* Access the view direction. This need not agree with the spatial's forward
* direction.
*
* @return the pre-existing vector (not null)
*/
public Vector3f getViewDirection() {
return viewDirection;
}
/**
* Realign the local forward vector to given direction vector, if null is
* supplied Vector3f.UNIT_Z is used. The input vector must be perpendicular
* to gravity vector. This normally only needs to be invoked when the
* gravity direction changed continuously and the local forward vector is
* off due to drift. E.g. after walking around on a sphere "planet" for a
* while and then going back to a Y-up coordinate system the local Z-forward
* might not be 100% aligned with the Z axis.
*
* @param vec the desired forward vector (perpendicular to the gravity
* vector, may be null, default=0,0,1)
*/
public void resetForward(Vector3f vec) {
if (vec == null) {
vec = Vector3f.UNIT_Z;
}
localForward.set(vec);
updateLocalCoordinateSystem();
}
/**
* Access the character's linear velocity in physics-space coordinates.
*
* @return the pre-existing vector (not null)
*/
public Vector3f getVelocity() {
return velocity;
}
/**
* Alter the gravity acting on this character. Note that this also realigns
* the local coordinate system of the character so that continuous changes
* in gravity direction are possible while maintaining a sensible control
* over the character.
*
* @param gravity an acceleration vector (not null, unaffected)
*/
public void setGravity(Vector3f gravity) {
rigidBody.setGravity(gravity);
localUp.set(gravity).normalizeLocal().negateLocal();
updateLocalCoordinateSystem();
}
/**
* Copy the character's gravity vector.
*
* @return a new acceleration vector (not null)
*/
public Vector3f getGravity() {
return rigidBody.getGravity();
}
/**
* Copy the character's gravity vector.
*
* @param store storage for the result (modified if not null)
* @return an acceleration vector (either the provided storage or a new
* vector, not null)
*/
public Vector3f getGravity(Vector3f store) {
return rigidBody.getGravity(store);
}
/**
* Alter how much motion in the local X-Z plane is damped.
*
* @param physicsDamping the desired damping factor (0→no damping, 1=no
* external forces, default=0.9)
*/
public void setPhysicsDamping(float physicsDamping) {
this.physicsDamping = physicsDamping;
}
/**
* Read how much motion in the local X-Z plane is damped.
*
* @return the damping factor (0→no damping, 1=no external forces)
*/
public float getPhysicsDamping() {
return physicsDamping;
}
/**
* Alter the height of collision shape.
*
* @param percent the desired height, as a percentage of the full height
*/
protected void setHeightPercent(float percent) {
scale.setY(percent);
rigidBody.setCollisionShape(getShape());
}
/**
* Test whether the character is on the ground, by means of a ray test.
*/
protected void checkOnGround() {
TempVars vars = TempVars.get();
Vector3f location = vars.vect1;
Vector3f rayVector = vars.vect2;
float height = getFinalHeight();
location.set(localUp).multLocal(height).addLocal(this.location);
rayVector.set(localUp).multLocal(-height - 0.1f).addLocal(location);
List
* We don't set the actual physics rotation but the view rotation here. It
* might actually be altered by the calculateNewForward method.
*
* @param quat desired orientation (not null, unaffected)
*/
@Override
protected void setPhysicsRotation(Quaternion quat) {
rotation.set(quat);
rotation.multLocal(rotatedViewDirection.set(viewDirection));
updateLocalViewDirection();
}
/**
* Add all managed physics objects to the specified space.
*
* @param space which physics space to add to (not null)
*/
@Override
protected void addPhysics(PhysicsSpace space) {
space.getGravity(localUp).normalizeLocal().negateLocal();
updateLocalCoordinateSystem();
space.addCollisionObject(rigidBody);
space.addTickListener(this);
}
/**
* Remove all managed physics objects from the specified space.
*
* @param space which physics space to remove from (not null)
*/
@Override
protected void removePhysics(PhysicsSpace space) {
space.removeCollisionObject(rigidBody);
space.removeTickListener(this);
}
/**
* Create spatial-dependent data. Invoked when this control is added to a
* spatial.
*
* @param spat the controlled spatial (not null, alias created)
*/
@Override
protected void createSpatialData(Spatial spat) {
rigidBody.setUserObject(spatial);
}
/**
* Destroy spatial-dependent data. Invoked when this control is removed from
* a spatial.
*
* @param spat the previously controlled spatial (not null)
*/
@Override
protected void removeSpatialData(Spatial spat) {
rigidBody.setUserObject(null);
}
/**
* Create a shallow clone for the JME cloner.
*
* @return a new control (not null)
*/
@Override
public Object jmeClone() {
BetterCharacterControl control = new BetterCharacterControl(radius, height, mass);
control.setJumpForce(jumpForce);
control.spatial = this.spatial;
return control;
}
/**
* Serialize this control, for example when saving to a J3O file.
*
* @param ex exporter (not null)
* @throws IOException from exporter
*/
@Override
public void write(JmeExporter ex) throws IOException {
super.write(ex);
OutputCapsule oc = ex.getCapsule(this);
oc.write(radius, "radius", 1);
oc.write(height, "height", 1);
oc.write(mass, "mass", 1);
oc.write(jumpForce, "jumpForce", new Vector3f(0, mass * 5, 0));
oc.write(physicsDamping, "physicsDamping", 0.9f);
}
/**
* De-serialize this control, for example when loading from a J3O file.
*
* @param im importer (not null)
* @throws IOException from importer
*/
@Override
public void read(JmeImporter im) throws IOException {
super.read(im);
InputCapsule in = im.getCapsule(this);
this.radius = in.readFloat("radius", 1);
this.height = in.readFloat("height", 2);
this.mass = in.readFloat("mass", 80);
this.physicsDamping = in.readFloat("physicsDamping", 0.9f);
this.jumpForce.set((Vector3f) in.readSavable("jumpForce", new Vector3f(0, mass * 5, 0)));
rigidBody = new PhysicsRigidBody(getShape(), mass);
jumpForce.set(new Vector3f(0, mass * 5, 0));
rigidBody.setAngularFactor(0);
}
}