Collision Handling with Sliding

All right, the next step is to provide collision handling that isn't obtrusive—in other words, the collision handling behaves in a way that the user expects. Instead of collision "sticking," you'll implement collision "sliding." For example, instead of stopping when the player hits an object, the player will slide to the side of it. Likewise, the player will slide against a wall and will be able to step over small objects on the floor. Also in this section, you'll implement some basic physics to allow objects to smoothly move up stairs, apply gravity, and allow the player to jump.

Object-to-Object Sliding

Previously, if an object-to-object collision occurred, you just resorted to moving the object to its original location. This is what created the effect of the moving object "sticking" to the static object it collided with. To fix this, you must make sure the moving object slides to the side of the static object. The logical solution is to slide the least amount required to get the two objects out of each other's way. That means the direction to slide is defined by the vector from the static object's center to the moving object's center, as in Screenshot. In this figure, when the larger object moves, it collides with the smaller object. The large object then moves away from the smaller object so that, after the slide, the two objects are next to one another but not colliding.

Screenshot Object-to-object sliding: The moving object is pushed away from the static object.

Java graphics 11fig13.gif


The amount to slide is the difference between the minimum distance and the actual distance:

float minDist = objectA.radius + objectB.radius;
float slideDistance = minDist - actualDist;


So, because the vector between the two object's centers has the length of actualDist, the formula becomes this:

float scale = slideDistance / actualDist;
vector.multiply(scale);


Or, if you know only the square of the distances, it is this:

float scale = (float)Math.sqrt(minDistSq / actualDistSq) - 1;
vector.multiply(scale);


Although that square root function is slow, you have to call it only when objects bump into one another, which isn't very often. A problem with sliding occurs when sliding against an object actually causes the object to slide into a wall or another object. In this case, the moving object can just revert to its previous location so it still appears to "stick"—the player will have to change direction. Object-to-object sliding is easy enough. All the sliding code is kept in the CollisionDetectionWithSliding class, which is a subclass of CollisionDetection. The method is shown in Listing 11.7.

Listing 11.7 Object-to-Object Sliding (CollisionDetectionWithSliding.java)
/**
 Handles an object collision. Object A is the moving
 object, and Object B is the object that Object A collided
 with. Object A slides around or steps on top of
 Object B if possible.
*/
protected boolean handleObjectCollision(GameObject objectA,
 GameObject objectB, float distSq, float minDistSq,
 Vector3D oldLocation)
{
 objectA.notifyObjectCollision(objectB);
 if (objectA.isFlying()) {
 return true;
 }
 float stepSize = objectA.getBounds().getTopHeight() / 6;
 Vector3D velocity =
 objectA.getTransform().getVelocity();
 // step up on top of object if possible
 float objectABottom = objectA.getY() +
 objectA.getBounds().getBottomHeight();
 float objectBTop = objectB.getY() +
 objectB.getBounds().getTopHeight();
 if (objectABottom + stepSize > objectBTop &&
 objectBTop +
 objectA.getBounds().getTopHeight() <
 objectA.getCeilHeight())
 {
 objectA.getLocation().y = (objectBTop -
 objectA.getBounds().getBottomHeight());
 if (velocity.y < 0) {
 objectA.setJumping(false);
 // don't let gravity get out of control
 velocity.y = -.01f;
 }
 return false;
 }
 if (objectA.getX() != oldLocation.x ||
 objectA.getZ() != oldLocation.z)
 {
 // slide to the side
 float slideDistFactor =
 (float)Math.sqrt(minDistSq / distSq) - 1;
 scratch.setTo(objectA.getX(), 0, objectA.getZ());
 scratch.subtract(objectB.getX(), 0, objectB.getZ());
 scratch.multiply(slideDistFactor);
 objectA.getLocation().add(scratch);
 // revert location if passing through a wall
 if (super.checkWalls(objectA, oldLocation, 0) != null) {
 return true;
 }
 return false;
 }
 return true;
}


In this code, the handleObjectCollision() method is overridden. First, it checks to see whether the moving object can step on top of the static object. If not, it slides the object to the side. If the result of sliding creates a collision with a wall, the object reverts to its original location. Speaking of colliding with walls, how about implementing some object-to-wall sliding?

Object-to-Wall Sliding

Sliding along the wall might seem like a complicated task, but it really just involves some simple math. If you know the goal location (the location the object would have moved to had there been no wall), you can easily find the slide location, as shown in Screenshot.

Screenshot Object-to-wall sliding: An object slides against a wall.

Java graphics 11fig14.gif


In this figure, the gray line points in the direction of the polygon's normal. You can find the slide location if you can find the length of this line. This is a simple right-triangle problem. Consider the vector to the collision location from the goal location:

