An Undecorated Full-Screen (UFS) Worm

Figure 4-5 shows the UFS version of WormChase, a full-screen JFrame without a titlebar or borders.

The absence of a titlebar means I have to rethink how to pause and resume the application (previously achieved by minimizing/maximizing the window) and how to terminate the game. The solution is to draw Pause and Quit buttons on the canvas at the bottom-right corner. Aside from using the Quit button, ending the game is possible by typing the Esc key, Ctrl-C, the q key, or the End key. Data that were previously displayed in text fields are written to the canvas at the lower-left corner.

Figure 4-6 gives the class diagrams for the UFS version of WormChase, including the public methods.

A comparison with the AFS class diagrams in Figure 4-2 shows a considerable simplification of WormChase and fewer methods in WormPanel.

The UFS worm

Figure 4-5. The UFS worm

Class diagrams for the UFS version of WormChase

Figure 4-6. Class diagrams for the UFS version of WormChase

The WormChase class no longer has to be a WindowListener and, therefore, doesn't contain window handler methods, such as windowClosing(). The pauseGame(), resumeGame(), and stopGame() methods in WormPanel are no longer required. The Worm class is unchanged, and the Obstacles class is altered only so it can call setBoxNumber() in WormPanel; this method was formerly in WormChase and wrote to a text field.

Tip

The code for the UFS WormChase can be found in the Worm/WormUFS/ directory.

The UFS WormChase Class

With the removal of the WindowListener methods, WormChase hardly does anything. It reads the requested FPS value from the command line, and its constructor creates the WormPanel object:

    public WormChase(long period)
    { super("The Worm Chase");

      Container c = getContentPane();
      c.setLayout( new BorderLayout() );

      WormPanel wp = new WormPanel(this, period);
      c.add(wp, "Center");

      setUndecorated(true);   // no borders or titlebar
      setIgnoreRepaint(true);  // turn off paint events since doing active rendering
      pack();
      setResizable(false);
      setVisible(true);
    }  // end of WormChase() constructor

The titlebars and other insets are switched off by calling setUndecorated(). setIgnoreRepaint() is utilized since no GUI components require paint events; WormPanel uses active rendering and, therefore, doesn't need paint events.

The simplicity of WormChase indicates that a separate JPanel as a drawing canvas is no longer needed. Moving WormPanel's functionality into WormChase is straightforward, and I'll explore that approach as part of the FSEM version of WormChase later in this chapter.

The Game Panel

WormPanel's constructor sets its size to that of the screen and stores the dimensions in the global variables pWidth and pHeight:

    Toolkit tk = Toolkit.getDefaultToolkit();
    Dimension scrDim = tk.getScreenSize();
    setPreferredSize(scrDim);   // set JPanel size

    pWidth = scrDim.width;      // store dimensions for later
    pHeight = scrDim.height;

The constructor creates two rectangles, pauseArea and quitArea, which represent the screen areas for the Pause and Quit buttons:

    private Rectangle pauseArea, quitArea;  // globals

    // in WormPanel()
    // specify screen areas for the buttons
    pauseArea = new Rectangle(pWidth-100, pHeight-45, 70, 15);
    quitArea  = new Rectangle(pWidth-100, pHeight-20, 70, 15);

The drawing of these buttons is left to gameRender(), which is described in the next section.

Button Behavior

As is common with many games, the Pause and Quit buttons are highlighted when the mouse moves over them. This transition is shown in Figure 4-7 when the mouse passes over the Pause button.

Highlighting the Pause button

Figure 4-7. Highlighting the Pause button

Another useful kind of feedback is to indicate that the game is paused by changing the wording of the Pause button to "Paused," as in Figure 4-8.

The Pause button when the game is paused

Figure 4-8. The Pause button when the game is paused

Tip

When the mouse moves over the Paused button, the text turns green.

The first step to implementing these behaviors is to record when the cursor is inside the pause or quit screen area. This is done by monitoring mouse movements, started in the constructor for WormPanel:

    addMouseMotionListener( new MouseMotionAdapter() {
      public void mouseMoved(MouseEvent e)
      { testMove(e.getX(), e.getY()); }
    });

testMove() sets two global Booleans (isOverPauseButton and isOverQuitButton) depending on whether the cursor is inside the pause or quit area:

    private void testMove(int x, int y)
    // is (x,y) over the Pause or Quit button?
    {
      if (running) {   // stops problems with a rapid move
                       // after pressing Quit
        isOverPauseButton = pauseArea.contains(x,y) ? true : false;
        isOverQuitButton = quitArea.contains(x,y) ? true : false;
      }
    }

The test of the running Boolean prevents button highlight changes after the player has pressed Quit but before the application exits.

