Tuesday, January 19, 2010

Other Animation Approaches









Other Animation Approaches


This chapter has been concerned with developing a threaded animation loop inside a JPanel. But other ways of implementing animation in Java exist, and I'll briefly consider two of them:


  • Using the Swing timer

  • Using the utility timer from java.util.timer


Both of them use a timer to trigger method calls at regular intervals. However, I'll present timing figures that show that the Swing timer doesn't have the necessary accuracy for my needs, while the utility timer is a possible alternative.



Swing Timer Animation


The Swing timer (in javax.swing.Timer) is used as the basis of animation examples in many Java textbooks.


The essential coding technique is to set a Timer object to "tick" every few milliseconds. Each tick sends an event to a specified ActionEvent listener, triggering a call to actionPerformed( ). actionPerformed( ) calls repaint( ) to send a repaint request to the JVM. Eventually, repainting reaches the paintComponent( ) method for the JPanel, which redraws the animation canvas. These stages are shown in Figure 2-1, which represents the test code in SwingTimerTest.java.



Figure 2-1. Swing timer animation



The SwingTimerTest class uses the Swing timer to draw the current average FPS values repeatedly into a JPanel. The period for the timer is obtained from the requested FPS given on the command line. The average FPS are calculated every second, based on FPS values collected over the previous 10 seconds.


main( ) reads in the user's required FPS and converts them to a period. It creates a JFrame and puts the SwingTimerPanel inside it.


The SwingTimerTest( ) constructor creates the timer and sends its "ticks" to itself:



new Timer(period, this).start( );



actionPerformed( ) wastes some time by calling a sillyTask( ) method that does a lot of looping and then requests a repaint:



public void actionPerformed(ActionEvent e)
{ sillyTask( );
repaint( );
}



paintComponent( ) updates the JPanel and records statistics:



public void paintComponent(Graphics g)
{
super.paintComponent(g);

// clear the background
g.setColor(Color.white);
g.fillRect (0, 0, PWIDTH, PHEIGHT);

// report average FPS
g.setColor(Color.black);
g.drawString("Average FPS: " + df.format(averageFPS), 10, 25);

reportStats( ); // record/report statistics
} // end of paintComponent( )



The most complicated part of this example is the statistics gathering done by reportStats( ). It's worth looking at the code since it appears again in Chapters 3 and 4.


reportStats( ) prints a line of statistics every second:



D>java SwingTimerTest 50
fps: 50; period: 20 ms
1 3.0099s 200.99% 50c 16.61 16.61 afps
1 2.7573s 175.73% 100c 17.34 16.98 afps
1 2.7344s 173.44% 150c 17.64 17.2 afps
1 2.746s 174.6% 200c 17.78 17.34 afps
1 2.7545s 175.45% 250c 17.85 17.45 afps
1 2.7522s 175.22% 300c 17.91 17.52 afps
1 2.7299s 172.99% 350c 17.96 17.59 afps
1 2.7581s 175.81% 400c 17.98 17.64 afps
: // more lines until ctrl-C is typed



The first line of the output lists the requested FPS and the corresponding period used by the timer. It's followed by multiple statistic lines, with a new line generated when the accumulated timer period reaches 1 second since the last line was printed.


Each statistics line presents six 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 paintComponent( ) since the program began, which should increase by the requested FPS value each second.


