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 ResolutionTimer 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.
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.
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 TimersJ2SE 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 TimersIt'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.
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.
Measuring Timer ResolutionThe 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 resolutionimport 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 Perfimport 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 AlertThere'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:
|