The other aspect of button behavior is to deal with a mouse press on top of a button. This is handled by extending testPress(), which previously only dealt with clicks on or near the worm:

    // in the WormPanel constructor
    addMouseListener( new MouseAdapter() {
      public void mousePressed(MouseEvent e)
      { testPress(e.getX(), e.getY()); }
    });


    private void testPress(int x, int y)
    {
    if (isOverPauseButton)
        isPaused = !isPaused;     // toggle pausing
      else if (isOverQuitButton)
        running = false;
      else {
        if (!isPaused && !gameOver) {
          // was mouse pressed on or near the worm?
         . . .
        }
      }
    }

The highlighted lines in testPress() replace the functionality supported by resumeGame(), pauseGame(), and stopGame() in the earlier windowed versions of WormChase.

Drawing the Game Canvas

The WormPanel canvas contains two elements absent in previous examples:

  • The time used and boxes information, drawn in the bottom-left corner

  • The Pause and Quit buttons, drawn in the bottom-right corner

The buttons are drawn in a different way if the cursor is over them, and the wording on the Pause button changes depending on whether the game is paused.

These new features are implemented in gameRender():

    private void gameRender()
    {
      // as before: create the image buffer initially
      // set the background to white
      ...

      // report average FPS and UPS at top left
      dbg.drawString("Average FPS/UPS: " + df.format(averageFPS) +
                        ", " + df.format(averageUPS), 20, 25);

      // report time used and boxes used at bottom left
      dbg.drawString("Time Spent: " + timeSpentInGame + " secs", 10, pHeight-15);
      dbg.drawString("Boxes used: " + boxesUsed, 260, pHeight-15);

      // draw the Pause and Quit "buttons"
      drawButtons(dbg);

      dbg.setColor(Color.black);

      // as before: draw game elements: the obstacles and the worm
      obs.draw(dbg);
      fred.draw(dbg);

      if (gameOver)
        gameOverMessage(dbg);
    }  // end of gameRender()


    private void drawButtons(Graphics g)
    {
      g.setColor(Color.black);

      // draw the Pause "button"
      if (isOverPauseButton)
        g.setColor(Color.green);

      g.drawOval( pauseArea.x, pauseArea.y, pauseArea.width, pauseArea.height);
      if (isPaused)
        g.drawString("Paused", pauseArea.x, pauseArea.y+10);
      else
        g.drawString("Pause", pauseArea.x+5, pauseArea.y+10);

      if (isOverPauseButton)
        g.setColor(Color.black);

      // draw the Quit "button"
      if (isOverQuitButton)
        g.setColor(Color.green);

      g.drawOval(quitArea.x, quitArea.y, quitArea.width, quitArea.height);
      g.drawString("Quit", quitArea.x+15, quitArea.y+10);

      if (isOverQuitButton)
        g.setColor(Color.black);
    }  // drawButtons()

Each button is an oval with a string over it. Highlighting triggers a change in the foreground color, using setColor(). Depending on the value of the isPaused Boolean, "Paused" or "Pause" is drawn.

Exiting the Game

The primary means for terminating the game remains the same as in previous examples: When the running Boolean is true, the animation loop will terminate. Before run() returns, the finishOff() method is called:

    private void finishOff()
    { if (!finishedOff) {
        finishedOff = true;
        printStats();
        System.exit(0);
      }
    }

The finishedOff Boolean is employed to stop a second call to finishOff() from printing the statistics information again.

The other way of calling finishOff() is from a shutdown hook (handler) set up when the JPanel is created:

    Runtime.getRuntime().addShutdownHook(new Thread() {
      public void run()
      { running = false;
        System.out.println("Shutdown hook executed");
        finishOff();
      }
    });

This code is normally called just before the application exits and is superfluous since finishOff() will have been executed. Its real benefit comes if the program terminates unexpectedly. The shutdown hook ensures that the statistics details are still reported in an abnormal exit situation.

This kind of defensive programming is often useful. For example, if the game state must be saved to an external file before the program terminates or if critical resources, such as files or sockets, must be properly closed.

Timings for UFS

Timing results for the UFS WormChase are given in Table 4-2.

Table 4-2. Average FPS/UPS rates for the UFS WormChase

Requested FPS

20

50

80

100

Windows 98

20/20

48/50

70/83

70/100

Windows 2000

18/20

19/50

18/83

18/100

Windows XP (1)

20/20

50/50

77/83

73/100

Windows XP (2)

20/20

50/50

68/83

69/100

WormChase on the Windows 2000 machine is the slowest, as usual, with marginally slower FPS values than the AFS version (it produces about 20 FPS). However, the poor performance is hidden by the high UPS number.

The Windows 98 and XP boxes produce reasonable to good frame rates when the requested FPS are 80 but are unable to go much faster. UFS frame rates are about 10 FPS slower than the AFS values at 80 FPS, which may be due to the larger rendering area. The UPS figures are unaffected.

Get Killer Game Programming in Java now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.