FPS and Sleeping for Varying Times


A weakness of the animation loop is that its execution speed is unconstrained. On a slow machine, it may loop 20 times per second; the same code on a fast machine may loop 80 times, making the game progress four times faster and perhaps making it unplayable. The loop's execution speed should be about the same on all platforms.

A popular measure of how fast an animation progresses is frames per second (FPS). For GamePanel, a frame corresponds to a single pass through the update-render-sleep loop inside run( ). Therefore, the desired 100 FPS imply that each iteration of the loop should take 1000/100 == 10 ms. This iteration time is stored in the period variable in GamePanel.

The use of active rendering makes it possible to time the update and render stages of each iteration. Subtracting this value from period gives the sleep time required to maintain the desired FPS. For instance, 100 FPS mean a period of 10 ms, and if the update/render steps take 6 ms, then sleep( ) should be called for 4 ms. Of course, this is different on each platform, so must be calculated at runtime.

The following modified run( ) method includes timing code and the sleep time calculation:

     public void run( )     /* Repeatedly: update, render, sleep so loop takes close        to period ms */     {       long beforeTime, timeDiff, sleepTime;        beforeTime = System.currentTimeMillis( );       running = true;       while(running) {         gameUpdate( );         gameRender( );         paintScreen( );         timeDiff = System.currentTimeMillis( ) - beforeTime;         sleepTime = period - timeDiff;   // time left in this loop         if (sleepTime <= 0)  // update/render took longer than period           sleepTime = 5;    // sleep a bit anyway         try {           Thread.sleep(sleepTime);  // in ms         }         catch(InterruptedException ex){}         beforeTime = System.currentTimeMillis( );       }       System.exit(0);     } // end of run( )

timeDiff holds the execution time for the update and render steps, which becomes part of the sleep time calculation.

One problem with this approach is if the update and drawing take longer than the specified period, then the sleep time becomes negative. The solution to this problem is to set the time to some small value to make the thread sleep a bit. This permits other threads and the JVM to execute if they wish. Obviously, this solution is still problematic: Why use 5 ms and not 2 or 20?

A more subtle issue is the resolution and accuracy of the timer and sleep operations (currentTimeMillis( ) and sleep( )). If they return inaccurate values, then the resulting FPS will be affected. These are such important problems that I'm going to spend the rest of this section looking at ways to ensure the timer has good resolution and the next major section considering sleep accuracy.

Timer Resolution

Timer resolution, or granularity, is the amount of time that must separate two timer calls so that different values are returned. For instance, what is the value of diff in the code fragment below?

     long t1 = System.currentTimeMillis( );     long t2 = System.currentTimeMillis( );     long diff = t2 - t1;  // in ms

The value depends on the resolution of currentTimeMillis( ), which unfortunately depends on the OS.

To be more precise, this depends on the resolution of the standard clock interrupt.


In Windows 95 and 98, the resolution is 55 ms, which means that repeated calls to currentTimeMillis( ) will only return different values roughly every 55 ms.

In the animation loop, the overall effect of poor resolution causes the animation to run slower than intended and reduces the FPS. This is due to the timeDiff value, which will be set to 0 if the game update and rendering time is less than 55 ms. This causes the sleep time to be assigned the iteration period value, rather than a smaller amount, causing each iteration to sleep longer than necessary.

To combat this, the minimum iteration period in GamePanel should be greater than 55 ms, indicating an upper limit of about 18 FPS. This frame rate is widely considered inadequate for games since the slow screen refresh appears as excessive flicker.

On Windows 2000, NT, and XP, currentTimeMillis( ) has a resolution of 10 to 15 ms, making it possible to obtain 67 to 100 FPS. This is considered acceptable to good for games. The Mac OS X and Linux have timer resolutions of 1 ms, which is excellent.

What's a Good FPS?

It's worth taking a brief diversion to consider what FPS values make for a good game.

A lower bound is dictated by the human eye and the critical flicker frequency (CFF), which is the rate at which a flickering light appears to be continuous. This occurs somewhere between 10 and 50 Hz, depending on the intensity of the light (translating into 10 to 50 FPS). For larger images, the position of the user relative to the image affects the perceived flicker, as well as the color contrasts and amount of detail in the picture.

Movies are shown at 24 FPS, but this number is somewhat misleading since each frame is projected onto the screen twice (or perhaps three times) by the rapid opening and closing of the projector's shutter. Thus, the viewer is actually receiving 48 (or 72) image flashes per second.

An upper bound for a good FPS values are the monitor refresh rate. This is typically 70 to 90 Hz, i.e., 70 to 90 FPS. A program doesn't need to send more frames per second than the refresh rate to the graphics card as the extra frames will not be displayed. In fact, an excessive FPS rate consumes needless CPU time and stretches the display card.

My monitor refreshes at 85 Hz, making 80 to 85 FPS the goal of the code here. This is the best FPS values since they match the monitor's refresh rate. Games often report higher values of 100 or more, but they're probably really talking about game UPS, which I'll consider a bit later on.


Am I Done Yet? (Nope)

Since the aim is about 85 FPS, then is the current animation loop sufficient for the job? Do I have to complicate it any further? For modern versions of Windows (e.g., NT, 2000, XP), the Mac, and Linux, their average/good timer resolutions mean that the current code is probably adequate.

The main problem is the resolution of the Windows 98 timer (55 ms; 18.2 FPS). Google Zeitgeist, a web site that reports interesting search patterns and trends taken from the Google search engine (http://www.google.com/press/zeitgeist.html), lists operating systems used to access Google. Windows 98 usage stood at about 16 percent in June 2004, having dropped from 29 percent the previous September. The winner was XP, gaining ground from 38 percent to 51 percent in the same interval.

If I'm prepared to extrapolate OS popularity from these search engine figures, then Windows 98 is rapidly on its way out. By the time you read this, sometime in 2005, Windows 98's share of the OS market will probably be below 10 percentit may be acceptable to ignore the slowness of its timer since few people will be using it.

Well, I'm not going to give up on Windows 98 since I'm still using it at home. Also, it's well worth investigating other approaches to see if they can give better timer resolution. This will allow us to improve the frame rate and to correct for errors in the sleep time and updates per second, both discussed in later sections.

Improved J2SE Timers

J2SE 1.4.2 has a microsecond accurate timer hidden in the undocumented class sun.misc.Perf. The diff calculation can be expressed as follows:

     Perf perf = Perf.getPerf( );     long countFreq = perf.highResFrequency( );     long count1 = perf.highResCounter( );     long count2 = perf.highResCounter( );     long diff =  (count2 - count1) * 1000000000L / countFreq ;                        // in nanoseconds

Perf is not a timer but a high-resolution counter, so it is suitable for measuring time intervals. highResCounter( ) returns the current counter value, and highResFrequency( ), the number of counts made per second. Perf's typical resolution is a few microseconds (2 to 6 microseconds on different versions of Windows).

My timer problems are solved in J2SE 5.0, with its System.nanoTime( ) method, which can be used to calculate time intervals in a similar way to the Perf timer. As the name suggests, nanoTime( ) returns an elapsed time in nanoseconds:

     long count1 = System.nanoTime( );     long count2 = System.nanoTime( );     long diff = (count2 - count1);   // in nanoseconds

The resolution of nanoTime( ) on Windows is similar to the Perf timer (1 to 6 microseconds).

Also, J2SE 5.0's new java.util.concurrent package for concurrent programming includes a TimeUnit class that can measure down to the nanosecond level.

Using Non-J2SE Timers

It's possible to employ a high resolution timer from one of Java's extensions. The Java Media Framework (JMF) timer is an option but, since the majority of this book is about Java 3D, I'll use the J3DTimer class.

The diff calculation recoded using the Java 3D timer becomes:

     long t1 = J3DTimer.getValue( );     long t2 = J3DTimer.getValue( );     long diff =  t2 - t1 ;   // in nanoseconds

getValue( ) returns a time in nanoseconds (ns). On Windows 98, the Java 3D timer has a resolution of about 900 ns, which improves to under 300 ns on my test XP box.

A drawback of using Java 3D is the need to install it in addition to J2SE, but it's quite straightforward. Sun's top-level web page for Java 3D is at http://java.sun.com/products/java-media/3D/. With a little work, the timer can be extracted from the rest of Java 3D, reducing the amount of software that needs to be installed. (See Appendix A for details.)


Another approach is to use a timer from a game engine. My favourite is Meat Fighter by Michael Birken (http://www.meatfighter.com). The StopWatchSource class provides a static method, getStopWatch( ), which uses the best resolution timer available in your system; it considers currentTimeMillis( ) and the JMF and Java 3D timers, if present. On Windows, Meat Fighter includes a 40-KB DLL containing a high-resolution timer. The GAGE timer is a popular choice (http://java.dnsalias.com/) and can employ J2SE 5.0's nanoTime( ) if it's available.

The main issue with using a timer that isn't part of Java's standard libraries is how to package it up with a game and ensure it can be easily installed on someone else's machine. The appendixes explain how to write installation routines for games that use the Java 3D timer.

Choosing to use a non-J2SE timer is a good choice for portability reasons. Code using nanoTime( ) is not backward-compatible with earlier versions of J2SE, which means you have to ensure the gamer has J2SE 5.0 installed to play your game.


Measuring Timer Resolution

The TimerRes class in Example 2-2 offers a simple way to discover the resolution of the System, Perf, and Java 3D timers on your machine. Perf is only available in J2SE 1.4.2, and Java 3D must be installed for J3DTimer.getResolution( ) to work.

Example 2-2. Testing timer resolution
 import com.sun.j3d.utils.timer.J3DTimer; public class TimerRes {   public static void main(String args[])   { j3dTimeResolution( );     sysTimeResolution( );     perfTimeResolution( );   }   private static void j3dTimeResolution( )   { System.out.println("Java 3D Timer Resolution: " +                     J3DTimer.getResolution( ) + " nsecs");   }   private static void sysTimeResolution( )   {     long total, count1, count2;     count1 = System.currentTimeMillis( );     count2 = System.currentTimeMillis( );     while(count1 == count2)       count2 = System.currentTimeMillis( );     total = 1000L * (count2 - count1);     count1 = System.currentTimeMillis( );     count2 = System.currentTimeMillis( );     while(count1 == count2)       count2 = System.currentTimeMillis( );     total += 1000L * (count2 - count1);     count1 = System.currentTimeMillis( );     count2 = System.currentTimeMillis( );     while(count1 == count2)       count2 = System.currentTimeMillis( );     total += 1000L * (count2 - count1);     count1 = System.currentTimeMillis( );     count2 = System.currentTimeMillis( );     while(count1 == count2)       count2 = System.currentTimeMillis( );     total += 1000L * (count2 - count1);     System.out.println("System Time resolution: " +                             total/4 + " microsecs");   } // end of sysTimeResolution( )   private static void perfTimeResolution( )   {     StopWatch sw = new StopWatch( );     System.out.println("Perf Resolution: " +                     sw.getResolution( ) + " nsecs");     sw.start( );     long time = sw.stop( );     System.out.println("Perf Time " + time  + " nsecs");   } } // end of TimerRes class 

The output for TimerRes running on a Windows 98 machine is shown below. The drawback of using currentTimeMillis( ) is quite apparent.

     > java TimerRes     Java 3D Timer Resolution: 838 nsecs     System Time resolution: 55000 microsecs     Perf Resolution: 5866 nsecs     Perf Time 19276 nsecs

StopWatch is my own class (shown in Example 2-3) and wraps up the Perf counter to make it easier to use as a kind of stopwatch. A geTResolution( ) method makes getting results easier.

Example 2-3. A wrapper utility for Perf
 import sun.misc.Perf;    // only in J2SE 1.4.2 public class StopWatch {   private Perf hiResTimer;   private long freq;   private long startTime;   public StopWatch( )   { hiResTimer = Perf.getPerf( );     freq = hiResTimer.highResFrequency( );   }   public void start( )   {  startTime = hiResTimer.highResCounter( ); }   public long stop( )   //  return the elapsed time in nanoseconds   {  return (hiResTimer.highResCounter( ) -                            startTime)*1000000000L/freq;  }   public long getResolution( )   // return counter resolution in nanoseconds   {     long diff, count1, count2;     count1 = hiResTimer.highResCounter( );     count2 = hiResTimer.highResCounter( );     while(count1 == count2)       count2 = hiResTimer.highResCounter( );     diff = (count2 - count1);     count1 = hiResTimer.highResCounter( );     count2 = hiResTimer.highResCounter( );     while(count1 == count2)       count2 = hiResTimer.highResCounter( );     diff += (count2 - count1);     count1 = hiResTimer.highResCounter( );     count2 = hiResTimer.highResCounter( );     while(count1 == count2)       count2 = hiResTimer.highResCounter( );     diff += (count2 - count1);     count1 = hiResTimer.highResCounter( );     count2 = hiResTimer.highResCounter( );     while(count1 == count2)       count2 = hiResTimer.highResCounter( );     diff += (count2 - count1);     return (diff*1000000000L)/(4*freq);   } // end of getResolution( ) } // end of StopWatch class 

The start( ) and stop( ) methods add a small overhead to the counter, as illustrated in the perfTimeResolution( ) method in TimerRes. The smallest time that can be obtained is around 10 to 40 ms, compared to the resolution of around 2 to 6 ms.

The resolution of System.nanoTime( ) can be measured using a variant of sysTimeResolution( ).

     private static void nanoTimeResolution( )     {       long total, count1, count2;       count1 = System.nanoTime( );       count2 = System.nanoTime( );       while(count1 == count2)         count2 = System.nanoTime( );       total = (count2 - count1);       count1 = System.nanoTime( );       count2 = System.nanoTime( );       while(count1 == count2)         count2 = System.nanoTime( );       total += (count2 - count1);       count1 = System.nanoTime( );       count2 = System.nanoTime( );       while(count1 == count2)         count2 = System.nanoTime( );       total += (count2 - count1);       count1 = System.nanoTime( );       count2 = System.nanoTime( );       while(count1 == count2)         count2 = System.nanoTime( );       total += (count2 - count1);       System.out.println("Nano Time resolution: " + total/4 + " ns");     } // end of nanoTimeResolution( )

The output of the method is in nanoseconds, e.g., 5866 ns for Windows 98 (about 6 ms). Here are values for other operating systems: 440 ns on Mac OS X, 1,000 ns on Linux, and 1,116 ns on Windows 2000 Pro.

Java 3D Timer Bug Alert

There's a rarely occurring bug in the J3DTimer class: J3DTimer.getResolution( ) and J3DTimer.getValue( ) return 0 on some versions of Windows XP and Linux. This can be checked by running the TimerRes application from the last section, or by executing this snippet of code:

     System.out.println("J3DTimer resolution (ns): " + J3DTimer.getResolution( ));     System.out.println("Current time (ns): " + J3DTimer.getValue( ));

If there's a problem, both numbers will be 0.

This bug's history can be found at https://java3d.dev.java.net/issues/show_bug.cgi?id=13 and has been fixed in the bug release version of Java 3D 1.3.2, which is "experimental" at the moment (December 2004), but will have been finished by the time you read this. It can be downloaded from https://java3d.dev.java.net/.

Here are two other solutions:

  • Switch back to System.currentTimeMillis( ), which is fast enough on Windows XP.

  • If you're using J2SE 5.0, then replace all the calls to J3DTimer.getValue( ) with System.nanoTime( ).



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