The Game Panel


The WormPanel class is similar to the GamePanel class developed in Chapter 2, with some additional methods for drawing the game scene. WormPanel contains an extended version of the reportStats( ) method used for timing the Swing and utility timers in Chapter 2, called printStats( ). Its principal extension is to report the average UPS (updates per second) in addition to the average FPS.

A class diagram showing all the WormPanel methods is given in Figure 3-4.

The WormPanel constructor sets up the game components and initializes timing elements:

     public WormPanel(WormChase wc, long period)     {       wcTop = wc;       this.period = period;       setBackground(Color.white);       setPreferredSize( new Dimension(PWIDTH, PHEIGHT));       setFocusable(true);       requestFocus( );    // now has focus, so receives key events       readyForTermination( );       // create game components       obs = new Obstacles(wcTop);       fred = new Worm(PWIDTH, PHEIGHT, obs);       addMouseListener( new MouseAdapter( ) {         public void mousePressed(MouseEvent e)

Figure 3-4. WormPanel methods in detail


         { testPress(e.getX( ), e.getY( )); }       });       // set up message font       font = new Font("SansSerif", Font.BOLD, 24);       metrics = this.getFontMetrics(font);       // initialise timing elements       fpsStore = new double[NUM_FPS];       upsStore = new double[NUM_FPS];       for (int i=0; i < NUM_FPS; i++) {         fpsStore[i] = 0.0;         upsStore[i] = 0.0;       }     }  // end of WormPanel( )

The time period intended for each frame (in nanoseconds) is passed to WormPanel from WormChase and stored in a global variable. readyForTermination( ) is the same as in Chapter 2: a KeyListener monitors the input for termination characters (e.g., Ctrl-C), then sets the running Boolean to false.

The message font is used to report the score when the game ends. fpsStore[] and upsStore[] are global arrays holding the previous ten FPS and UPS values calculated by the statistics code.

User Input

The testPress( ) method handles mouse presses on the canvas, which will be aimed at the worm's red head. If the press is sufficiently near to the head, then the game is won. If the press touches the worm's body (the black circles), then nothing occurs; otherwise, an obstacle is added to the scene at that (x, y) location:

     private void testPress(int x, int y)     // is (x,y) near the head or should an obstacle be added?     {       if (!isPaused && !gameOver) {         if (fred.nearHead(x,y)) {   // was mouse press near the head?           gameOver = true;           score = (40 - timeSpentInGame) + 40 - obs.getNumObstacles( ));               // hack together a score         }         else {   // add an obstacle if possible           if (!fred.touchedAt(x,y))   // was worm's body not touched?             obs.add(x,y);         }       }     }  // end of testPress( )

testPress( ) starts by testing isPaused and gameOver. If isPaused is true then the game is paused, and mouse presses should be ignored. Similarly, if the game is over (gameOver == true), then the input is disregarded.

WormChase's WindowListener methods respond to window events by calling the following methods in WormPanel to affect the isPaused and running flags:

     public void resumeGame( )     // called when the JFrame is activated / deiconified     {  isPaused = false;  }     public void pauseGame( )     // called when the JFrame is deactivated / iconified     { isPaused = true;   }     public void stopGame( )     // called when the JFrame is closing     {  running = false;   }

As discussed in Chapter 2, pausing and resumption don't utilize the THRead wait( ) and notify( ) methods to affect the animation thread.

The Animation Loop

