Monday, May 17, 2010

Want to Start Game Programming? Start With Pong

In this post you will learn: basic game programming, including collision detection, timing and threading, and event handling.

Even though Pong was created in 1972, it still has a lot to teach us today about basic gaming. I will use Java to show the basics, since Java includes screen widgets in the basic API. You can use just about any language to do this, though.

Step 1: create the screen. We will need a basic screen layout to hold the game screen and a high score screen. Because what fun is a game if you don't keep score? I will use a class that extends JPanel to hold the two sub-panels. Why JPanel? It allows easy re-use of the logic. If you want to make this a stand-alone app, create a JFrame in the main method to hold it. If you want to use this as part of a larger app, you can add the Panel to whatever parent container you wish - JTabbedPane, Applet, etc.



public JavaPongPanel(){
Dimension size = new Dimension(600, 400);
setLayout(new BorderLayout());
mainPanel = new JPanel();
scorePanel = new JPanel();
add(mainPanel, BorderLayout.CENTER);
add(scorePanel, BorderLayout.EAST);
mainPanel.setPreferredSize(size);
scorePanel.setPreferredSize(new Dimension(100, size.height));
...



What we've got so far is a screen that's 600x400, and two panels added to it - one in the center, and one to the right.

Step 2: draw and move paddles. Nothing going on so far. We need to get some interactivity. We will add a MouseMotionListener to the game panel to make the paddles track the user's movements.



@Override
public void mouseMoved(MouseEvent arg0) {
Point2D.Double p = new Point2D.Double(arg0.getPoint().x, arg0.getPoint().y);
mousePoint = p;
Dimension d = mainPanel.getSize();
double xPoint = mousePoint.x - (PADDLE_WIDTH / 2);
double leftXPoint = 10;
double rightXPoint = d.width - PADDLE_HEIGHT - 10;

double yPoint = mousePoint.y - (PADDLE_WIDTH / 2);
double bottomYPoint = d.height - PADDLE_HEIGHT - 10;
double topYPoint = 10;

//top paddle
tPaddle.x = xPoint;
tPaddle.y = topYPoint;
//bottom paddle
bPaddle.x = xPoint;
bPaddle.y = bottomYPoint;
//left paddle
lPaddle.x = leftXPoint;
lPaddle.y = yPoint;
//right paddle
rPaddle.x = rightXPoint;
rPaddle.y = yPoint;

animate();
}



The four paddle variables are global Rectangle2D.Doubles, to allow us to track collisions. We'll get to that in a minute. Notice the animate() method call at the end. That is to update the screen with the movement of the mouse, to make the paddles track the mouse. We also need to call


mainPanel.addMouseMotionListener(this);


to have mouse events forward from the panel to the listener.

Step 3: Animation. There will be two sources of animation changes - the paddle motion that we just did, and the movement of the ball. The movement of the ball will be done regardless of user interaction, so for that we will need to use a thread. JavaPongPanel already implements Runnable, so we will need to define a run() method.


@Override
public void run() {
try {
while (ballMoving) {
Thread.sleep(10);
pdv.setPoint(pdv.getNextPoint());
checkCollision();
animate();
}
} catch (Throwable t) {
t.printStackTrace();
}
}


Now we've got a thread that will set the ball location (pdv is a PointDirectionVelocity class that holds the ball's current position, direction and velocity to calculate the next position if it doesn't hit anything), check for collision, and redraw the screen. Now we just need a reason to start the thread. We will print a message to the screen, and when the user clicks on the screen, the game will start. So in the constructor, you will have this:


MouseInputAdapter adapter = new MouseInputAdapter(){
public void mouseClicked(MouseEvent arg0) {
if(!JavaPongPanel.this.ballMoving){
JavaPongPanel.this.ballMoving = true;
Thread t = new Thread(JavaPongPanel.this);
t.start();
}
}
};
mainPanel.addMouseListener(adapter);


Which will start the thread when the user clicks on the screen, if the game isn't already started. The animate method needs to draw the ball position, the paddle position, and the score.


private void animate(){
drawScore();
Dimension d = mainPanel.getSize();
Graphics2D screen = (Graphics2D) mainPanel.getGraphics();
Graphics2D graphics = (Graphics2D) image.createGraphics();
graphics.setBackground(Color.black);
graphics.clearRect(0, 0, d.width, d.height);
graphics.setColor(Color.white);

graphics.fill(tPaddle);
graphics.fill(bPaddle);
graphics.fill(lPaddle);
graphics.fill(rPaddle);
drawBall(graphics);
screen.drawImage(image, 0, 0, null);
}

private void drawScore(){
Graphics2D scoreG = (Graphics2D) scorePanel.getGraphics();
scoreG.setBackground(Color.blue);
scoreG.clearRect(0, 0, scorePanel.getWidth(), scorePanel.getHeight());
scoreG.setColor(Color.white);
scoreG.drawString("Score: " + score, 10, 20);
}

private void drawBall(Graphics2D graphics){
graphics.setColor(Color.white);
if(ballMoving){
ball.x = pdv.getPoint().x - (BALL_WIDTH / 2);
ball.y = pdv.getPoint().y - (BALL_HEIGHT / 2);
graphics.fill(ball);
}
else{
Dimension d = mainPanel.getSize();
resetBall();
int xCenter = d.width / 2;
int yCenter = d.height / 2;
graphics.drawString("Click to Begin", xCenter - 50, yCenter - 20);
graphics.fill(ball);
}
}


Note that I'm actually using the graphics object from an image, not from the component. This is double-buffering and allows clearer screen refreshing than drawing each thing at a time to the live screen. It's not absolutely necessary, but it helps the game look cleaner. To create the image, you would call

image = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_RGB);

in the constructor after you set the screen size.

Step 4: Collision detection. I have used the Java Shape API to define the Rectangles for the paddles, and the Ellipse for the ball. This gives me the ability to let Java check for intersection and react when the ball is in contact with the paddle. We now have a checkCollision method:


private void checkCollision(){
Dimension d = mainPanel.getSize();
Point2D.Double ballPoint = pdv.getPoint();
//paddle detection

double direction = pdv.getDirection();
if(ball.intersects(tPaddle) && (direction > 90 && direction <> 270 && direction <> 180 && direction <> 0 && direction <>= d.width){
ballMoving = false;
resetBall();
}else if(ballPoint.x <= 0){ ballMoving = false; resetBall(); } else if(ballPoint.y >= d.height){
ballMoving = false;
resetBall();
}else if(ballPoint.y <= 0){ ballMoving = false; resetBall(); } }


The check for direction in the paddle collision checking is to handle the case where the collision isn't detected until the ball is somewhat inside the paddle, and the next point would not totally clear the paddle. Without this check, the ball would appear to "stick" to the paddle and shimmy down the length of it. This is not what we want. The screen edge detection will put the ball back in the center, and restart the game.

So there you go: a couple hundred lines of Java code, and you're got a playable game. From here, there are many things you could do. Instead of just reflecting the ball off the paddle, you could adjust the angle relative to the paddle, and if the hit isn't on-center, you can add some angle to the ball. You've already got a ball bouncing off of rectangles, you're most of the way to a Breakout-style game. It's all up to your imagination.