Ok, here is a first version of the HovercraftCharacterControl. This is an ugly code and a lot of things need to be done. For example, I don’t know if it’s framerate independent or not (I don’t exactly know what make a physicscharacter framerate dependent).
This is a (poor) implementation of what I said before, with a hovercraft part and a “magnet” part. Every physics tick I detect collisions under the character control (with a ray for now, but I’ll try to update this to use a real collision detection) and if the ground is too close I push it(the character control) up and if the ground is a bit far (not too far) I pull the character control down.
This would be easier to implement if there was some “magnet” and “reactor” component in bullet, and I think that it could be a nice thing to add (to have vehicles like in the game “killer loop” for example
). It would also be a way to avoid vehicles flip (just magnet the wheel or even the chassis).
My main problem was to find a function that goes from [0, 1] to [0, 1] with a very harsh changement, like x^200. Why ? Because I wanted a fast recovery for almost every “recovery height” but still a smooth and small recovery for the end. To be clear, I detect the collision and according to the “hit fraction” (the distance from the rigidbody where the collision occurred, 0 being near and 1 being far) I apply a recovery with a very strong value for [1, 0.0001] and a really weaker recovery for ]0.0001, 0]. This is the idea : fast most of time, smooth at the end.
However, I didn’t find a good function like that and I made some terrible code.
I tested this with a stepheight of 1.1 (this is a very high value, it means that you can walk and you won’t care for obstacles with a height of 1m10 cm or less). It works, even if the vertical recovery is not high enough (you cannot walk straight forward without losing speed).
I think that the final implementation should take care of the walk direction, so it will be able to apply a vertical recovery speed that allow the character control to walk and “fly” above the objects. If the character is slow, the recovery will be slow etc.
I had some problems with the jump and the “magnet” and I made a quick fix that will work in some case. I need to rework on this too.
Also, I think that use a Vector3f for the walkDirection is not a good idea.
First, we are here talking about walk, not fly. This should be a Vectof2f, planar in 3d to the plan normal with the gravity. Second, I think that there should be 2 different things, the walk speed and the walk direction. Both should be a float but the direction should be an angle. I didn’t implement this yet and before I made this I would like to know what you think about it.
Also, I need to trigger the collision when I am “hovering” over the ground. I don’t know how to do this.
End, the last but not the least, i need to add a “slope” checking, meaning that i’ll keep only collisions that occured in a certain angle under the charactercontrol. I think i’ll do an other picture to explain that. And i’ll end this by doing an article to sumerize all of this.
To instanciate a hovercharactercontrol you need to do something like that:
[java]
new HovercraftCharacterControl(0.5f, 1.8f, 79, 1.1f);
[/java]
Where 0.5f if the radius of the character control, 1.8f its height, 79 its weight (mass … I know, physicly speaking it’s not true ^^ ) and 1.1f its stepHeight – even if you should lower this value.
I didn’t try it for normal environments as the only thing I have is a cube world.
[java]
/**
*
-
@author Bubuche
*/
public class HovercraftCharacterControl
extends AbstractPhysicsControl
implements PhysicsTickListener
{
protected final Vector3f scale = new Vector3f(1, 1, 1);
protected final Vector3f walkDirection = new Vector3f(1, 1, 1);
protected final Vector3f localUp = new Vector3f(0, 1, 0);
protected final Vector3f localForward = new Vector3f(0, 0, 1);
protected final Quaternion localForwardRotation = new Quaternion(Quaternion.DIRECTION_Z);
protected final Vector3f rotatedViewDirection = new Vector3f(0, 0, 1);
protected final Quaternion rotation = new Quaternion(Quaternion.DIRECTION_Z);
protected final Vector3f viewDirection = new Vector3f(0, 0, 1);
protected static final Logger logger = Logger.getLogger(HovercraftCharacterControl.class.getName());
protected float walkAngle;
private float stepHeight;
private float jumpHeight;
private List<PhysicsRayTestResult> bottomNear;
private PhysicsRigidBody rigidBody;
private float radius;
private float height;
private float mass;
private boolean onGround;
private boolean jump;
private boolean isInJump;
public HovercraftCharacterControl(float radius, float height, float mass, float stepHeight)
{
this.stepHeight = stepHeight;
if (stepHeight > height)
{
throw new IllegalArgumentException(“stepHeight(=” + stepHeight + “) > height(=” + height + “)”);
}
this.stepHeight = stepHeight;
this.jumpHeight = 1;
this.bottomNear = new ArrayList<>();
this.radius = radius;
this.height = height;
this.mass = mass;
rigidBody = new PhysicsRigidBody(getShape(), mass);
rigidBody.setAngularFactor(0);
rigidBody.setAngularDamping(Float.POSITIVE_INFINITY);
rigidBody.setFriction(0);
}
public void jump()
{
this.jump = true;
this.isInJump = true;
}
public void setJumpHeight(float jumpHeight)
{
this.jumpHeight = jumpHeight;
}
public float jumpHeight()
{
return this.jumpHeight;
}
public boolean isOnGround()
{
return this.onGround;
}
public void setWalkDirection(Vector3f walkDirection)
{
this.walkDirection.set(walkDirection);
}
public void setGravity(Vector3f gravity)
{
rigidBody.setGravity(gravity);
localUp.set(gravity).normalizeLocal().negateLocal();
updateLocalCoordinateSystem();
}
/**
- 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.
-
- MADE BY NORMEN !
-
-
@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;
}
/**
- Updates the local coordinate system from the localForward and localUp
- vectors, adapts localForward, sets localForwardRotation quaternion to local
- z-forward rotation.
-
- MADE BY NORMEN !
-
*/
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.
-
- MADE BY NORMEN !
-
*/
protected void updateLocalViewDirection()
{
//update local rotation quaternion to use for view rotation
localForwardRotation.multLocal(rotatedViewDirection.set(viewDirection));
calculateNewForward(rotation, rotatedViewDirection, localUp);
}
/**
- Gets the scaled height.
-
-
@return
*/
protected float getFinalHeight()
{
return height * scale.getY();
}
/**
- Gets the scaled radius.
-
-
@return
*/
protected float getFinalRadius()
{
return radius * scale.getZ();
}
protected void applyVerticalRecovery(float tpf)
{
float cylinderHalfHeight = (height - stepHeight) / 2;
TempVars vars;
vars = TempVars.get();
final Vector3f position = vars.vect1;
final Vector3f limit = vars.vect2;
final Vector3f upPush = vars.vect3;
final Vector3f gravity = vars.vect5;
final Vector3f cylinderBottom = vars.vect6;
rigidBody.getPhysicsLocation(position);
rigidBody.getGravity(gravity);
float gravityLength = gravity.length();
gravity.normalizeLocal();
cylinderBottom.set(gravity);
cylinderBottom.multLocal(cylinderHalfHeight);
//position.addLocal(cylinderBottom);
/* check if on ground */
limit.set(gravity);
limit.multLocal(stepHeight);
limit.addLocal(position);
bottomNear.clear();
space.rayTest(position, limit, bottomNear);
onGround = !bottomNear.isEmpty();
if (!onGround)
{
vars.release();
return;
}
PhysicsRayTestResult result = bottomNear.get(bottomNear.size() - 1);
float fraction = result.getHitFraction();
float usedFraction = fraction;
rigidBody.getGravity(upPush);
upPush.negateLocal();
upPush.normalizeLocal();
fraction = (float) Math.pow(fraction, 100);
//fraction = (float) Math.pow(2, func * func * func);
//fraction = fraction - 1;
if ( fraction < 0.0001f )
{
fraction = 0.0001f;
}
upPush.multLocal(gravityLength+ (1/fraction) );
gravity.negateLocal();
rigidBody.applyCentralForce(upPush);
vars.release();
}
protected void applyVerticalRecoveryAndMagnet(float tpf)
{
float cylinderHalfHeight = (height - stepHeight) / 2;
TempVars vars;
vars = TempVars.get();
final Vector3f position = vars.vect1;
final Vector3f limit = vars.vect2;
final Vector3f upPush = vars.vect3;
final Vector3f gravity = vars.vect5;
final Vector3f cylinderBottom = vars.vect6;
rigidBody.getPhysicsLocation(position);
rigidBody.getGravity(gravity);
float gravityLength = gravity.length();
gravity.normalizeLocal();
cylinderBottom.set(gravity);
cylinderBottom.multLocal(cylinderHalfHeight);
//position.addLocal(cylinderBottom);
/* check if on ground */
limit.set(gravity);
limit.multLocal(stepHeight * 2);
limit.addLocal(position);
bottomNear.clear();
space.rayTest(position, limit, bottomNear);
PhysicsRayTestResult resultNear = null;
PhysicsRayTestResult resultFar = null;
for ( PhysicsRayTestResult r : bottomNear )
{
if ( r.getHitFraction() <= 0.5f )
{
if ( resultNear != null )
{
if ( resultNear.getHitFraction() < r.getHitFraction() )
{
continue;
}
}
resultNear = r;
}
else
{
if ( resultFar != null )
{
if ( resultFar.getHitFraction() > r.getHitFraction() )
{
continue;
}
}
resultFar = r;
}
}
boolean useMagnet;
onGround = (resultNear != null);
useMagnet = (resultFar != null);
if (!onGround && !useMagnet)
{
vars.release();
return;
}
rigidBody.getGravity(upPush);
upPush.negateLocal();
upPush.normalizeLocal();
if ( onGround )
{
isInJump = false;
float fraction = resultNear.getHitFraction() * 2;
fraction = (float) Math.pow(fraction, 100);
//fraction = (float) Math.pow(2, func * func * func);
//fraction = fraction - 1;
if ( fraction < 0.0001f )
{
fraction = 0.0001f;
}
upPush.multLocal(gravityLength+ (1/fraction) );
}
else if ( ! isInJump )
{
float fraction = (resultFar.getHitFraction() - 0.5f)* 2;
fraction = (float) Math.pow(fraction, 200);
if ( fraction < 0.00001f )
{
fraction = 0.00001f;
}
upPush.multLocal(- (1/fraction) );
}
rigidBody.applyCentralForce(upPush);
vars.release();
}
@Override
public void update(float tpf)
{
float cylinderHalfHeight = cylinderHalfHeight();
TempVars vars;
vars = TempVars.get();
Vector3f ballLocation = vars.vect1;
Vector3f localizedLocation = vars.vect2;
Vector3f shift = vars.vect3;
rigidBody.getPhysicsLocation(ballLocation);
localizedLocation.set(ballLocation);
rigidBody.getGravity(shift);
shift.normalizeLocal();
shift.multLocal(cylinderHalfHeight + stepHeight);
localizedLocation.addLocal(shift);
//spatial.localToWorld(ballLocation, localizedLocation);
//spatial.setLocalTranslation(localizedLocation);
applyPhysicsTransform(localizedLocation, rotation);
vars.release();
}
private float cylinderHalfHeight()
{
return (height - stepHeight) / 2;
}
@Override
public void prePhysicsTick(PhysicsSpace space, float tpf)
{
applyVerticalRecoveryAndMagnet(tpf);
applyWalkDirection();
if ( jump && onGround )
{
jump = false;
isInJump = true;
applyJump(tpf);
}
}
private void applyWalkDirection()
{
if ( walkDirection.equals(Vector3f.ZERO) )
{
if ( onGround )
{
rigidBody.setLinearVelocity(Vector3f.ZERO);
}
return;
}
TempVars vars;
vars = TempVars.get();
float angle;
Vector3f normalizedWalkDirection = vars.vect1;
Vector3f normalizedGravity = vars.vect2;
Vector3f precVerticalVelocity = vars.vect3;
Vector3f finalVelocity = vars.vect4;
normalizedWalkDirection.set(walkDirection);
normalizedWalkDirection.normalizeLocal();
getGravity(normalizedGravity);
normalizedGravity.normalizeLocal();
angle = normalizedWalkDirection.angleBetween(normalizedGravity);
if (angle < 0)
{
angle = -angle;
}
if (angle < FastMath.FLT_EPSILON)
{
vars.release();
return;
}
Vector3f direction = vars.vect3;
Vector3f plan = vars.vect4;
normalizedGravity.cross(normalizedWalkDirection, direction);
direction.cross(normalizedGravity, plan);
normalizedWalkDirection.projectLocal(plan);
normalizedWalkDirection.normalizeLocal();
finalVelocity.set(normalizedWalkDirection);
finalVelocity.multLocal(15);
if ( ! onGround )
{
/* keep the old vertical velocity */
rigidBody.getLinearVelocity(precVerticalVelocity);
precVerticalVelocity.projectLocal(normalizedGravity);
finalVelocity.addLocal(precVerticalVelocity);
}
rigidBody.setLinearVelocity(finalVelocity);
vars.release();
}
private void applyJump(float tpf)
{
TempVars vars;
vars = TempVars.get();
Vector3f gravity = vars.vect1;
Vector3f v = vars.vect2;
Vector3f precVelocity = vars.vect3;
rigidBody.getGravity(gravity);
float l;
l = gravity.length();
float g = l / mass;
v.set(gravity);
v.normalizeLocal();
float speedLength = jumpHeight * g * 2;
speedLength = (float) Math.sqrt(speedLength);
v.multLocal(-speedLength);
rigidBody.getLinearVelocity(precVelocity);
v.addLocal(precVelocity);
rigidBody.setLinearVelocity(v);
vars.release();
}
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();
}
@Override
protected void createSpatialData(Spatial spat)
{
rigidBody.setUserObject(spatial);
}
@Override
protected void removeSpatialData(Spatial spat)
{
rigidBody.setUserObject(null);
}
@Override
protected void setPhysicsLocation(Vector3f vec)
{
rigidBody.setPhysicsLocation(vec);
}
public void warp(Vector3f vec)
{
setPhysicsLocation(vec);
}
@Override
protected void setPhysicsRotation(Quaternion quat)
{
rotation.set(quat);
rotation.multLocal(rotatedViewDirection.set(viewDirection));
updateLocalViewDirection();
}
@Override
protected void addPhysics(PhysicsSpace space)
{
space.getGravity(localUp).normalizeLocal().negateLocal();
updateLocalCoordinateSystem();
space.addCollisionObject(rigidBody);
space.addTickListener(this);
}
@Override
protected void removePhysics(PhysicsSpace space)
{
space.removeCollisionObject(rigidBody);
space.removeTickListener(this);
}
@Override
public Control cloneForSpatial(Spatial spatial)
{
HovercraftCharacterControl control = new HovercraftCharacterControl(radius, height, mass, stepHeight);
return control;
}
@Override
public void physicsTick(PhysicsSpace space, float tpf)
{
}
public void getGravity(Vector3f gravity)
{
rigidBody.getGravity(gravity);
}
}
[/java]