Skip to main content

Section 14.7 CASE STUDY: The Game of Pong

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.

Figure 14.7.1. Pong game.

Subsection 14.7.1 A 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.

Figure 14.7.2. Pong classes.

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.

Subsection 14.7.2 Implementation of the Pong Program

We begin our discussion of the program's implementation with the Paddle class implementation (ListingĀ 14.7.3).

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 = 20;
    private int gameAreaHeight;
    private int locationX, locationY;
    private PongFrame frame;

    public Paddle(PongFrame f) {
        frame = f;
        gameAreaHeight = f.getHeight() - BORDER;
        locationX = f.getWidth() - WIDTH;
        locationY = gameAreaHeight / 2;
    } // Paddle()

    public void resetLocation() {
        gameAreaHeight = frame.getHeight() - BORDER;
        locationX = frame.getWidth() - WIDTH;
    }

    public int getX() { return locationX; }

    public int getY() {  return locationY; }

    public void moveUp() {
        if (locationY > BORDER)
            locationY -= DELTA;
    } // moveUp()

    public void moveDown() {
        if (locationY + HEIGHT < gameAreaHeight + BORDER)
            locationY += DELTA;
    } // moveDown()
} // Paddle
Listing 14.7.3. Definition of the Paddle class.

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:

g.fillRect(pad.getX(),pad.getY(),Paddle.WIDTH,Paddle.HEIGHT);

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:

g.fillOval(ball.getX(),ball.getY(),ball.SIZE,ball.SIZE);

As with the paddle, the frame uses the ball's getX() and getY() method to determine the ball's current location.

import javax.swing.*;
import java.awt.Toolkit;

class Ball extends Thread {
    public static final int SIZE = 10; // Diameter of the ball
    private PongFrame frame; // Reference to the frame
    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(PongFrame f) {
        frame = f;
        locationX = 50; // Set initial location
        locationY = 50;
    } // Ball()

    public int getX() { return locationX; } // getX()

    public int getY() { return locationY; } // getY()

    public void move() {
        rightWall = frame.getWidth() - SIZE; // Define bouncing region
        leftWall = 0;
        topWall = 20; // And location of walls
        bottomWall = frame.getHeight() - SIZE;
        locationX = locationX + directionX; // Calculate a new location
        locationY = locationY + directionY;
        if (frame.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
            frame.repaint();
            try {
                sleep(15);
            } catch (InterruptedException e) {
            }
        } // while
    } // run()
} // Ball
Listing 14.7.4. Definition of the Ball class.

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:

locationX = locationX + directionX; // Calculate location
locationY = locationY + directionY;

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.

Subsection 14.7.3 The 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.

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class PongFrame extends JFrame implements KeyListener {
    private Ball ball;
    private Paddle pad;

    public PongFrame() {
        setTitle("Pong");
        setBackground(Color.white);
        addKeyListener(this);
        pad = new Paddle(this); // Create the paddle
        ball = new Ball(this); // Create the ball
        ball.start();
    } // PongFrame()

    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

    public static void main(String[] args) {
        PongFrame f = new PongFrame();
        f.setSize(400, 400);
        f.setVisible(true);
    }
} // PongFrame
Listing 14.7.5. Definition of the PongFrame class.

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.

Subsection 14.7.4 Animating 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.

Exercises Self-Study Exercise

Activity 14.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.

You have attempted of activities on this page.