Sunday, August 22, 2010

More Collision Detection: Bouncing the Right Way

A common result of a collision between two objects in a video game is a bounce. To do this right, we have to know which direction to make things bounce. Here's one way to do that easily in Java. I've got a ball that's going to bounce in one of eight directions depending on how it hits a brick.

Diagram showing the results of a ball hitting a brick and bouncing, depending on how it strikes the brick. Collision detection in action.

I've marked the changes to the ball's velocity that will occur as a result of each possible type of collision. The three arrows on the left side all show that the xVel (x velocity) is a negative value, the three arrows on the bottom all show that yVel (y velocity) should become a positive value.

In the cases of the corners, both the x and y velocities are set. The bounces off the middles of the sides all cause one velocity to be set, but the other is left alone. For example, a bounce off the right side of the brick sets the xVel to a positive value, but the yVel is unchanged. So if yVel was a positive value before (meaning the ball is travelling down on screen) then it will keep travelling down. If it was negative--moving up--then it will keep moving up. Only the x direction of movement will change. It will start moving from left to right instead of right to left.

It's really easy to detect a collision, but now we need to know the relationship between the two colliding objects so that we can make the ball bounce in the correct direction. As we learned in Stopping Jonathan Livingston Seagull, the effect of how much we move an object before we check a collision can cause problems if we don't understand the limits of how we detect them.

In this program, our ball's movement is presently set at one quarter of its width. We could go as high as one half its width and still have our collision detection routines work properly here. Fortunately, we want to keep the distance the ball moves each turn short to make the movement look smoother.

When we hit the brick, we want to see where it hits the ball. This will tell the ball which way to bounce, just like a real ball. In the drawing above, we have a red brick on a cyan (blue-green) playfield. The ball is dark blue. Where the ball is intersecting, or overlapping, the brick is shown in yellow. Now we just need to know which side of the ball is getting "pushed in" this way. Then we bounce the opposite way.

Here's the program, the key code we're talking about is highlighted:
/* A simple video game style kernel, revision 4.
by Mark Graybill, August 2010
Has a ball bouncing off a brick.
Because the ball moves at a fixed speed,
slow enough we won't miss a collision,
we can use simple colliision detection.

Uses the Brick class from the article
"Multiple Constructor Methods"
*/

import java.util.*;
import java.awt.*;
import javax.swing.*;
import java.lang.Math;

public class VidBrick extends JPanel{

public Rectangle screen, bounds; // The screen area and boundary.
public JFrame frame; // A JFrame to put the graphics into.
public VGTimerTask vgTask; // The TimerTask that runs the game.
public VGBall ball; // The game ball, a subclass of Rectangle.
private Brick brick; // A brick for the ball to interact with.

/* This sets up the screen area, and creates instances of
the JFrame, VGBall, VGTimerTask, etc that we'll need. */
public VidBrick(){
super();
screen = new Rectangle(0, 0, 600, 400);
bounds = new Rectangle(0, 0, 600, 400); // Give some temporary values.
ball = new VGBall();
frame = new JFrame("VidBrick");
vgTask = new VGTimerTask();
brick = new Brick();
}

class VGTimerTask extends TimerTask{
public void run(){
ball.move();
frame.repaint();
}
}

class VGBall extends Rectangle{
int xVel, yVel; // The ball's velocity.
Color ballColor; // The color of the ball.

/* Create a VGBall with default location of upper left
corner, size of 20x20 pixels, moving at one quarter
its height and width per turn--plus it's blue. */
public VGBall(){
super(0, 0, 20, 20);
xVel = width/4;
yVel = height/4;
ballColor=new Color(0, 0, 128);
}

/* Move the Ball. */
public void move(){
// Move the ball according to the game rules.
x+=xVel; // Move horizontally.
y+=yVel; // Move vertically.

// Detect edges and bounce if necessary.
if (x > (bounds.width - width)){
xVel = -xVel; // reverse movement.
x = bounds.width - width; // Set location to screen edge.
}
if (y > (bounds.height - height)){
yVel = -yVel; // reverse movement.
y = bounds.height - height;
}
if (x <= 0) { xVel = -xVel; x = 0; }
if (y <= 0) { yVel = -yVel; y = 0; }

// Detect Brick and bounce if necessary.
if (intersects(brick)) {
// Get the intersection rectangle to find out which way to bounce.
Rectangle iRect = intersection(brick);
// If we hit on the left side, go left (negative x velocity).
if ((x+(width/2))<(iRect.x+(iRect.width/2))){xVel=(0-Math.abs(xVel));}
// If we hit on the right side, go right (positive x velocity).
if ((x+(width/2))>(iRect.x+(iRect.width/2))){xVel=Math.abs(xVel);}
// If we hit on the top, go up.
if ((y+(height/2))<(iRect.y+(iRect.height/2))){yVel=(0-Math.abs(yVel));}
// If we hit on the bottom, go down.
if ((y+(height/2))>(iRect.y+(iRect.height/2))){yVel=Math.abs(yVel);}
} // if intersects
}

/* Draw the ball into the provided graphics context. Preserves
the context's drawing color setting. */

public void draw(Graphics g){
// the ball draws itself in the graphics context given.
Color gcColor = g.getColor(); // Preserve the present color.
g.setColor(ballColor); // Use the ball's color for the ball.
g.fillRect(x, y, width, height); // Draw the ball.
g.setColor(gcColor); // Restore prior color.
} // end draw()
} // end of class VGBall

public void paintComponent(Graphics g){
// Get the drawing area bounds for game logic.
bounds = g.getClipBounds();
// Clear the drawing area.
g.clearRect(screen.x, screen.y, screen.width, screen.height);
// Draw the brick.
g.setColor(brick.getColor());
g.fillRect(brick.x, brick.y, brick.width, brick.height);
// Draw the ball.
ball.draw(g);
}

/* Main program loop. */
public static void main(String arg[]){

java.util.Timer vgTimer = new java.util.Timer(); // Create a Timer object
VidBrick panel = new VidBrick();

panel.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
panel.frame.setSize(panel.screen.width, panel.screen.height);

panel.frame.setContentPane(panel);
panel.frame.setVisible(true);

// Set up the brick.
panel.brick.x = panel.screen.width/3;
panel.brick.y = panel.screen.height/3;
panel.brick.width = panel.screen.width/3;
panel.brick.height = panel.screen.height/3;

// Set up a timer to do the vgTask regularly.
vgTimer.schedule(panel.vgTask, 0, 33);
}
}

The way we figure out which way to bounce is to find the center of the ball and the center of the intersection rectangle (the yellow box in the picture, where the ball and the brick overlap.) Then if the center of the intersection (shown as an orange dot on the pic) is to the right of the center of the ball (a white dot) we bounce left. If it's to the left, we bounce right, if it's above we bounce down, below we bounce up.

This way we don't have to worry about the size or shape of the brick itself. All we care about is the bit that strikes the ball. Then the ball bounces the correct way.

If we had wanted to get really fancy, we could have made the ball bounce at different angles depending on the angle between the center of the ball and the center of the intersection rectangle. If you're familiar with trigonometry, take a look at the Math class in Java and see if you can implement this.
StumbleUpon