OSDN Git Service

- add first version of BetterCharacterControl, WIP
authornormen667 <normen667@75d07b2b-3a1a-0410-a2c5-0572b91ccdca>
Fri, 8 Feb 2013 03:09:35 +0000 (03:09 +0000)
committernormen667 <normen667@75d07b2b-3a1a-0410-a2c5-0572b91ccdca>
Fri, 8 Feb 2013 03:09:35 +0000 (03:09 +0000)
TODO: damping of local x/z plane physics forces

git-svn-id: http://jmonkeyengine.googlecode.com/svn/trunk@10363 75d07b2b-3a1a-0410-a2c5-0572b91ccdca

engine/src/bullet-common/com/jme3/bullet/control/BetterCharacterControl.java [new file with mode: 0644]
engine/src/test/jme3test/bullet/TestBetterCharacter.java [new file with mode: 0644]

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 (file)
index 0000000..e6abf18
--- /dev/null
@@ -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<PhysicsRayTestResult> 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<PhysicsRayTestResult> 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 (file)
index 0000000..b737909
--- /dev/null
@@ -0,0 +1,287 @@
+/*\r
+ * Copyright (c) 2009-2012 jMonkeyEngine All rights reserved. <p/>\r
+ * Redistribution and use in source and binary forms, with or without\r
+ * modification, are permitted provided that the following conditions are met:\r
+ * \r
+ * * Redistributions of source code must retain the above copyright notice,\r
+ * this list of conditions and the following disclaimer. <p/> * Redistributions\r
+ * in binary form must reproduce the above copyright notice, this list of\r
+ * conditions and the following disclaimer in the documentation and/or other\r
+ * materials provided with the distribution. <p/> * Neither the name of\r
+ * 'jMonkeyEngine' nor the names of its contributors may be used to endorse or\r
+ * promote products derived from this software without specific prior written\r
+ * permission. <p/> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND\r
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT\r
+ * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A\r
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR\r
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\r
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\r
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;\r
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,\r
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR\r
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF\r
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r
+ */\r
+package jme3test.bullet;\r
+\r
+import com.jme3.app.SimpleApplication;\r
+import com.jme3.bullet.BulletAppState;\r
+import com.jme3.bullet.PhysicsSpace;\r
+import com.jme3.bullet.collision.shapes.MeshCollisionShape;\r
+import com.jme3.bullet.control.BetterCharacterControl;\r
+import com.jme3.bullet.control.RigidBodyControl;\r
+import com.jme3.bullet.debug.DebugTools;\r
+import com.jme3.input.KeyInput;\r
+import com.jme3.input.controls.ActionListener;\r
+import com.jme3.input.controls.KeyTrigger;\r
+import com.jme3.material.Material;\r
+import com.jme3.math.FastMath;\r
+import com.jme3.math.Quaternion;\r
+import com.jme3.math.Vector3f;\r
+import com.jme3.renderer.RenderManager;\r
+import com.jme3.scene.CameraNode;\r
+import com.jme3.scene.Geometry;\r
+import com.jme3.scene.Node;\r
+import com.jme3.scene.control.CameraControl.ControlDirection;\r
+import com.jme3.scene.shape.Sphere;\r
+import com.jme3.system.AppSettings;\r
+\r
+/**\r
+ * A walking physical character followed by a 3rd person camera. (No animation.)\r
+ *\r
+ * @author normenhansen, zathras\r
+ */\r
+public class TestBetterCharacter extends SimpleApplication implements ActionListener {\r
+\r
+    private BulletAppState bulletAppState;\r
+    private BetterCharacterControl physicsCharacter;\r
+    private Node characterNode;\r
+    private CameraNode camNode;\r
+    boolean rotate = false;\r
+    private Vector3f walkDirection = new Vector3f(0, 0, 0);\r
+    private Vector3f viewDirection = new Vector3f(0, 0, 1);\r
+    boolean leftStrafe = false, rightStrafe = false, forward = false, backward = false,\r
+            leftRotate = false, rightRotate = false;\r
+    private Vector3f normalGravity = new Vector3f(0, -9.81f, 0);\r
+    private Geometry planet;\r
+\r
+    public static void main(String[] args) {\r
+        TestBetterCharacter app = new TestBetterCharacter();\r
+        AppSettings settings = new AppSettings(true);\r
+        settings.setRenderer(AppSettings.LWJGL_OPENGL2);\r
+        settings.setAudioRenderer(AppSettings.LWJGL_OPENAL);\r
+        app.setSettings(settings);\r
+        app.start();\r
+    }\r
+\r
+    @Override\r
+    public void simpleInitApp() {\r
+        //setup keyboard mapping\r
+        setupKeys();\r
+\r
+        // activate physics\r
+        bulletAppState = new BulletAppState();\r
+        stateManager.attach(bulletAppState);\r
+        bulletAppState.setDebugEnabled(true);\r
+\r
+        // init a physics test scene\r
+        PhysicsTestHelper.createPhysicsTestWorldSoccer(rootNode, assetManager, bulletAppState.getPhysicsSpace());\r
+        PhysicsTestHelper.createBallShooter(this, rootNode, bulletAppState.getPhysicsSpace());\r
+        setupPlanet();\r
+\r
+        // Create a node for the character model\r
+        characterNode = new Node("character node");\r
+        characterNode.setLocalTranslation(new Vector3f(4, 5, 2));\r
+\r
+        // Add a character control to the node so we can add other things and\r
+        // control the model rotation\r
+        physicsCharacter = new BetterCharacterControl(0.3f, 2.5f, 8f);\r
+        physicsCharacter.setDebugTools(new DebugTools(assetManager));\r
+        characterNode.addControl(physicsCharacter);\r
+        getPhysicsSpace().add(physicsCharacter);\r
+\r
+        // Load model, attach to character node\r
+        Node model = (Node) assetManager.loadModel("Models/Jaime/Jaime.j3o");\r
+        model.setLocalScale(1.50f);\r
+        characterNode.attachChild(model);\r
+\r
+        // Add character node to the rootNode\r
+        rootNode.attachChild(characterNode);\r
+\r
+        // Set forward camera node that follows the character, only used when\r
+        // view is "locked"\r
+        camNode = new CameraNode("CamNode", cam);\r
+        camNode.setControlDir(ControlDirection.SpatialToCamera);\r
+        camNode.setLocalTranslation(new Vector3f(0, 2, -6));\r
+        Quaternion quat = new Quaternion();\r
+        // These coordinates are local, the camNode is attached to the character node!\r
+        quat.lookAt(Vector3f.UNIT_Z, Vector3f.UNIT_Y);\r
+        camNode.setLocalRotation(quat);\r
+        characterNode.attachChild(camNode);\r
+        // Disable by default, can be enabled via keyboard shortcut\r
+        camNode.setEnabled(false);\r
+    }\r
+\r
+    @Override\r
+    public void simpleUpdate(float tpf) {\r
+        // Apply planet gravity to character if close enough (see below)\r
+        checkPlanetGravity();\r
+\r
+        // Get current forward and left vectors of model by using its rotation\r
+        // to rotate the unit vectors\r
+        Vector3f modelForwardDir = characterNode.getWorldRotation().mult(Vector3f.UNIT_Z);\r
+        Vector3f modelLeftDir = characterNode.getWorldRotation().mult(Vector3f.UNIT_X);\r
+\r
+        // WalkDirection is global!\r
+        // You *can* make your character fly with this.\r
+        walkDirection.set(0, 0, 0);\r
+        if (leftStrafe) {\r
+            walkDirection.addLocal(modelLeftDir.mult(3));\r
+        } else if (rightStrafe) {\r
+            walkDirection.addLocal(modelLeftDir.negate().multLocal(3));\r
+        }\r
+        if (forward) {\r
+            walkDirection.addLocal(modelForwardDir.mult(3));\r
+        } else if (backward) {\r
+            walkDirection.addLocal(modelForwardDir.negate().multLocal(3));\r
+        }\r
+        physicsCharacter.setWalkDirection(walkDirection);\r
+\r
+        // ViewDirection is local to characters physics system!\r
+        // The final world rotation depends on the gravity and on the state of\r
+        // setApplyPhysicsLocal()\r
+        if (leftRotate) {\r
+            Quaternion rotateL = new Quaternion().fromAngleAxis(FastMath.PI * tpf, Vector3f.UNIT_Y);\r
+            rotateL.multLocal(viewDirection);\r
+        } else if (rightRotate) {\r
+            Quaternion rotateR = new Quaternion().fromAngleAxis(-FastMath.PI * tpf, Vector3f.UNIT_Y);\r
+            rotateR.multLocal(viewDirection);\r
+        }\r
+        physicsCharacter.setViewDirection(viewDirection);\r
+        fpsText.setText("Touch da ground = " + physicsCharacter.isOnGround());\r
+        if (!lockView) {\r
+            cam.lookAt(characterNode.getWorldTranslation().add(new Vector3f(0, 2, 0)), Vector3f.UNIT_Y);\r
+        }\r
+    }\r
+\r
+    private void setupPlanet() {\r
+        Material material = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");\r
+        material.setTexture("ColorMap", assetManager.loadTexture("Interface/Logo/Monkey.jpg"));\r
+        //immovable sphere with mesh collision shape\r
+        Sphere sphere = new Sphere(64, 64, 20);\r
+        planet = new Geometry("Sphere", sphere);\r
+        planet.setMaterial(material);\r
+        planet.setLocalTranslation(30, -15, 30);\r
+        planet.addControl(new RigidBodyControl(new MeshCollisionShape(sphere), 0));\r
+        rootNode.attachChild(planet);\r
+        getPhysicsSpace().add(planet);\r
+    }\r
+\r
+    private void checkPlanetGravity() {\r
+        Vector3f planetDist = planet.getWorldTranslation().subtract(characterNode.getWorldTranslation());\r
+        if (planetDist.length() < 24) {\r
+            physicsCharacter.setGravity(planetDist.normalizeLocal().multLocal(9.81f));\r
+        } else {\r
+            physicsCharacter.setGravity(normalGravity);\r
+        }\r
+    }\r
+\r
+    private PhysicsSpace getPhysicsSpace() {\r
+        return bulletAppState.getPhysicsSpace();\r
+    }\r
+\r
+    public void onAction(String binding, boolean value, float tpf) {\r
+        if (binding.equals("Strafe Left")) {\r
+            if (value) {\r
+                leftStrafe = true;\r
+            } else {\r
+                leftStrafe = false;\r
+            }\r
+        } else if (binding.equals("Strafe Right")) {\r
+            if (value) {\r
+                rightStrafe = true;\r
+            } else {\r
+                rightStrafe = false;\r
+            }\r
+        } else if (binding.equals("Rotate Left")) {\r
+            if (value) {\r
+                leftRotate = true;\r
+            } else {\r
+                leftRotate = false;\r
+            }\r
+        } else if (binding.equals("Rotate Right")) {\r
+            if (value) {\r
+                rightRotate = true;\r
+            } else {\r
+                rightRotate = false;\r
+            }\r
+        } else if (binding.equals("Walk Forward")) {\r
+            if (value) {\r
+                forward = true;\r
+            } else {\r
+                forward = false;\r
+            }\r
+        } else if (binding.equals("Walk Backward")) {\r
+            if (value) {\r
+                backward = true;\r
+            } else {\r
+                backward = false;\r
+            }\r
+        } else if (binding.equals("Jump")) {\r
+            physicsCharacter.jump();\r
+        } else if (binding.equals("Duck")) {\r
+            if (value) {\r
+                physicsCharacter.setDucked(true);\r
+            } else {\r
+                physicsCharacter.setDucked(false);\r
+            }\r
+        } else if (binding.equals("Lock View")) {\r
+            if (value && lockView) {\r
+                lockView = false;\r
+            } else if (value && !lockView) {\r
+                lockView = true;\r
+            }\r
+            flyCam.setEnabled(!lockView);\r
+            camNode.setEnabled(lockView);\r
+        }\r
+    }\r
+    private boolean lockView = false;\r
+\r
+    private void setupKeys() {\r
+        inputManager.addMapping("Strafe Left",\r
+                new KeyTrigger(KeyInput.KEY_U),\r
+                new KeyTrigger(KeyInput.KEY_Z));\r
+        inputManager.addMapping("Strafe Right",\r
+                new KeyTrigger(KeyInput.KEY_O),\r
+                new KeyTrigger(KeyInput.KEY_X));\r
+        inputManager.addMapping("Rotate Left",\r
+                new KeyTrigger(KeyInput.KEY_J),\r
+                new KeyTrigger(KeyInput.KEY_LEFT));\r
+        inputManager.addMapping("Rotate Right",\r
+                new KeyTrigger(KeyInput.KEY_L),\r
+                new KeyTrigger(KeyInput.KEY_RIGHT));\r
+        inputManager.addMapping("Walk Forward",\r
+                new KeyTrigger(KeyInput.KEY_I),\r
+                new KeyTrigger(KeyInput.KEY_UP));\r
+        inputManager.addMapping("Walk Backward",\r
+                new KeyTrigger(KeyInput.KEY_K),\r
+                new KeyTrigger(KeyInput.KEY_DOWN));\r
+        inputManager.addMapping("Jump",\r
+                new KeyTrigger(KeyInput.KEY_F),\r
+                new KeyTrigger(KeyInput.KEY_SPACE));\r
+        inputManager.addMapping("Duck",\r
+                new KeyTrigger(KeyInput.KEY_G),\r
+                new KeyTrigger(KeyInput.KEY_LSHIFT),\r
+                new KeyTrigger(KeyInput.KEY_RSHIFT));\r
+        inputManager.addMapping("Lock View",\r
+                new KeyTrigger(KeyInput.KEY_RETURN));\r
+        inputManager.addListener(this, "Strafe Left", "Strafe Right");\r
+        inputManager.addListener(this, "Rotate Left", "Rotate Right");\r
+        inputManager.addListener(this, "Walk Forward", "Walk Backward");\r
+        inputManager.addListener(this, "Jump", "Duck", "Lock View");\r
+    }\r
+\r
+    @Override\r
+    public void simpleRender(RenderManager rm) {\r
+    }\r
+}\r