For the sake of completeness, I include the run( ) method from WormPanel. The parts of it which differ from the animation loop in the section "Separating Updates from Rendering" in Chapter 2 are marked in bold:

     public void run( )     /* The frames of the animation are drawn inside the while loop. */     {       long beforeTime, afterTime, timeDiff, sleepTime;       long overSleepTime = 0L;       int noDelays = 0;       long excess = 0L;                gameStartTime = J3DTimer.getValue( );       prevStatsTime = gameStartTime;       beforeTime = gameStartTime;       running = true;       while(running) {         gameUpdate( );         gameRender( );         paintScreen( );         afterTime = J3DTimer.getValue( );         timeDiff = afterTime - beforeTime;         sleepTime = (period - timeDiff) - overSleepTime;         if (sleepTime > 0) {   // some time left in this cycle           try {             Thread.sleep(sleepTime/1000000L);  // nano -> ms           }           catch(InterruptedException ex){}           overSleepTime = (J3DTimer.getValue( ) - afterTime) - sleepTime;         }         else {    // sleepTime <= 0; frame took longer than the period           excess -= sleepTime;  // store excess time value           overSleepTime = 0L;           if (++noDelays >= NO_DELAYS_PER_YIELD) {             Thread.yield( );   // give another thread a chance to run             noDelays = 0;           }         }         beforeTime = J3DTimer.getValue( );         /* If frame animation is taking too long, update the game state            without rendering it, to get the updates/sec nearer to            the required FPS. */         int skips = 0;         while((excess > period) && (skips < MAX_FRAME_SKIPS)) {           excess -= period;           gameUpdate( );    // update state but don't render           skips++;         }              framesSkipped += skips;         storeStats( );       }            printStats( );       System.exit(0);   // so window disappears     } // end of run( )

The global variables, gameStartTime and prevStatsTime, are utilized in the statistics calculations, as is the frameSkipped variable. frameSkipped holds the total number of skipped frames since the last UPS calculation in storeStats( ). printStats( ) reports selected numbers and statistics at program termination time.

Statistics Gathering

storeStats( ) is a close relative of the reportStats( ) method of the section "Swing Timer Animation" in Chapter 2. Again for completeness, I list the method here, as well as the new global variables which it manipulates in addition to the ones described in Chapter 2. The parts of reportStats( ), which are new (or changed), are marked in bold:

     // used for gathering statistics         : // many, see "Swing Timer Animation" section, chapter 2     private long gameStartTime;     private int timeSpentInGame = 0;    // in seconds     private long framesSkipped = 0L;     private long totalFramesSkipped = 0L;     private double upsStore[];     private double averageUPS = 0.0;     private void storeStats( )     {       frameCount++;       statsInterval += period;       if (statsInterval >= MAX_STATS_INTERVAL) {         long timeNow = J3DTimer.getValue( );         timeSpentInGame =           (int) ((timeNow - gameStartTime)/1000000000L);  // ns-->secs         wcTop.setTimeSpent( timeSpentInGame );         long realElapsedTime = timeNow - prevStatsTime;              // time since last stats collection         totalElapsedTime += realElapsedTime;         double timingError = (double)              (realElapsedTime-statsInterval) / statsInterval)*100.0;         totalFramesSkipped += framesSkipped;         double actualFPS = 0;     // calculate the latest FPS and UPS         double actualUPS = 0;         if (totalElapsedTime > 0) {           actualFPS = (((double)frameCount / totalElapsedTime) *                                                    1000000000L);           actualUPS = (((double)(frameCount + totalFramesSkipped) /07                        totalElapsedTime) * 1000000000L);         }         // store the latest FPS and UPS         fpsStore[ (int)statsCount%NUM_FPS ] = actualFPS;         upsStore[ (int)statsCount%NUM_FPS ] = actualUPS;         statsCount = statsCount+1;         double totalFPS = 0.0;     // total the stored FPSs and UPSs         double totalUPS = 0.0;         for (int i=0; i < NUM_FPS; i++) {           totalFPS += fpsStore[i];           totalUPS += upsStore[i];         }         if (statsCount < NUM_FPS) { // obtain the average FPS and UPS           averageFPS = totalFPS/statsCount;           averageUPS = totalUPS/statsCount;         }         else {           averageFPS = totalFPS/NUM_FPS;           averageUPS = totalUPS/NUM_FPS;         }     /*        System.out.println(          timedf.format( (double) statsInterval/1000000000L) + " " +          timedf.format((double) realElapsedTime/1000000000L)+"s "+          df.format(timingError) + "% " +          frameCount + "c " +          framesSkipped + "/" + totalFramesSkipped + " skip; " +          df.format(actualFPS) + " " + df.format(averageFPS)+" afps; " +          df.format(actualUPS) + " " + df.format(averageUPS)+" aups" );     */         framesSkipped = 0;         prevStatsTime = timeNow;         statsInterval = 0L;   // reset       }     }  // end of storeStats( )

gameStartTime is used to calculate timeSpentInGame, which WormPanel reports to the player by writing to the time text field in the top-level window. As in Chapter 2, the statsInterval value is a sum of the requested periods adding up to MAX_STATS_INTERVAL. The difference is that the period is measured in nanoseconds here (due to the use of the Java 3D timer). This means that the timingError calculation doesn't need to translate the statsInterval value from milliseconds to nanoseconds before using it.

The main additions to storeStats( ) are the calculation of UPS values, the storage in the upsStore[] array, and the use of that array to calculate an average UPS. The UPS value comes from these statements:

     totalFramesSkipped += framesSkipped;     actualUPS = (((double)(frameCount + totalFramesSkipped) /                          totalElapsedTime) * 1000000000L);

frameCount is the total number of rendered frames in the game so far, which is added to the total number of skipped frames.

A skipped frame is a game state update which wasn't rendered.


The total is equivalent to the total number of game updates. The division by the total elapsed time and multiplication by 1,000,000,000 gives the UPS.

The large println( ) call in storeStats( ) produces a line of statistics. It is commented out since it is intended for debugging purposes. Here is the typical output:

     >java WormChase 80     fps: 80; period: 12 ms     1.008 1.2805s 27.03% 84c 22/22 skip; 65.6 65.6 afps; 82.78 82.78 aups     1.008 1.0247s 1.66% 168c 2/24 skip; 72.88 69.24 afps; 83.29 83.04 aups     1.008 1.0287s 2.06% 252c 1/25 skip; 75.59 71.36 afps; 83.08 83.05 aups     1.008 1.0107s 0.27% 336c 0/25 skip; 77.34 72.85 afps; 83.09 83.06 aups     1.008 1.0087s 0.07% 420c 0/25 skip; 78.46 73.97 afps; 83.13 83.07 aups     1.008 1.0087s 0.07% 504c 0/25 skip; 79.22 74.85 afps; 83.15 83.09 aups     1.008 1.0087s 0.07% 588c 0/25 skip; 79.77 75.55 afps; 83.17 83.1 aups     1.008 1.0088s 0.08% 672c 0/25 skip; 80.19 76.13 afps; 83.18 83.11 aups     Frame Count/Loss: 707 / 25     Average FPS: 76.13     Average UPS: 83.11     Time Spent: 8 secs     Boxes used: 0

Each statistics line presents ten numbers. The first three relate to the execution time. The first number is the accumulated timer period since the last output, which will be close to one second. The second number is the actual elapsed time, measured with the Java 3D timer, and the third value is the percentage error between the two numbers.

The fourth number is the total number of calls to run( ) since the program began, which should increase by the FPS value each second. The fifth and sixth numbers (separated by a /) are the frames skipped in this interval and the total number of frames skipped since the game began. A frame skip is a game update without a corresponding render. The seventh and eighth numbers are the current UPS and average. The ninth and tenth numbers are the current FPS and the average.

The output after the statistics lines comes from printStats( ), which is called as run( ) is finishing. It gives a briefer summary of the game characteristics:

     private void printStats( )     {       System.out.println("Frame Count/Loss: " + frameCount +                                  " / " + totalFramesSkipped);       System.out.println("Average FPS: " + df.format(averageFPS));       System.out.println("Average UPS: " + df.format(averageUPS));       System.out.println("Time Spent: " + timeSpentInGame + " secs");       System.out.println("Boxes used: " + obs.getNumObstacles( ));     }  // end of printStats( )

Drawing the Canvas

The behavior specific to the WormChase game originates in two method calls at the start of the animation loop:

     while(running) {       gameUpdate( );    // game state is updated       gameRender( );    // render to a buffer       paintScreen( );   // paint with the buffer       // sleep a bit       // perhaps call gameUpdate( )       // gather statistics     }

gameUpdate( ) changes the game state every frame. For WormChase, this consists of requesting that the worm (called fred) moves:

     private void gameUpdate( )     { if (!isPaused && !gameOver)         fred.move( );     }

The details of the move are left to fred in the usual object-oriented style. No move will be requested if the game is paused or has finished.

gameRender( ) draws the game elements (e.g., the worm and obstacles) to an image acting as a buffer:

     private void gameRender( )     {       if (dbImage == null){         dbImage = createImage(PWIDTH, PHEIGHT);         if (dbImage == null) {           System.out.println("dbImage is null");           return;         }         else           dbg = dbImage.getGraphics( );       }       // clear the background       dbg.setColor(Color.white);       dbg.fillRect (0, 0, PWIDTH, PHEIGHT);       dbg.setColor(Color.blue);       dbg.setFont(font);       // report average FPS and UPS at top left       dbg.drawString("Average FPS/UPS: " + df.format(averageFPS) +                ", " + df.format(averageUPS), 20, 25);       dbg.setColor(Color.black);           // draw game elements: the obstacles and the worm       obs.draw(dbg);       fred.draw(dbg);       if (gameOver)         gameOverMessage(dbg);     }  // end of gameRender( )

gameRender( ) begins in the manner described in Chapter 2: the first call to the method causes the image and its graphics context to be created, and the following lines draw the background, game elements, and finally the "game over" message. The ordering is important: things further back in the game are drawn first.

A useful debugging addition to gameRender( ) is to draw the average FPS and UPS values on the canvas; these operations would normally be commented out when the coding is completed.


The actual game elements are drawn by passing draw requests onto the worm and the obstacles objects:

     obs.draw(dbg);     fred.draw(dbg);

This approach relieves the game panel of drawing work and moves the drawing activity to the object responsible for the game component's behavior.

The gameOverMessage( ) method uses font metrics and the length of the message to place it in the center of the drawing area. Typical output is shown in Figure 3-5.

Figure 3-5. Game Over message


As the number of obstacles indicates, a frame rate of 80 FPS makes it very difficult for the player to hit the worm.

paintScreen( ) actively renders the buffer image to the JPanel canvas and is unchanged from the section "Converting to Active Rendering" in Chapter 2:

     private void paintScreen( )     // use active rendering to put the buffered image on-screen     {       Graphics g;       try {         g = this.getGraphics( );         if ((g != null) && (dbImage != null))           g.drawImage(dbImage, 0, 0, null);         Toolkit.getDefaultToolkit( ).sync( );  // sync the display on some systems         g.dispose( );       }       catch (Exception e)       { System.out.println("Graphics context error: " + e);  }     } // end of paintScreen( )



Killer Game Programming in Java
Killer Game Programming in Java
ISBN: 0596007302
EAN: 2147483647
Year: 2006
Pages: 340

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net