The 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, which is 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 walls 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 paddle, it emits a pong sound.
Subsection14.7.1A Multithreaded Design
Let’s develop a multithreaded GUI to play the game of Pong. Figure 14.7.1 shows how the game’s GUI should appear.
There are three objects involved in this program: the frame, which serves as the GUI, the ball, which is represented as a blue circle in the program, and the paddle, which is represented by a red rectangle along the right edge of the frame. What cannot be seen in this 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.
We will develop class definitions for the ball, paddle, and the frame. 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 frame will employ a listener object to listen for and respond to the user’s key presses.
Figure 14.7.2 provides an overview of the object-oriented design of the Pong program. The PongFrame class is the main class. It uses ly to keep track of its motion within the program’s drawing panel. The design strategy employed here leaves the drawing of the ball up to the frame. The Ball thread itself just handles the movement within the program’s drawing panel. Note that the Ball() constructor takes a reference to the PongFrame. As we will see, the Ball uses this reference to set the dimensions of the frame’s drawing panel. Also, as the Ball moves, it will repeatedly call the frame’s repaint() method to draw the ball.
The Paddle class is responsible for moving the paddle up and down along the drawing panel’s right edge. Its public methods, moveUP() and moveDown(), will be called by the frame in response to the user pressing the up and down arrows on the keyboard. Because the frame needs to know where to draw the paddle, the paddle class contains several public methods, getX(), getY(), and resetLocation(), whose tasks are to report the paddle’s location or to adjust its location in case the frame is resized.
The PongFrame controls the overall activity of the program. Note in particular its ballHitsPaddle() method. This method has the task of determining when the ball and paddle come in contact as the ball continuously moves around in the frame’s drawing panel. As in the ThreadedDotty example earlier in the chapter, it is necessary for the Ball and the the frame to be implemented as separated threads so that the frame can be responsive to the user’s key presses.
Subsection14.7.2Implementation of the Pong Program
We begin our discussion of the program’s implementation with the Paddle class implementation (Listing 14.7.3).
Class constants, HEIGHT and WIDTH are used to define the size of the Paddle, which is represented on the frame as a simple rectangle. The frame will use the Graphics.fillRect() method to draw the paddle:
Note how the frame 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 frame each time the user presses an up- or down-arrow key. They simply change the paddle’s location by DELTA pixels up or down.
The Ball class (Fig. Listing 14.7.4) uses the class constant SIZE to determine the size of the oval that represents the ball, drawn by the frame as follows:
As with the paddle, the frame 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 frame when we discuss the frame 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 frame’s drawing area. This method begins by moving the ball by one pixel left, right, up, or down by adjusting the values of its locationX and locationY coordinates:
The directionX and directionY variables are set to either \(+1\) or \(-1\text{,}\) 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 walls 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 frame method, ballHitsPaddle() is used to determine whether the ball has hit the paddle. This is necessary because only the frame knows the locations of both the ball and the paddle.
Subsection14.7.3The KeyListenerInterface
The implementation of the PongFrame class is shown in Listing 14.7.5. The frame’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 ActionEvent s. Whenever a key is pressed, it generates KeyEvent s, which are passed to the appropriate methods of the KeyListener interface.
There’s a bit of redundancy in the KeyListener interface in the sense that a single key press and release generates three KeyEvent s: 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 a key-typed and key-released event, for this program, we will take action whenever one 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 the key. 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 those keys were typed, the appropriate paddle method, moveUP() or moveDown(), is called.
Note that even though we are not using the keyPressed() and keyReleased() methods in this program, it is still necessary to provide implementations for these methods in the frame. 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 both the keyPressed() and keyReleased() methods.
Subsection14.7.4Animating the Bouncing Ball
Computer animation is accomplished by repeatedly drawing, erasing, and re-drawing an object at different locations on the drawing panel. The frame’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 constructor method PongFrame(), when the program 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 frame’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 frame’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. Note that before painting the paddle, we first call its resetLocation() method. This causes the paddle to be relocated in case the user has resized the frame’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 it 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 needn’t 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.
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 most 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 that should be drawn from this chapter is that multithreaded programs must be carefully designed in order to work effectively.
ExercisesSelf-Study Exercise
Activity14.7.1.Pong Game.
Use Replit to have a go at Pong. If you want a challenge, add a second ball to the game. You’ll have to copy the source code and save it on your own system.