diff --git a/engine/src/bullet-common/com/jme3/bullet/control/BetterCharacterControl.java b/engine/src/bullet-common/com/jme3/bullet/control/BetterCharacterControl.java new file mode 100644 index 000000000..e6abf187d --- /dev/null +++ b/engine/src/bullet-common/com/jme3/bullet/control/BetterCharacterControl.java @@ -0,0 +1,685 @@ +/* + * Copyright (c) 2009-2012 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.debug.DebugTools; +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.scene.control.Control; +import com.jme3.util.TempVars; +import java.io.IOException; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This is intended to be a replacement for the internal bullet character class. + * A RigidBody with cylinder collision shape is used and its velocity is set + * continuously, a ray test is used to check if the character is on the ground. + * + * The character keeps his own local coordinate system which adapts based on the + * gravity working on the character so the character will always stand upright. + * + * Forces in the local x/z plane are dampened while those in the local y + * direction are applied fully (e.g. jumping, falling). + * + * @author normenhansen + */ +public class BetterCharacterControl extends AbstractPhysicsControl implements PhysicsTickListener { + + protected static final Logger logger = Logger.getLogger(BetterCharacterControl.class.getName()); + protected PhysicsRigidBody rigidBody; + protected float radius; + protected float height; + protected float mass; + 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 z-forward quaternion for the "local absolute" z-forward direction. + */ + protected final Quaternion localForwardRotation = new Quaternion(Quaternion.DIRECTION_Z); + /** + * Is a z-forward vector based on the view direction and the current local + * x/z plane. + */ + protected final Vector3f viewDirection = new Vector3f(0, 0, 1); + /** + * Stores final spatial location, corresponds to RigidBody location. + */ + protected final Vector3f location = new Vector3f(); + /** + * Stores final spatial rotation, is a z-forward rotation based on the view + * direction and the current local x/z plane. See also 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; + protected final Vector3f physicsDampening = new Vector3f(0.3f, 0, 0.3f); + 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; + + /** + * Only used for serialization, do not use this constructor. + */ + public BetterCharacterControl() { + jumpForce = new Vector3f(); + } + + /** + * Creates a new character with the given properties. Note that to avoid + * issues the final height when ducking should be larger than 2x radius. The + * jumpForce will be set to an upwards force of 5x mass. + * + * @param radius + * @param height + * @param mass + */ + 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); + } + + @Override + public void update(float tpf) { + super.update(tpf); + rigidBody.getPhysicsLocation(location); + //rotation has been set through viewDirection + applyPhysicsTransform(location, rotation); + debugTools.setPinkArrow(location, localForward); + } + private DebugTools debugTools = null; + + public void setDebugTools(DebugTools debugTools) { + this.debugTools = debugTools; + } + + @Override + public void render(RenderManager rm, ViewPort vp) { + super.render(rm, vp); + debugTools.show(rm, vp); + } + + /** + * Used internally, don't call manually + * + * @param space + * @param tpf + */ + public void prePhysicsTick(PhysicsSpace space, float tpf) { + checkOnGround(); + if (wantToUnDuck && checkCanUnDuck()) { + setHeightPercent(1); + wantToUnDuck = false; + ducked = false; + } + + //TODO: this damping (physicsInfluence) is not framerate decoupled +// Vector3f physicsPlane = localForwardRotation.mult(physicsDampening); +// Vector3f counter = velocity.mult(physicsPlane).negateLocal().multLocal(tpf * 100.0f); +// velocity.addLocal(counter); +// debugTools.setGreenArrow(location, counter); + + debugTools.setBlueArrow(location, walkDirection); + + float designatedVelocity = walkDirection.length(); + if (designatedVelocity > 0) { + TempVars vars = TempVars.get(); + 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 + debugTools.setYellowArrow(location, localWalkDirection); + velocity.addLocal(localWalkDirection); + vars.release(); + } else { + debugTools.setYellowArrow(location, Vector3f.ZERO); + } + rigidBody.setLinearVelocity(velocity); + if (jump) { + //TODO: precalculate jump force + TempVars vars = TempVars.get(); + Vector3f rotatedJumpForce = vars.vect1; + rotatedJumpForce.set(jumpForce); + rigidBody.applyImpulse(localForwardRotation.multLocal(rotatedJumpForce), Vector3f.ZERO); + jump = false; + vars.release(); + } + } + + /** + * Used internally, don't call manually + * + * @param space + * @param tpf + */ + public void physicsTick(PhysicsSpace space, float tpf) { + rigidBody.getLinearVelocity(velocity); + debugTools.setRedArrow(location, velocity); + } + + /** + * Move the character somewhere. Note the character also takes the location + * of any spatial its being attached to in the moment it is attached. + * + * @param vec The new character location. + */ + 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; + } + + /** + * Set the jump force as a Vector3f. The jump force is local to the + * characters coordinate system, which normally is always z-forward (in + * world coordinates, parent coordinates when set to applyLocalPhysics) + * + * @param jumpForce The new jump force + */ + public void setJumpForce(Vector3f jumpForce) { + this.jumpForce.set(jumpForce); + } + + /** + * Gets the current jump force. The default is 5 * character mass in y + * direction. + * + * @return + */ + 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 + */ + 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 + */ + 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 + */ + public boolean isDucked() { + return ducked; + } + + /** + * Sets the height multiplication factor for ducking. + * + * @param factor The factor by which the height should be multiplied when + * ducking + */ + public void setDuckedFactor(float factor) { + duckedFactor = factor; + } + + /** + * Gets the height multiplication factor for ducking. + * + * @return + */ + public float getDuckedFactor() { + return duckedFactor; + } + + /** + * Sets the walk direction of the character. 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); + } + + /** + * Gets the current walk direction and speed of the character. The length of + * the vector defines the speed. + * + * @return + */ + public Vector3f getWalkDirection() { + return walkDirection; + } + + /** + * Sets the view direction for the character. Note this only defines the + * rotation of the spatial in the local x/z plane of the character. + * + * @param vec + */ + public void setViewDirection(Vector3f vec) { + viewDirection.set(vec); + updateLocalViewDirection(); + } + + /** + * Gets the current view direction, note this doesn't need to correspond + * with the spatials forward direction. + * + * @return + */ + public Vector3f getViewDirection() { + return viewDirection; + } + + /** + * Realign the local forward vector to given direction vector, if null is + * supplied Vector3f.UNIT_Z is used. Input vector has to be perpendicular to + * current gravity vector. This normally only needs to be called 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% alinged with Z axis. + * + * @param vec The new forward vector, has to be perpendicular to the current + * gravity vector! + */ + public void resetForward(Vector3f vec) { + localForward.set(vec); + updateLocalCoordinateSystem(); + } + + /** + * Get the current linear velocity along the three axes of the character. + * This is prepresented in world coordinates, parent coordinates when the + * control is set to applyLocalPhysics. + * + * @return The current linear velocity of the character + */ + public Vector3f getVelocity() { + return velocity; + } + + /** + * Set the gravity for 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 + */ + public void setGravity(Vector3f gravity) { + rigidBody.setGravity(gravity); + localUp.set(gravity).normalizeLocal().negateLocal(); + updateLocalCoordinateSystem(); + } + + /** + * Get the current gravity of the character. + * + * @return + */ + public Vector3f getGravity() { + return rigidBody.getGravity(); + } + + /** + * Get the current gravity of the character. + * + * @param store The vector to store the result in + * @return + */ + public Vector3f getGravity(Vector3f store) { + return rigidBody.getGravity(store); + } + + /** + * This actually sets a new collision shape to the character to change the + * height of the capsule. + * + * @param percent + */ + protected void setHeightPercent(float percent) { + scale.setY(percent); + rigidBody.setCollisionShape(getShape()); + } + + /** + * This checks if the character is on the ground by doing 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 - FastMath.ZERO_TOLERANCE).addLocal(location); + debugTools.setMagentaArrow(location, rayVector.subtract(location)); + List results = space.rayTest(location, rayVector); + vars.release(); + for (PhysicsRayTestResult physicsRayTestResult : results) { + if (!physicsRayTestResult.getCollisionObject().equals(rigidBody)) { + onGround = true; + return; + } + } + onGround = false; + } + + /** + * This checks if the character can go from ducked to unducked state by + * doing a ray test. + */ + protected boolean checkCanUnDuck() { + TempVars vars = TempVars.get(); + Vector3f location = vars.vect1; + Vector3f rayVector = vars.vect2; + location.set(localUp).multLocal(FastMath.ZERO_TOLERANCE).addLocal(this.location); + rayVector.set(localUp).multLocal(height + FastMath.ZERO_TOLERANCE).addLocal(location); + debugTools.setMagentaArrow(location, rayVector.subtract(location)); + List results = space.rayTest(location, rayVector); + vars.release(); + for (PhysicsRayTestResult physicsRayTestResult : results) { + if (!physicsRayTestResult.getCollisionObject().equals(rigidBody)) { + return false; + } + } + debugTools.setMagentaArrow(location, Vector3f.ZERO); + return true; + } + + /** + * Gets a new collision shape based on the current scale parameter. The + * created collisionshape is a capsule collision shape that is attached to a + * compound collision shape with an offset to set the object center at the + * bottom of the capsule. + * + * @return + */ + protected CollisionShape getShape() { + //TODO: cleanup size mess.. + CapsuleCollisionShape capsuleCollisionShape = new CapsuleCollisionShape(getFinalRadius(), (getFinalHeight() - (2 * getFinalRadius()))); + CompoundCollisionShape compoundCollisionShape = new CompoundCollisionShape(); + Vector3f addLocation = new Vector3f(0, (getFinalHeight() / 2.0f), 0); + compoundCollisionShape.addChildShape(capsuleCollisionShape, addLocation); + return compoundCollisionShape; + } + + /** + * Gets the scaled height. + * + * @return + */ + protected float getFinalHeight() { + return height * scale.getY(); + } + + /** + * Gets the scaled radius. + * + * @return + */ + protected float getFinalRadius() { + return radius * scale.getZ(); + } + + /** + * Updates the local coordinate system from the localForward and localUp + * vectors, adapts localForward, sets localForwardRotation quaternion to + * local z-forward rotation. + */ + protected void updateLocalCoordinateSystem() { + //gravity vector has possibly changed, calculate new world forward (UNIT_Z) + calculateNewForward(localForwardRotation, localForward, localUp); + rigidBody.setPhysicsRotation(localForwardRotation); + updateLocalViewDirection(); + } + + /** + * Updates the local x/z-flattened view direction and the corresponding + * rotation quaternion for the spatial. + */ + protected void updateLocalViewDirection() { + //update local rotation quaternion to use for view rotation + localForwardRotation.multLocal(rotatedViewDirection.set(viewDirection)); + calculateNewForward(rotation, rotatedViewDirection, localUp); + } + + /** + * This method works similar to Camera.lookAt but where lookAt sets the + * priority on the direction, this method sets the priority on the up vector + * so that the result direction vector and rotation is guaranteed to be + * perpendicular to the up vector. + * + * @param rotation The rotation to set the result on or null to create a new + * Quaternion, this will be set to the new "z-forward" rotation if not null + * @param direction The direction to base the new look direction on, will be + * set to the new direction + * @param worldUpVector The up vector to use, the result direction will be + * perpendicular to this + * @return + */ + protected final void calculateNewForward(Quaternion rotation, Vector3f direction, Vector3f worldUpVector) { + if (direction == null) { + return; + } + TempVars vars = TempVars.get(); + Vector3f newLeft = vars.vect1; + Vector3f newLeftNegate = vars.vect2; + + newLeft.set(worldUpVector).crossLocal(direction).normalizeLocal(); + if (newLeft.equals(Vector3f.ZERO)) { + if (direction.x != 0) { + newLeft.set(direction.y, -direction.x, 0f).normalizeLocal(); + } else { + newLeft.set(0f, direction.z, -direction.y).normalizeLocal(); + } + logger.log(Level.INFO, "Zero left for direction {0}, up {1}", new Object[]{direction, worldUpVector}); + } + newLeftNegate.set(newLeft).negateLocal(); + direction.set(worldUpVector).crossLocal(newLeftNegate).normalizeLocal(); + if (direction.equals(Vector3f.ZERO)) { + direction.set(Vector3f.UNIT_Z); + logger.log(Level.INFO, "Zero left for left {0}, up {1}", new Object[]{newLeft, worldUpVector}); + } + if (rotation != null) { + rotation.fromAxes(newLeft, worldUpVector, direction); + } + vars.release(); + } + + /** + * This is implemented from AbstractPhysicsControl and called when the + * spatial is attached for example. + * + * @param vec + */ + @Override + protected void setPhysicsLocation(Vector3f vec) { + rigidBody.setPhysicsLocation(vec); + location.set(vec); + } + + /** + * We set the current spatial as UserObject so the user can find his + * spatial. + * + * @param spatial + */ + @Override + public void setSpatial(Spatial spatial) { + super.setSpatial(spatial); + rigidBody.setUserObject(spatial); + } + + /** + * This is implemented from AbstractPhysicsControl and called when the + * spatial is attached for example. We don't set the actual physics rotation + * but the view rotation here. It might actually be altered by the + * calculateNewForward method. + * + * @param quat + */ + @Override + protected void setPhysicsRotation(Quaternion quat) { + rotation.set(quat); + rotation.multLocal(rotatedViewDirection.set(viewDirection)); + updateLocalViewDirection(); + } + + /** + * This is implemented from AbstractPhysicsControl and called when the + * control is supposed to add all objects to the physics space. + * + * @param space + */ + @Override + protected void addPhysics(PhysicsSpace space) { + space.getGravity(localUp).normalizeLocal().negateLocal(); + updateLocalCoordinateSystem(); + + space.addCollisionObject(rigidBody); + space.addTickListener(this); + } + + /** + * This is implemented from AbstractPhysicsControl and called when the + * control is supposed to remove all objects from the physics space. + * + * @param space + */ + @Override + protected void removePhysics(PhysicsSpace space) { + space.removeCollisionObject(rigidBody); + space.removeTickListener(this); + } + + public Control cloneForSpatial(Spatial spatial) { + BetterCharacterControl control = new BetterCharacterControl(radius, height, mass); + control.setJumpForce(jumpForce); + control.setSpatial(spatial); + return control; + } + + @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)); + } + + @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.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); + } +} diff --git a/engine/src/test/jme3test/bullet/TestBetterCharacter.java b/engine/src/test/jme3test/bullet/TestBetterCharacter.java new file mode 100644 index 000000000..b73790959 --- /dev/null +++ b/engine/src/test/jme3test/bullet/TestBetterCharacter.java @@ -0,0 +1,287 @@ +/* + * Copyright (c) 2009-2012 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 jme3test.bullet; + +import com.jme3.app.SimpleApplication; +import com.jme3.bullet.BulletAppState; +import com.jme3.bullet.PhysicsSpace; +import com.jme3.bullet.collision.shapes.MeshCollisionShape; +import com.jme3.bullet.control.BetterCharacterControl; +import com.jme3.bullet.control.RigidBodyControl; +import com.jme3.bullet.debug.DebugTools; +import com.jme3.input.KeyInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.KeyTrigger; +import com.jme3.material.Material; +import com.jme3.math.FastMath; +import com.jme3.math.Quaternion; +import com.jme3.math.Vector3f; +import com.jme3.renderer.RenderManager; +import com.jme3.scene.CameraNode; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.scene.control.CameraControl.ControlDirection; +import com.jme3.scene.shape.Sphere; +import com.jme3.system.AppSettings; + +/** + * A walking physical character followed by a 3rd person camera. (No animation.) + * + * @author normenhansen, zathras + */ +public class TestBetterCharacter extends SimpleApplication implements ActionListener { + + private BulletAppState bulletAppState; + private BetterCharacterControl physicsCharacter; + private Node characterNode; + private CameraNode camNode; + boolean rotate = false; + private Vector3f walkDirection = new Vector3f(0, 0, 0); + private Vector3f viewDirection = new Vector3f(0, 0, 1); + boolean leftStrafe = false, rightStrafe = false, forward = false, backward = false, + leftRotate = false, rightRotate = false; + private Vector3f normalGravity = new Vector3f(0, -9.81f, 0); + private Geometry planet; + + public static void main(String[] args) { + TestBetterCharacter app = new TestBetterCharacter(); + AppSettings settings = new AppSettings(true); + settings.setRenderer(AppSettings.LWJGL_OPENGL2); + settings.setAudioRenderer(AppSettings.LWJGL_OPENAL); + app.setSettings(settings); + app.start(); + } + + @Override + public void simpleInitApp() { + //setup keyboard mapping + setupKeys(); + + // activate physics + bulletAppState = new BulletAppState(); + stateManager.attach(bulletAppState); + bulletAppState.setDebugEnabled(true); + + // init a physics test scene + PhysicsTestHelper.createPhysicsTestWorldSoccer(rootNode, assetManager, bulletAppState.getPhysicsSpace()); + PhysicsTestHelper.createBallShooter(this, rootNode, bulletAppState.getPhysicsSpace()); + setupPlanet(); + + // Create a node for the character model + characterNode = new Node("character node"); + characterNode.setLocalTranslation(new Vector3f(4, 5, 2)); + + // Add a character control to the node so we can add other things and + // control the model rotation + physicsCharacter = new BetterCharacterControl(0.3f, 2.5f, 8f); + physicsCharacter.setDebugTools(new DebugTools(assetManager)); + characterNode.addControl(physicsCharacter); + getPhysicsSpace().add(physicsCharacter); + + // Load model, attach to character node + Node model = (Node) assetManager.loadModel("Models/Jaime/Jaime.j3o"); + model.setLocalScale(1.50f); + characterNode.attachChild(model); + + // Add character node to the rootNode + rootNode.attachChild(characterNode); + + // Set forward camera node that follows the character, only used when + // view is "locked" + camNode = new CameraNode("CamNode", cam); + camNode.setControlDir(ControlDirection.SpatialToCamera); + camNode.setLocalTranslation(new Vector3f(0, 2, -6)); + Quaternion quat = new Quaternion(); + // These coordinates are local, the camNode is attached to the character node! + quat.lookAt(Vector3f.UNIT_Z, Vector3f.UNIT_Y); + camNode.setLocalRotation(quat); + characterNode.attachChild(camNode); + // Disable by default, can be enabled via keyboard shortcut + camNode.setEnabled(false); + } + + @Override + public void simpleUpdate(float tpf) { + // Apply planet gravity to character if close enough (see below) + checkPlanetGravity(); + + // Get current forward and left vectors of model by using its rotation + // to rotate the unit vectors + Vector3f modelForwardDir = characterNode.getWorldRotation().mult(Vector3f.UNIT_Z); + Vector3f modelLeftDir = characterNode.getWorldRotation().mult(Vector3f.UNIT_X); + + // WalkDirection is global! + // You *can* make your character fly with this. + walkDirection.set(0, 0, 0); + if (leftStrafe) { + walkDirection.addLocal(modelLeftDir.mult(3)); + } else if (rightStrafe) { + walkDirection.addLocal(modelLeftDir.negate().multLocal(3)); + } + if (forward) { + walkDirection.addLocal(modelForwardDir.mult(3)); + } else if (backward) { + walkDirection.addLocal(modelForwardDir.negate().multLocal(3)); + } + physicsCharacter.setWalkDirection(walkDirection); + + // ViewDirection is local to characters physics system! + // The final world rotation depends on the gravity and on the state of + // setApplyPhysicsLocal() + if (leftRotate) { + Quaternion rotateL = new Quaternion().fromAngleAxis(FastMath.PI * tpf, Vector3f.UNIT_Y); + rotateL.multLocal(viewDirection); + } else if (rightRotate) { + Quaternion rotateR = new Quaternion().fromAngleAxis(-FastMath.PI * tpf, Vector3f.UNIT_Y); + rotateR.multLocal(viewDirection); + } + physicsCharacter.setViewDirection(viewDirection); + fpsText.setText("Touch da ground = " + physicsCharacter.isOnGround()); + if (!lockView) { + cam.lookAt(characterNode.getWorldTranslation().add(new Vector3f(0, 2, 0)), Vector3f.UNIT_Y); + } + } + + private void setupPlanet() { + Material material = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + material.setTexture("ColorMap", assetManager.loadTexture("Interface/Logo/Monkey.jpg")); + //immovable sphere with mesh collision shape + Sphere sphere = new Sphere(64, 64, 20); + planet = new Geometry("Sphere", sphere); + planet.setMaterial(material); + planet.setLocalTranslation(30, -15, 30); + planet.addControl(new RigidBodyControl(new MeshCollisionShape(sphere), 0)); + rootNode.attachChild(planet); + getPhysicsSpace().add(planet); + } + + private void checkPlanetGravity() { + Vector3f planetDist = planet.getWorldTranslation().subtract(characterNode.getWorldTranslation()); + if (planetDist.length() < 24) { + physicsCharacter.setGravity(planetDist.normalizeLocal().multLocal(9.81f)); + } else { + physicsCharacter.setGravity(normalGravity); + } + } + + private PhysicsSpace getPhysicsSpace() { + return bulletAppState.getPhysicsSpace(); + } + + public void onAction(String binding, boolean value, float tpf) { + if (binding.equals("Strafe Left")) { + if (value) { + leftStrafe = true; + } else { + leftStrafe = false; + } + } else if (binding.equals("Strafe Right")) { + if (value) { + rightStrafe = true; + } else { + rightStrafe = false; + } + } else if (binding.equals("Rotate Left")) { + if (value) { + leftRotate = true; + } else { + leftRotate = false; + } + } else if (binding.equals("Rotate Right")) { + if (value) { + rightRotate = true; + } else { + rightRotate = false; + } + } else if (binding.equals("Walk Forward")) { + if (value) { + forward = true; + } else { + forward = false; + } + } else if (binding.equals("Walk Backward")) { + if (value) { + backward = true; + } else { + backward = false; + } + } else if (binding.equals("Jump")) { + physicsCharacter.jump(); + } else if (binding.equals("Duck")) { + if (value) { + physicsCharacter.setDucked(true); + } else { + physicsCharacter.setDucked(false); + } + } else if (binding.equals("Lock View")) { + if (value && lockView) { + lockView = false; + } else if (value && !lockView) { + lockView = true; + } + flyCam.setEnabled(!lockView); + camNode.setEnabled(lockView); + } + } + private boolean lockView = false; + + private void setupKeys() { + inputManager.addMapping("Strafe Left", + new KeyTrigger(KeyInput.KEY_U), + new KeyTrigger(KeyInput.KEY_Z)); + inputManager.addMapping("Strafe Right", + new KeyTrigger(KeyInput.KEY_O), + new KeyTrigger(KeyInput.KEY_X)); + inputManager.addMapping("Rotate Left", + new KeyTrigger(KeyInput.KEY_J), + new KeyTrigger(KeyInput.KEY_LEFT)); + inputManager.addMapping("Rotate Right", + new KeyTrigger(KeyInput.KEY_L), + new KeyTrigger(KeyInput.KEY_RIGHT)); + inputManager.addMapping("Walk Forward", + new KeyTrigger(KeyInput.KEY_I), + new KeyTrigger(KeyInput.KEY_UP)); + inputManager.addMapping("Walk Backward", + new KeyTrigger(KeyInput.KEY_K), + new KeyTrigger(KeyInput.KEY_DOWN)); + inputManager.addMapping("Jump", + new KeyTrigger(KeyInput.KEY_F), + new KeyTrigger(KeyInput.KEY_SPACE)); + inputManager.addMapping("Duck", + new KeyTrigger(KeyInput.KEY_G), + new KeyTrigger(KeyInput.KEY_LSHIFT), + new KeyTrigger(KeyInput.KEY_RSHIFT)); + inputManager.addMapping("Lock View", + new KeyTrigger(KeyInput.KEY_RETURN)); + inputManager.addListener(this, "Strafe Left", "Strafe Right"); + inputManager.addListener(this, "Rotate Left", "Rotate Right"); + inputManager.addListener(this, "Walk Forward", "Walk Backward"); + inputManager.addListener(this, "Jump", "Duck", "Lock View"); + } + + @Override + public void simpleRender(RenderManager rm) { + } +}