Perceived Performance

Sometimes performance has little to do with raw numbers or benchmarks, but instead with what the user perceives. Does it look like it's running smoothly? One idea is to make sure the game doesn't look like it's "frozen" when it's really just communicating with a server or loading a lot of files. Put long-running tasks such as these in a separate thread. Also give the user visual notification if something is happening in the background, by using progress bars or wait cursors. For example, give the user something to do while the game is loading, such as an interesting animation, or even let the user play a simple pong game.

Timer Resolution

One of the biggest factors in perceived performance is the granularity of the system timer. Unfortunately on Windows machines, timer resolution isn't very accurate. Here are the different timer granularities of System.currentTimeMillis() for different operating systems:

Windows 95/98/Me: 55ms

Windows NT/2000/XP: 10–15ms

Mac OS X: 1ms

Linux: 1ms

With a 55ms timer resolution, that means about 18 updates per second, even if the game is capable of drawing 60 frames per seconds. To get an idea of what this is like, take a look at Screenshot. In this figure, the gray ball represents the poor timer resolution, and the white ball represents a high-resolution timer.

Screenshot A poor timer resolution (gray ball) gives bumpy visual updates, unlike a high-resolution timer (white ball).

Java graphics 16fig02.gif


If you're curious what the timer resolution of your system is, run the test in Listing 16.5.

Listing 16.5 GranularityTest.java
/**
 Measures the granularity of System.currentTimeMillis().
*/
public class GranularityTest {
 public static final int NUM_SAMPLES = 100;
 public static void main(String[] args) {
 long sum = 0;
 int count = 0;
 long lastTime = System.currentTimeMillis();
 while (count < NUM_SAMPLES) {
 long currTime = System.currentTimeMillis();
 // if the time changed, record the difference
 if (currTime > lastTime) {
 long granularity = currTime - lastTime;
 // keep a running sum of the granularity
 sum+=granularity;
 count++;
 lastTime = currTime;
 }
 }
 // display results
 System.out.println("Average granularity of " +
 "System.currentTimeMillis(): " + ((float)sum / count));
 }
}


One solution to this problem is to use JNI, or the Java Native Interface, to call some native code that uses a more accurate timer. This problem manifests itself only on Windows, so you'd need a JNI call only for Windows machines and would continue to use System.currentTimeMillis() on other machines. Windows has a multimedia timer that is more accurate. You can learn more about JNI in the Java documentation. However, if you're not interested in working with native code, there are a few other solutions. One is to just use the refresh rate of the display as a timer. However, this works only if you know your game can run at that refresh rate. For most games, the frame rate varies. Another idea is to make timer estimates based on previous time samples. You can do this by averaging the last few timer values. As an example, consider this hypothetical situation, in which the times represent the amount of time passed for each frame:

Actual Time

Timer Value

Average Time

Frame 1: 28ms

Frame 1: 0ms

Frame 1: 0ms

Frame 2: 28ms

Frame 2: 55ms

Frame 2: 28ms

Frame 3: 28ms

Frame 3: 0ms

Frame 3: 18ms

Frame 4: 28ms

Frame 4: 55ms

Frame 4: 28ms

Frame 5: 28ms

Frame 5: 0ms

Frame 5: 22ms

Frame 6: 28ms

Frame 6: 55ms

Frame 6: 28ms

Here, the timer has a 55ms granularity. The game runs at about 35 frames per second, or 28ms per frame. Averaging the timer values is not perfect but gives better results than the actual timer value. Because there are more timer samples, the estimate becomes more accurate. You'll actually implement this technique now. The TimeSmoothie class, in Listing 16.6, keeps track of the last few time samples and averages them to get the current time value. It also has functions to calculate the frame rate.
Listing 16.6 TimeSmoothie.java
package com.brackeen.javagamebook.util;
/**
 Smoothes out the jumps in time due to poor timer accuracy.
 This is a simple algorithm that is slightly inaccurate (the
 smoothed time may be slightly ahead of real time) but gives
 better-looking results.
*/
public class TimeSmoothie {
 /**
 How often to recalc the frame rate
 */
 protected static final long FRAME_RATE_RECALC_PERIOD = 500;
 /**
 Don't allow the elapsed time between frames to be more than 100 ms
 */
 protected static final long MAX_ELAPSED_TIME = 100;
 /**
 Take the average of the last few samples during the last 100ms
 */
 protected static final long AVERAGE_PERIOD = 100;
 protected static final int NUM_SAMPLES_BITS = 6; // 64 samples
 protected static final int NUM_SAMPLES = 1 << NUM_SAMPLES_BITS;
 protected static final int NUM_SAMPLES_MASK = NUM_SAMPLES - 1;
 protected long[] samples;
 protected int numSamples = 0;
 protected int firstIndex = 0;
 // for calculating frame rate
 protected int numFrames = 0;
 protected long startTime;
 protected float frameRate;
 public TimeSmoothie() {
 samples = new long[NUM_SAMPLES];
 }
 /**
 Adds the specified time sample and returns the average
 of all the recorded time samples.
 */
 public long getTime(long elapsedTime) {
 addSample(elapsedTime);
 return getAverage();
 }
 /**
 Adds a time sample.
 */
 public void addSample(long elapsedTime) {
 numFrames++;
 // cap the time
 elapsedTime = Math.min(elapsedTime, MAX_ELAPSED_TIME);
 // add the sample to the list
 samples[(firstIndex + numSamples) & NUM_SAMPLES_MASK] =
 elapsedTime;
 if (numSamples == samples.length) {
 firstIndex = (firstIndex + 1) & NUM_SAMPLES_MASK;
 }
 else {
 numSamples++;
 }
 }
 /**
 Gets the average of the recorded time samples.
 */
 public long getAverage() {
 long sum = 0;
 for (int i=numSamples-1; i>=0; i--) {
 sum+=samples[(firstIndex + i) & NUM_SAMPLES_MASK];
 // if the average period is already reached, go ahead and return
 // the average.
 if (sum >= AVERAGE_PERIOD) {
 Math.round((double)sum / (numSamples-i));
 }
 }
 return Math.round((double)sum / numSamples);
 }
 /**
 Gets the frame rate (number of calls to getTime() or
 addSample() in real time). The frame rate is recalculated
 every 500ms.
 */
 public float getFrameRate() {
 long currTime = System.currentTimeMillis();
 // calculate the frame rate every 500 milliseconds
 if (currTime > startTime + FRAME_RATE_RECALC_PERIOD) {
 frameRate = (float)numFrames * 1000 /
 (currTime - startTime);
 startTime = currTime;
 numFrames = 0;
 }
 return frameRate;
 }
}


To use this class, you simply have to add one line of code in the main game loop to return an estimated elapsed time based on the elapsed time reported by the clock:

public void update(long elapsedTime) {
 ...
 // smooth out the elapsed time
 elapsedTime = timeSmoothie.getTime(elapsedTime);
 // update the world based on the elapsed time
 // from the time smoothie.
 updateWorld(elapsedTime);
}


This code works best when the frame rate is fairly consistent. Change can be okay, but if the frame rate tends to vary wildly, TimeSmoothie won't give as accurate results. In that case, a JNI function might be more appropriate.



   
Comments