vector.setTo(actualX, 0, actualZ);
vector.subtract(goalX, 0, goalZ);


Then the length of the gray line is the dot product between this vector and the polygon's normal:

float length = vector.getDotProduct(wall.getNormal());


So, the slide location is this:

float slideX = goalX + length * wall.getNormal().x;
float slideZ = goalZ + length * wall.getNormal().z;


That's the basic idea. Along with some extra checks, the entire code for object-to-wall sliding is in Listing 11.8.

Listing 11.8 Object-to-Wall Sliding (CollisionDetectionWithSliding.java)
private Vector3D scratch = new Vector3D();
private Vector3D originalLocation = new Vector3D();
...
/**
 Checks for a game object collision with the walls of the
 BSP tree. Returns the first wall collided with, or null if
 there was no collision. If there is a collision, the
 object slides along the wall and again checks for a
 collision. If a collision occurs on the slide, the object
 reverts back to its old location.
*/
public BSPPolygon checkWalls(GameObject object,
 Vector3D oldLocation, long elapsedTime)
{
 float goalX = object.getX();
 float goalZ = object.getZ();
 BSPPolygon wall = super.checkWalls(object,
 oldLocation, elapsedTime);
 // if collision found and object didn't stop itself
 if (wall != null && object.getTransform().isMoving()) {
 float actualX = object.getX();
 float actualZ = object.getZ();
 // dot product between wall's normal and line to goal
 scratch.setTo(actualX, 0, actualZ);
 scratch.subtract(goalX, 0, goalZ);
 float length = scratch.getDotProduct(wall.getNormal());
 float slideX = goalX + length * wall.getNormal().x;
 float slideZ = goalZ + length * wall.getNormal().z;
 object.getLocation().setTo(
 slideX, object.getY(), slideZ);
 originalLocation.setTo(oldLocation);
 oldLocation.setTo(actualX, oldLocation.y, actualZ);
 // use a smaller radius for sliding
 PolygonGroupBounds bounds = object.getBounds();
 float originalRadius = bounds.getRadius();
 bounds.setRadius(originalRadius-1);
 // check for collision with slide position
 BSPPolygon wall2 = super.checkWalls(object,
 oldLocation, elapsedTime);
 // restore changed parameters
 oldLocation.setTo(originalLocation);
 bounds.setRadius(originalRadius);
 if (wall2 != null) {
 object.getLocation().setTo(
 actualX, object.getY(), actualZ);
 return wall2;
 }
 }
 return wall;
}


In this code, the checkWalls() method of CollisionDetection is overridden. First the slide is applied; then it checks whether any wall collisions occurred after the slide. If so, the object is moved back to the collision location. You can always disable object-to-wall sliding. Projectiles, for example, override the notify methods so that projectiles stop when they hit a wall, floor, or ceiling:

public void notifyWallCollision() {
 transform.getVelocity().setTo(0,0,0);
}
public void notifyFloorCollision() {
 transform.getVelocity().setTo(0,0,0);
}
public void notifyCeilingCollision() {
 transform.getVelocity().setTo(0,0,0);
}


This creates the effect of a projectile "sticking" to a wall or other object, so you can emulate the previous collision-handling technique. Now you've got sliding for objects and walls. Next up: sliding up stairs (and a little gravity).

Gravity and Sliding Up Stairs (Object-to-Floor Sliding)

Another common sliding effect is to allow the player and other objects to slide smoothly up stairs. Otherwise, as in the first collision-detection demo, the player seems to jitter when moving up stairs because the y location of the object instantly changes to the higher stairstep. Also, when you move from a higher platform to a lower one, you instantly drop to the lower level instead of gradually dropping because of gravity. Gravity will work the same as in , by accelerating the object's downward velocity as time progresses. If the object's y location is higher than the floor's y location, gravity can be applied to the object. The opposite is true for sliding up stairs: If the object's y location is lower than the floor's y location, a scoot-up acceleration can be applied to the object. The physics used for the game is summed up in the Physics class in Listing 11.9. This is just a start, and you'll add more to it later.

Listing 11.9 Gravity and Scooting (Physics.java)
/**
 Default gravity in units per millisecond squared
*/
public static final float DEFAULT_GRAVITY_ACCEL = -.002f;
/**
 Default scoot-up (acceleration traveling up stairs)
 in units per millisecond squared.
*/
public static final float DEFAULT_SCOOT_ACCEL = .006f;
private float gravityAccel = DEFAULT_GRAVITY_ACCEL;
private float scootAccel = DEFAULT_SCOOT_ACCEL;
private Vector3D velocity = new Vector3D();
/**
 Applies gravity to the specified GameObject according
 to the amount of time that has passed.
*/
public void applyGravity(GameObject object, long elapsedTime) {
 velocity.setTo(0, gravityAccel * elapsedTime, 0);
 object.getTransform().addVelocity(velocity);
}
/**
 Applies the scoot-up acceleration to the specified
 GameObject according to the amount of time that has passed.
*/
public void scootUp(GameObject object, long elapsedTime) {
 velocity.setTo(0, scootAccel * elapsedTime, 0);
 object.getTransform().addVelocity(velocity);
}


