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