- The metrics calculation logic is separated out from the game logic, greatly improving the game class' cohesion.
- This new metrics class is directly reusable and stands on its own integrity.
- The metrics calculations are all encapsulated neatly into private methods, eliminating the need for a programmer to understand them and reducing the odds of making an error in reproducing this functionality.
- Several variables in the original code were assigned but then never referenced. This code eliminates them.
- This code is documented with comments. Not completely or perfectly, but much better than the textbook examples.
I will follow up in a separate post to show what my game loop using this class looks like.
public class MHRuntimeMetrics
{
// Constants to make the math easier and less error-prone.
private static final long ONE_SECOND_IN_MILLI = 1000L;
private static final long ONE_MILLI_IN_NANO = 1000000L;
private static final long ONE_SECOND_IN_NANO = ONE_SECOND_IN_MILLI * ONE_MILLI_IN_NANO;
/** The frame rate we're trying to achieve. */
public static final short TARGET_FPS = 50; // 28 = average FPS for Japanese anime
/** Average period in nanoseconds required to achieve the target frame rate. */
public static final long PERIOD = ONE_SECOND_IN_NANO/TARGET_FPS;
/** Maximum number of renders to skip when compensating for long loop iterations. */
static final short MAX_FRAME_SKIPS = 2;
/** Nanoseconds between metrics calculations. */
private static final long MAX_STATS_INTERVAL = ONE_SECOND_IN_NANO;
/** Number of measurements to track for calculating an average. */
private static final int SAMPLE_SIZE = 10;
private int frameCount = 0; // Number of frames elapsed.
private int statsInterval; // Time since last metrics calculation.
private long gameStartTime; // Approximate time that game started.
private long prevStatsTime = 0; // Time that metrics were last calculated.
private long totalElapsedTime = 0; // Total time elapsed in game loop.
private int totalFramesSkipped; // Total renders skipped due to long loop iterations.
private int framesSkipped = 0; // No. renders skipped since last calculation.
private int statsCount = 0; // Index into fpsStore and upsStore arrays.
private double averageFPS; // Calculated average frames per second.
private double averageUPS; // Calculated average updates per second.
private int[] fpsStore, upsStore; // Arrays for storing calculation results.
private long startTime, endTime; // Start and end times for current loop iteration.
private long excess = 0; // Extra time acquired from short loop iterations.
/****************************************************************
* Default constructor.
*/
public MHRuntimeMetrics()
{
gameStartTime = System.nanoTime();
prevStatsTime = gameStartTime;
fpsStore = new int[SAMPLE_SIZE];
upsStore = new int[SAMPLE_SIZE];
}
/****************************************************************
*/
public void recordStartTime()
{
startTime = System.nanoTime();
}
/****************************************************************
*/
public void recordEndTime()
{
endTime = System.nanoTime();
storeStats(); // Update the stored metrics.
Toolkit.getDefaultToolkit().sync(); // Sync the display (for Linux users).
}
/****************************************************************
*/
private int nanoToSec(long nano)
{
return (int)(nano / ONE_SECOND_IN_NANO);
}
/****************************************************************
*/
private long nanoToMilli(long nano)
{
return nano / ONE_MILLI_IN_NANO;
}
/****************************************************************
*/
private void storeStats()
{
frameCount++;
statsInterval += PERIOD;
if (statsInterval >= MAX_STATS_INTERVAL)
{
long timeNow = System.nanoTime();
long realElapsedTime = timeNow - prevStatsTime; // time since last stats collection
totalElapsedTime += realElapsedTime;
totalFramesSkipped += framesSkipped;
int actualFPS = 0; // calculate the latest FPS and UPS
int actualUPS = 0;
if (totalElapsedTime > 0)
{
actualFPS = (frameCount / nanoToSec(totalElapsedTime));
actualUPS = ((frameCount + totalFramesSkipped) / nanoToSec(totalElapsedTime));
}
// store the latest FPS and UPS
fpsStore[ statsCount%SAMPLE_SIZE ] = actualFPS;
upsStore[ statsCount%SAMPLE_SIZE ] = actualUPS;
statsCount += 1;
double totalFPS = 0.0; // total the stored FPSs and UPSs
double totalUPS = 0.0;
for (int i=0; i < SAMPLE_SIZE; i++)
{
totalFPS += fpsStore[i];
totalUPS += upsStore[i];
}
if (statsCount < SAMPLE_SIZE) // obtain the average FPS and UPS
{
averageFPS = totalFPS/statsCount;
averageUPS = totalUPS/statsCount;
}
else
{
averageFPS = totalFPS/SAMPLE_SIZE;
averageUPS = totalUPS/SAMPLE_SIZE;
}
framesSkipped = 0;
prevStatsTime = timeNow;
statsInterval = 0; // reset
}
} // end of storeStats()
/****************************************************************
* Calculate how long the application thread should sleep based on the time
* it took to run the game loop.
*/
public void sleep()
{
long sleepTime = PERIOD - (endTime - startTime);
if (sleepTime > 0)
{
try
{
Thread.sleep(nanoToMilli(sleepTime));
}
catch (final InterruptedException e)
{
}
}
else
{
excess -= sleepTime; // store excess time value
Thread.yield(); // give another thread a chance to run
}
}
/****************************************************************
*/
public int getFramesPerSecond()
{
return (int) averageFPS;
}
/****************************************************************
*/
public int getUpdatesPerSecond()
{
return (int) averageUPS;
}
/****************************************************************
*/
public int getTimeSpentInGame()
{
return nanoToSec(System.nanoTime() - gameStartTime);
}
/****************************************************************
*/
public boolean shouldUpdate()
{
boolean updateNeeded = (excess > PERIOD) && (framesSkipped < MAX_FRAME_SKIPS);
if (updateNeeded)
{
excess -= MHRuntimeMetrics.PERIOD;
framesSkipped++;
}
return updateNeeded;
}
No comments:
Post a Comment