The animation loop in run( ) depends on a good timer and the accuracy of the sleep( ) call. The previous major section dealt with alternatives to currentTimeMillis( ). In this section, I consider ways of improving the sleep( ) code in run( ), so the required frame rate is consistently achieved. The SleepAcc class measures sleep accuracy. Example 2-4 calls sleep( ) with increasingly small values and measures the actual sleep time using the Java 3D timer. Example 2-4. Measuring sleep( ) accuracyimport java.text.DecimalFormat; import com.sun.j3d.utils.timer.J3DTimer; public class SleepAcc { private static DecimalFormat df; public static void main(String args[]) { df = new DecimalFormat("0.##"); // 2 dp // test various sleep values sleepTest(1000); sleepTest(500); sleepTest(200); sleepTest(100); sleepTest(50); sleepTest(20); sleepTest(10); sleepTest(5); sleepTest(1); } // end of main( ) private static void sleepTest(int delay) { long timeStart = J3DTimer.getValue( ); try { Thread.sleep(delay); } catch(InterruptedException e) {} double timeDiff = ((double)(J3DTimer.getValue( ) - timeStart))/(1000000L); double err = ((delay - timeDiff)/timeDiff) * 100; System.out.println("Slept: " + delay + " ms J3D: " + df.format(timeDiff) + " ms err: " + df.format(err) + " %" ); } // end of sleepTest( ) } // end of SleepAcc class The difference between the requested and actual sleep delay is negligible for times of 50 ms or more and gradually increases to a +/-10 to 20 percent error at 1 ms. A typical run is: D>java SleepAcc Slept: 1000 ms J3D: 999.81 ms err: 0.02 % Slept: 500 ms J3D: 499.54 ms err: 0.09 % Slept: 200 ms J3D: 199.5 ms err: 0.25 % Slept: 100 ms J3D: 99.56 ms err: 0.44 % Slept: 50 ms J3D: 49.59 ms err: 0.82 % Slept: 20 ms J3D: 20.53 ms err: -2.59 % Slept: 10 ms J3D: 10.52 ms err: -4.91 % Slept: 5 ms J3D: 5.42 ms err: -7.78 % Slept: 1 ms J3D: 1.15 ms err: -13.34 % : // more lines until ctrl-C is typed The reason for this inaccuracy is probably due to the complexity of the operation, involving the suspension of a thread and context switching with other activities. Even after the sleep time has finished, a thread has to wait to be selected for execution by the thread scheduler. How long it has to wait depends on the overall load of the JVM (and OS) at that moment. sleep( )'s implementation varies between operating systems and different versions of Java, making analysis difficult. Under Windows 98 and J2SE 1.4.2, sleep( ) utilizes a large native function (located in jvm.dll), which employs the Windows kernel sleep( ) function with a reported accuracy of 1 ms. The conclusion is that I should extend the animation loop to combat sleep( )'s inaccuracies. Handling Sleep InaccuraciesThis version of run( ) in this section revises the previous one in three main ways:
Here's the updated method: private static final int NO_DELAYS_PER_YIELD = 16; /* Number of frames with a delay of 0 ms before the animation thread yields to other running threads. */ public void run( ) /* Repeatedly update, render, sleep so loop takes close to period nsecs. Sleep inaccuracies are handled. The timing calculation use the Java 3D timer. */ { long beforeTime, afterTime, timeDiff, sleepTime; long overSleepTime = 0L; int noDelays = 0; beforeTime = J3DTimer.getValue( ); 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 overSleepTime = 0L; if (++noDelays >= NO_DELAYS_PER_YIELD) { Thread.yield( ); // give another thread a chance to run noDelays = 0; } } beforeTime = J3DTimer.getValue( ); } System.exit(0); } // end of run( ) If the sleep( ) call sleeps for 12 ms instead of the desired 10 ms, then overSleepTime will be assigned 2 ms. On the next iteration of the loop, this value will be deducted from the sleep time, reducing it by 2 ms. In this way, sleep inaccuracies are corrected. If the game update and rendering steps take longer than the iteration period, then sleepTime will have a negative value and this iteration will not include a sleep stage. This causes the noDelays counter to be incremented, and when it reaches NO_DELAYS_PER_YIELD, yield( ) will be called. This allows other threads to execute if they need to and avoids the use of an arbitrary sleep period in run( ). The switch to the Java 3D timer is mostly a matter of changing the calls to System.currentTimeMillis( ) to J3DTimer.getValue( ). Time values change from milliseconds to nanoseconds, which motivates the change to long variables. Also, the sleep time must be converted from nanoseconds to milliseconds before calling sleep( ), or I'll be waiting a long time for the game to wake up. If you prefer to use System.nanoTime( ) from J2SE 5.0, you can globally search and replace, changing every J3DTimer.getValue( ) call to System.nanoTime( ). You don't have to import the Java 3D packages if you choose this approach. |