14.7. Case Study: The Game of PongThe game of Pong was one of the first computer video games and was all the rage in the 1970s. The game consists of a ball that moves horizontally and vertically within a rectangular region, and a single paddle, located at the right edge of the region, that can be moved up and down by the user. When the ball hits the top, left, or bottom wall or the paddle, it bounces off in the opposite direction. If the ball misses the paddle, it passes through the right wall and re-emerges at the left wall. Each time the ball bounces off a wall or the paddle, it emits a pong sound. 14.7.1. A Multithreaded DesignLet's develop a multithreaded applet to play the game of Pong. Figure 14.29 shows how the game's GUI should appear. There are three objects involved in this program: the applet, which serves as the GUI, the ball, which is represented as a blue circle in the applet, and the paddle, which is represented by a red rectangle along the right edge of the applet. What cannot be seen in the figure is that the ball moves autonomously, bouncing off the walls and paddle. The paddle's motion is controlled by the user by pressing the up- and down-arrow keys on the keyboard. Figure 14.29. The UI for Pong.
We will develop class definitions for the ball, paddle, and applet. Following the example of our dot-drawing program earlier in the chapter, we will employ two independent threads, one for the GUI and one for the ball. Because the user will control the movements of the paddle, the applet will employ a listener object to listen for and respond to the user's key presses. Figure 14.30 provides an overview of the object-oriented design of the Pong program. The PongApplet class is the main class. It uses instances of the Ball and Paddle classes. PongApplet is a subclass of JApplet and implements the KeyListener interface. This is another of the several event handlers provided in the java.awt library. This one handles KeyEvents, and the KeyListener interface consists of three abstract methods, keyPressed(), keyTyped(), and keyReleased(), all of which are associated with the act of pressing a key on the keyboard. All three of these methods are implemented in the PongApplet class. A key-typed event occurs when a key is pressed down. A key-release event occurs when a key that has been pressed down is released. A key-press event is a combination of both of these events. Figure 14.30. Design of the Pong program. |
public class Paddle { public static final int HEIGHT = 50; // Paddle size public static final int WIDTH = 10; private static final int DELTA = HEIGHT/2; // Move size private static final int BORDER = 0; private int gameAreaHeight; private int locationX, locationY; private PongApplet applet; public Paddle (PongApplet a) { applet = a; gameAreaHeight = a.getHeight(); locationX = a.getWidth()-WIDTH; locationY = gameAreaHeight/2; } // Paddle() public void resetLocation() { gameAreaHeight = applet.getHeight(); locationX = applet.getWidth()-WIDTH; } public int getX() { return locationX; } // getX() public int getY() { return locationY; } // getY() public void moveUp () { if (locationY > BORDER ) locationY -= DELTA; } // moveUp() public void moveDown() { if (locationY + HEIGHT < gameAreaHeight - BORDER) locationY += DELTA; } // moveDown() } // Paddle class |
Class constants HEIGHT and WIDTH are used to define the size of the Paddle, which is represented on the applet as a simple rectangle. The applet will use Graphics fillRect() method to draw the paddle:
g.fillRect(pad.getX(),pad.getY(),Paddle.WIDTH,Paddle.HEIGHT);
Note how the applet uses the paddle's getX() and getY() methods to get the paddle's current location.
The class constants DELTA and BORDER are used to control the paddle's movement. DELTA represents the number of pixels that the paddle moves on each move up or down, and BORDER is used with gameAreaHeight to keep the paddle within the drawing area. The moveUp() and moveDown() methods are called by the applet each time the user presses an up- or down-arrow key. They change the paddle's location by DELTA pixels up or down.
The Ball class (Fig. 14.32) uses the class constant SIZE to determine the size of the oval that represents the ball, drawn by the applet as follows:
g.fillOval(ball.getX(),ball.getY(),ball.SIZE,ball.SIZE);
import javax.swing.*; import java.awt.Toolkit; public class Ball extends Thread { public static final int SIZE = 10; // Diameter of the ball private PongApplet applet; // Reference to the applet private int topWall, bottomWall, leftWall, rightWall; // Boundaries private int locationX, locationY; // Current location of the ball private int directionX = 1, directionY = 1; // x- and y-direction (1 or -1) private Toolkit kit = Toolkit.getDefaultToolkit(); // For beep() method public Ball(PongApplet app) { applet = app; locationX = leftWall + 1; // Set initial location locationY = bottomWall/2; } // Ball() public int getX() { return locationX; } // getX() public int getY() { return locationY; } // getY() public void move() { rightWall = applet.getWidth() - SIZE; // Define bouncing region leftWall = topWall = 0; // And location of walls bottomWall = applet.getHeight() - SIZE; locationX = locationX + directionX; // Calculate a new location locationY = locationY + directionY; if (applet.ballHitsPaddle()){ directionX = -1; // move toward left wall kit.beep(); } // if ball hits paddle if (locationX <= leftWall){ directionX = + 1; // move toward right wall kit.beep(); } // if ball hits left wall if (locationY + SIZE >= bottomWall || locationY <= topWall){ directionY = -directionY; // reverse direction kit.beep(); } // if ball hits top or bottom walls if (locationX >= rightWall + SIZE) { locationX = leftWall + 1; // jump back to left wall } // if ball goes through right wall } // move() public void run() { while (true) { move(); // Move applet.repaint(); try { sleep(15); } catch (InterruptedException e) {} } // while } // run() } // Ball class |
As with the paddle, the applet uses the ball's getX() and getY() method to determine the ball's current location.
Unlike the paddle, however, the ball moves autonomously. Its run() method, which is inherited from its Thread superclass, repeatedly moves the ball, draws the ball, and then sleeps for a brief interval (to slow down the speed of the ball's apparent motion). The run() method itself is quite simple because it consists of a short loop. We will deal with the details of how the ball is painted on the applet when we discuss the applet itself.
The most complex method in the Ball class is the move() method. This is the method that controls the ball's movement within the boundaries of the applet's drawing area. This method begins by moving the ball one pixel left, right, up, or down by adjusting the values of its locationX and locationY coordinates:
locationX = locationX + directionX; // Calculate location locationY = locationY + directionY;
The directionX and directionY variables are set to either +1 or -1, depending on whether the ball is moving left or right, up or down. After the ball is moved, the method uses a sequence of if statements to check whether the ball is touching one of the walls or the paddle. If the ball is in contact with the top, left, or bottom wall or the paddle, its direction is changed by reversing the value of the directionX or directionY variable. The direction changes depend on whether the ball has touched a horizontal or vertical wall. When the ball touches the right wall, having missed the paddle, it passes through the right wall and re-emerges from the left wall going in the same direction.
Note how the applet method ballHitsPaddle() is used to determine whether the ball has hit the paddle. This is necessary because only the applet knows the locations of both the ball and the paddle.
The implementation of the PongApplet class is shown in Figure 14.33. The applet's main task is to manage the drawing of the ball and paddle and to handle the user's key presses. Handling keyboard events is a simple matter of implementing the KeyListener interface. This works in much the same way as the ActionListener interface, which is used to handle button clicks and other ActionEvents. Whenever a key is pressed, it generates KeyEvents, which are passed to the appropriate methods of the KeyListener interface.
import javax.swing.*; import java.awt.*; import java.awt.event.*; public class PongApplet extends JApplet implements KeyListener { private Ball ball; private Paddle pad; public void init() { setBackground(Color.white); addKeyListener(this); pad = new Paddle(this); // Create the paddle ball = new Ball(this); // Create the ball ball.start(); requestFocus(); // Required to receive key events } // init() public void paint (Graphics g ) { g.setColor(getBackground()); // Erase the drawing area g.fillRect(0,0,getWidth(),getHeight()); g.setColor(Color.blue); // Paint the ball g.fillOval(ball.getX(),ball.getY(),ball.SIZE,ball.SIZE); pad.resetLocation(); // Paint the paddle g.setColor(Color.red); g.fillRect(pad.getX(),pad.getY(),Paddle.WIDTH,Paddle.HEIGHT); } // paint() public boolean ballHitsPaddle() { return ball.getX() + Ball.SIZE >= pad.getX() && ball.getY() >= pad.getY() && ball.getY() <= pad.getY() + Paddle.HEIGHT; } // ballHitsPaddle() public void keyPressed( KeyEvent e) { // Check for arrow keys int keyCode = e.getKeyCode(); if (keyCode == e.VK_UP) // Up arrow pad.moveUp(); else if (keyCode == e.VK_DOWN) // Down arrow pad.moveDown(); } // keyReleased() public void keyTyped(KeyEvent e) {} // Unused public void keyReleased( KeyEvent e) {} // Unused } // PongApplet class |
There is a bit of redundancy in the KeyListener interface in the sense that a single key press and release generates three KeyEvents: a key-typed event, when the key is pressed, a key-released event when the key is released, and a key-pressed event when the key is pressed and released. While it is important for some programs to be able to distinguish between key-typed and key-released events, for this program we will take action whenever either of the arrow keys is pressed (typed and released). Therefore, we implement the keyPressed() method as follows:
public void keyPressed( KeyEvent e) { // Check arrow keys int keyCode = e.getKeyCode(); if (keyCode == e.VK_UP) // Up arrow pad.moveUp(); else if (keyCode == e.VK_DOWN) // Down arrow pad.moveDown(); } // keyReleased()
Each key on the keyboard has a unique code that identifies it. The key's code is gotten from the KeyEvent object by means of the getKeyCode() method. Then it is compared with the codes for the up-arrow and down-arrow keys, which are implemented as class constants, VK_UP and VK_DOWN, in the KeyEvent class. If either of these keys is typed, the appropriate paddle method, moveUP() or moveDown(), is called.
Even though we are not using the keyPressed() and keyReleased() methods in this program, it is still necessary to provide implementations for them in the applet. In order to implement an interface, such as the KeyListener interface, you must implement all the abstract methods in the interface. That is why we provide trivial implementations of the keyPressed() and keyReleased() methods.
Computer animation is accomplished by repeatedly drawing, erasing, and redrawing an object at different locations on the drawing panel. The applet's paint() method is used for drawing the ball and the paddle at their current locations. The paint() method is never called directly. Rather, it is called automatically after the init() method, when the applet is started. It is then invoked indirectly by the program by calling the repaint() method, which is called in the run() method of the Ball class. The reason that paint() is called indirectly is because Java needs to pass it the applet's current Graphics object. Recall that in Java all drawing is done using a Graphics object.
In order to animate the bouncing ball, we first erase the current image of the ball, then we draw the ball in its new location. We also draw the paddle in its current location. These steps are carried out in the applet's paint() method. First the drawing area is cleared by painting its rectangle in the background color. Then the ball and paddle are painted at their current locations. Before painting the paddle, we first call its resetLocation() method. This causes the paddle to be relocated in case the user has resized the applet's drawing area. There is no need to do this for the ball because the ball's drawing area is updated within the Ball.move() method every time the ball is moved.
One problem with computer animations of this sort is that the repeated drawing and erasing of the drawing area can cause the screen to flicker. In some drawing environments a technique known as double buffering is used to reduce the flicker. In double buffering, an invisible, off-screen, buffer is used for the actual drawing operations and is then used to replace the visible image all at once when the drawing is done. Fortunately, Java's Swing components, including JApplet and JFrame, perform an automatic form of double buffering, so we need not worry about it. Some graphics environments, including Java's AWT environment, do not perform double buffering automatically, in which case the program itself must carry it out.
Double buffering
Like the other examples in this chapter, the game of Pong provides a simple illustration of how threads are used to coordinate concurrent actions in a computer program. As computer game fans will realize, most modern interactive computer games utilize a multithreaded design. The use of threads allows our interactive programs to achieve a responsiveness and sophistication that is not possible in single-threaded programs. One of the great advantages of Java is that it simplifies the use of threads, thereby making thread programming accessible to programmers. However, one of the lessons illustrated in this chapter is that multithreaded programs must be carefully designed in order to work effectively.
Exercise 14.12 | Modify the PongApplet program so that it contains a second ball that starts at a different location from the first ball. |