The more complicated step is knowing when to apply either gravity or the scoot-up acceleration for various situations. For example, gravity isn't applied to a flying object. Also, you want to stop a scoot-up acceleration if the player is above the ground but not if the object is jumping (we'll get to jumping in a minute). All the various situations are covered in the code in Listing 11.10.

Listing 11.10 Check Floor and Ceiling (CollisionDetectionWithSliding.java)
/**
 Checks for object collisions with the floor and ceiling.
 Uses object.getFloorHeight() and object.getCeilHeight()
 for the floor and ceiling values.
 Applies gravity if the object is above the floor,
 and scoots the object up if the player is below the floor
 (for smooth movement up stairs).
*/
protected void checkFloorAndCeiling(GameObject object,
 long elapsedTime)
{
 float floorHeight = object.getFloorHeight();
 float ceilHeight = object.getCeilHeight();
 float bottomHeight = object.getBounds().getBottomHeight();
 float topHeight = object.getBounds().getTopHeight();
 Vector3D v = object.getTransform().getVelocity();
 Physics physics = Physics.getInstance();
 // check if on floor
 if (object.getY() + bottomHeight == floorHeight) {
 if (v.y < 0) {
 v.y = 0;
 }
 }
 // check if below floor
 else if (object.getY() + bottomHeight < floorHeight) {
 if (!object.isFlying()) {
 // if falling
 if (v.y < 0) {
 object.notifyFloorCollision();
 v.y = 0;
 object.getLocation().y =
 floorHeight - bottomHeight;
 }
 else if (!object.isJumping()) {
 physics.scootUp(object, elapsedTime);
 }
 }
 else {
 object.notifyFloorCollision();
 v.y = 0;
 object.getLocation().y =
 floorHeight - bottomHeight;
 }
 }
 // check if hitting ceiling
 else if (object.getY() + topHeight > ceilHeight) {
 object.notifyCeilingCollision();
 if (v.y > 0) {
 v.y = 0;
 }
 object.getLocation().y = ceilHeight - topHeight;
 if (!object.isFlying()) {
 physics.applyGravity(object, elapsedTime);
 }
 }
 // above floor
 else {
 if (!object.isFlying()) {
 // if scooting-up, stop the scoot
 if (v.y > 0 && !object.isJumping()) {
 v.y = 0;
 object.getLocation().y =
 floorHeight - bottomHeight;
 }
 else {
 physics.applyGravity(object, elapsedTime);
 }
 }
 }
}


This code overrides the checkFloorAndCeiling() method in the CollisionDetection class. The only thing left is to implement jumping—you know, for fun.

Make It Jump

Jumping works pretty much the same as it did in —just apply an upward velocity to an object. The power of this initial velocity diminishes as gravity is applied. Sometimes, though, you want to guarantee how high an object can jump. Screenshot shows the equation for this.

Screenshot Finding the jump velocity to jump at a specific height.

Java graphics 11fig15.gif


In Screenshot, the jump velocity (v) is found based on the height to jump (y–y0) and gravity acceleration (a). This is one of the standard velocity/acceleration formulas found in many physics textbooks. With this, you can add a few more methods to the Physics class to implement jumping, shown here in Listing 11.11.

Listing 11.11 Jumping (Physics.java)
/**
 Sets the specified GameObject's vertical velocity to jump
 to the specified height. Calls getJumpVelocity() to
 calculate the velocity, which uses the Math.sqrt()
 function.
*/
public void jumpToHeight(GameObject object, float jumpHeight) {
 jump(object, getJumpVelocity(jumpHeight));
}
/**
 Sets the specified GameObject's vertical velocity to the
 specified jump velocity.
*/
public void jump(GameObject object, float jumpVelocity) {
 velocity.setTo(0, jumpVelocity, 0);
 object.getTransform().getVelocity().y = 0;
 object.getTransform().addVelocity(velocity);
}
/**
 Returns the vertical velocity needed to jump the specified
 height (based on current gravity). Uses the Math.sqrt()
 function.
*/
public float getJumpVelocity(float jumpHeight) {
 // use velocity/acceleration formal: v*v = -2 * a(y-y0)
 // (v is jump velocity, a is accel, y-y0 is max height)
 return (float)Math.sqrt(-2*gravityAccel*jumpHeight);
}


These methods enable you to find the jump velocity needed to jump a certain height (based on the gravity acceleration) and to actually make an object jump.



   
Comments