Friday, September 24, 2010

A Slightly Improved FPS/UPS/Sleep Time Calculator

Irritated by the monolithic structure provided by the book's examples, I decided to separate out the metrics calculations into a separate class. I'm not sure if this is the best possible solution, but it does provide certain advantages over the author's examples.  To name a few:

  • 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