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.
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 |
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.