The fifth number is the current FPS, calculated by dividing the total number of calls by the total elapsed time since the program began. The sixth number is an average of the last 10 FPS numbers (or fewer, if 10 numbers haven't been calculated yet).


The reportStats( ) method, and its associated global variables, are shown here:



private static long MAX_STATS_INTERVAL = 1000L;
// record stats every 1 second (roughly)

private static int NUM_FPS = 10;
// number of FPS values stored to get an average

// used for gathering statistics
private long statsInterval = 0L; // in ms
private long prevStatsTime;
private long totalElapsedTime = 0L;

private long frameCount = 0;
private double fpsStore[];
private long statsCount = 0;
private double averageFPS = 0.0;

private DecimalFormat df = new DecimalFormat("0.##"); // 2 dp
private DecimalFormat timedf = new DecimalFormat("0.####"); //4 dp

private int period; // period between drawing in ms

private void reportStats( )
{
frameCount++;
statsInterval += period;

if (statsInterval >= MAX_STATS_INTERVAL) {
long timeNow = J3DTimer.getValue( );

long realElapsedTime = timeNow - prevStatsTime;
// time since last stats collection
totalElapsedTime += realElapsedTime;

long sInterval = (long)statsInterval*1000000L; // ms --> ns
double timingError =
((double)(realElapsedTime - sInterval)) / sInterval * 100.0;

double actualFPS = 0; // calculate the latest FPS
if (totalElapsedTime > 0)
actualFPS = (((double)frameCount / totalElapsedTime) * 1000000000L);
// store the latest FPS
fpsStore[ (int)statsCount%NUM_FPS ] = actualFPS;
statsCount = statsCount+1;

double totalFPS = 0.0; // total the stored FPSs
for (int i=0; i < NUM_FPS; i++)
totalFPS += fpsStore[i];


if (statsCount < NUM_FPS) // obtain the average FPS
averageFPS = totalFPS/statsCount;
else
averageFPS = totalFPS/NUM_FPS;

System.out.println(
timedf.format( (double) statsInterval/1000) + " " +
timedf.format((double) realElapsedTime/1000000000L) + "s " +
df.format(timingError) + "% " +
frameCount + "c " +
df.format(actualFPS) + " " +
df.format(averageFPS) + " afps" );

prevStatsTime = timeNow;
statsInterval = 0L; // reset
}
} // end of reportStats( )



reportStats( ) is called in paintComponent( ) after the timer has "ticked." This is recognized by incrementing frameCount and adding the period amount to statsInterval.


The FPS values are stored in the fpsStore[] array. When the array is full, new values overwrite the old ones by cycling around the array. The average FPS smooth over variations in the application's execution time.


Table 2-1 shows the reported average FPS on different versions of Windows when the requested FPSs were 20, 50, 80, and 100.


Table 2-1. Reported average FPS for SwingTimerTest

Requested FPS

20

50

80

100

Windows 98

18

18

18

18

Windows 2000

19

49

49

98

Windows XP

16

32

64

64



Each test was run three times on a lightly loaded machine, running for a few minutes. The results show a wide variation in the accuracy of the timer, but the results for the 80 FPS request are poor to downright awful in all cases. The Swing timer can't be recommended for high frame rate games.


The timer is designed for repeatedly triggering actions after a fixed period. However, the actual action frequency can drift because of extra delays introduced by the garbage collector or long-running game updates and rendering. It may be possible to code round this by dynamically adjusting the timer's period using setDelay( ).


The timer uses currentTimeMillis( ) internally, with its attendant resolution problems.


The official Java tutorial contains more information about the Swing timer and animation, located in the Swing trail in "Performing Animations" (http://java.sun.com/docs/books/tutorial/uiswing/painting/animation.html).




The Utility Timer


A timer is available in the java.util.Timer class. Instead of scheduling calls to actionPerformed( ), the run( ) method of a TimerTask object is invoked.


The utility timer provides more flexibility over scheduling than the Swing timer: Tasks can run at a fixed rate or a fixed period after a previous task. The latter approach is similar to the Swing timer and means that the timing of the calls can drift. In fixed-rate scheduling, each task is scheduled relative to the scheduled execution time of the initial task. If a task is delayed for any reason (such as garbage collection), two or more tasks will occur in rapid succession to catch up.


The most important difference between javax.Swing.Timer and java.util.Timer is that the latter does not run its tasks in the event dispatching thread. Consequently, the test code employs three classes: one for the timer, consisting of little more than a main( ) function, a subclass of TimerTask for the repeated task, and a subclass of JPanel as a canvas.


These components are shown in Figure 2-2, which represents the test code in UtilTimerTest.java.



Figure 2-2. Utility timer animation



The timer schedules the TimerTask at a fixed rate:



MyTimerTask task = new MyTimerTask(...);
Timer t = new Timer( );
t.scheduleAtFixedRate(task, 0, period);



The TimerTask run( ) method wastes some time looping in sillyTask( ) and then repaints its JPanel:



class MyTimerTask extends TimerTask
{
// global variables and other methods

public void run( )
{ sillyTask( );
pp.repaint( );
}

private void sillyTask( )
{...}

} // end of MyTimerTask



The JPanel is subclassed to paint the current average FPS values onto the canvas, and to call reportStats( ) to record timing information. Its paintComponent( ) and reportStats( ) are the same as in SwingTimerTest.


Table 2-2 shows the reported average FPS on different versions of Windows, when the requested FPSs are 20, 50, 80, and 100.


Table 2-2. Reported average FPSs for UtilTimerTest

Requested FPS

20

50

80

100

Windows 98

20

47

81

94

Windows 2000

20

50

83

99

Windows XP

20

50

83

95



The average FPS are excellent, which is somewhat surprising since currentTimeMillis( ) is employed in the timer's scheduler. The average hides that it takes 1 to 2 minutes for the frame rate to rise towards the average. Also, JVM garbage collection reduces the FPS for a few seconds each time it occurs.


The average FPS for a requested 80 FPS are often near 83 due to a quirk of my coding. The frame rate is converted to an integer period using (int) 1000/80 == 12 ms. Later, this is converted back to a frame rate of 1000/12, which is 83.333.


The drawback of the utility timer is that the details of the timer and sleeping operations are mostly out of reach of the programmer and so, are not easily modified, unlike the threaded animation loop.


The Java tutorial contains information about the utility timer and TimerTasks in the threads trail under the heading "Using the Timer and TimerTask Classes" (http://java.sun.com/docs/books/tutorial/essential/threads/timer.html).










    No comments: