Sponsored sites

Tuesday, 7 October 2014

Measuring FPS

In the previous entry we have created a game loop that runs at a constant speed and constant (more or less) FPS.
How can we measure it? Check the new MainThread.java class.

001package net.obviam.droidz;
002 
003import java.text.DecimalFormat;
004 
005import android.graphics.Canvas;
006import android.util.Log;
007import android.view.SurfaceHolder;
008 
009 
010/**
011 * @author impaler
012 *
013 * The Main thread which contains the game loop. The thread must have access to
014 * the surface view and holder to trigger events every game tick.
015 */
016public class MainThread extends Thread {
017     
018    private static final String TAG = MainThread.class.getSimpleName();
019     
020    // desired fps
021    private final static int    MAX_FPS = 50;  
022    // maximum number of frames to be skipped
023    private final static int    MAX_FRAME_SKIPS = 5;   
024    // the frame period
025    private final static int    FRAME_PERIOD = 1000 / MAX_FPS;
026     
027    // Stuff for stats */
028    private DecimalFormat df = new DecimalFormat("0.##");  // 2 dp
029    // we'll be reading the stats every second
030    private final static int    STAT_INTERVAL = 1000; //ms
031    // the average will be calculated by storing
032    // the last n FPSs
033    private final static int    FPS_HISTORY_NR = 10;
034    // last time the status was stored
035    private long lastStatusStore = 0;
036    // the status time counter
037    private long statusIntervalTimer    = 0l;
038    // number of frames skipped since the game started
039    private long totalFramesSkipped         = 0l;
040    // number of frames skipped in a store cycle (1 sec)
041    private long framesSkippedPerStatCycle  = 0l;
042 
043    // number of rendered frames in an interval
044    private int frameCountPerStatCycle = 0;
045    private long totalFrameCount = 0l;
046    // the last FPS values
047    private double  fpsStore[];
048    // the number of times the stat has been read
049    private long    statsCount = 0;
050    // the average FPS since the game started
051    private double  averageFps = 0.0;
052 
053    // Surface holder that can access the physical surface
054    private SurfaceHolder surfaceHolder;
055    // The actual view that handles inputs
056    // and draws to the surface
057    private MainGamePanel gamePanel;
058 
059    // flag to hold game state
060    private boolean running;
061    public void setRunning(boolean running) {
062        this.running = running;
063    }
064 
065    public MainThread(SurfaceHolder surfaceHolder, MainGamePanel gamePanel) {
066        super();
067        this.surfaceHolder = surfaceHolder;
068        this.gamePanel = gamePanel;
069    }
070 
071    @Override
072    public void run() {
073        Canvas canvas;
074        Log.d(TAG, "Starting game loop");
075        // initialise timing elements for stat gathering
076        initTimingElements();
077         
078        long beginTime;     // the time when the cycle begun
079        long timeDiff;      // the time it took for the cycle to execute
080        int sleepTime;      // ms to sleep (<0 if we're behind)
081        int framesSkipped;  // number of frames being skipped
082 
083        sleepTime = 0;
084         
085        while (running) {
086            canvas = null;
087            // try locking the canvas for exclusive pixel editing
088            // in the surface
089            try {
090                canvas = this.surfaceHolder.lockCanvas();
091                synchronized (surfaceHolder) {
092                    beginTime = System.currentTimeMillis();
093                    framesSkipped = 0// resetting the frames skipped
094                    // update game state
095                    this.gamePanel.update();
096                    // render state to the screen
097                    // draws the canvas on the panel
098                    this.gamePanel.render(canvas);             
099                    // calculate how long did the cycle take
100                    timeDiff = System.currentTimeMillis() - beginTime;
101                    // calculate sleep time
102                    sleepTime = (int)(FRAME_PERIOD - timeDiff);
103                     
104                    if (sleepTime > 0) {
105                        // if sleepTime > 0 we're OK
106                        try {
107                            // send the thread to sleep for a short period
108                            // very useful for battery saving
109                            Thread.sleep(sleepTime);   
110                        } catch (InterruptedException e) {}
111                    }
112                     
113                    while (sleepTime < 0 && framesSkipped < MAX_FRAME_SKIPS) {
114                        // we need to catch up
115                        this.gamePanel.update(); // update without rendering
116                        sleepTime += FRAME_PERIOD;  // add frame period to check if in next frame
117                        framesSkipped++;
118                    }
119 
120                    if (framesSkipped > 0) {
121                        Log.d(TAG, "Skipped:" + framesSkipped);
122                    }
123                    // for statistics
124                    framesSkippedPerStatCycle += framesSkipped;
125                    // calling the routine to store the gathered statistics
126                    storeStats();
127                }
128            } finally {
129                // in case of an exception the surface is not left in
130                // an inconsistent state
131                if (canvas != null) {
132                    surfaceHolder.unlockCanvasAndPost(canvas);
133                }
134            }   // end finally
135        }
136    }
137 
138    /**
139     * The statistics - it is called every cycle, it checks if time since last
140     * store is greater than the statistics gathering period (1 sec) and if so
141     * it calculates the FPS for the last period and stores it.
142     *
143     *  It tracks the number of frames per period. The number of frames since
144     *  the start of the period are summed up and the calculation takes part
145     *  only if the next period and the frame count is reset to 0.
146     */
147    private void storeStats() {
148        frameCountPerStatCycle++;
149        totalFrameCount++;
150         
151        // check the actual time
152        statusIntervalTimer += (System.currentTimeMillis() - statusIntervalTimer);
153         
154        if (statusIntervalTimer >= lastStatusStore + STAT_INTERVAL) {
155            // calculate the actual frames pers status check interval
156            double actualFps = (double)(frameCountPerStatCycle / (STAT_INTERVAL / 1000));
157             
158            //stores the latest fps in the array
159            fpsStore[(int) statsCount % FPS_HISTORY_NR] = actualFps;
160             
161            // increase the number of times statistics was calculated
162            statsCount++;
163             
164            double totalFps = 0.0;
165            // sum up the stored fps values
166            for (int i = 0; i < FPS_HISTORY_NR; i++) {
167                totalFps += fpsStore[i];
168            }
169             
170            // obtain the average
171            if (statsCount < FPS_HISTORY_NR) {
172                // in case of the first 10 triggers
173                averageFps = totalFps / statsCount;
174            } else {
175                averageFps = totalFps / FPS_HISTORY_NR;
176            }
177            // saving the number of total frames skipped
178            totalFramesSkipped += framesSkippedPerStatCycle;
179            // resetting the counters after a status record (1 sec)
180            framesSkippedPerStatCycle = 0;
181            statusIntervalTimer = 0;
182            frameCountPerStatCycle = 0;
183 
184            statusIntervalTimer = System.currentTimeMillis();
185            lastStatusStore = statusIntervalTimer;
186//          Log.d(TAG, "Average FPS:" + df.format(averageFps));
187            gamePanel.setAvgFps("FPS: " + df.format(averageFps));
188        }
189    }
190 
191    private void initTimingElements() {
192        // initialise timing elements
193        fpsStore = new double[FPS_HISTORY_NR];
194        for (int i = 0; i < FPS_HISTORY_NR; i++) {
195            fpsStore[i] = 0.0;
196        }
197        Log.d(TAG + ".initTimingElements()", "Timing elements for stats initialised");
198    }
199 
200}
I introduced a simple measuring function. I count the number of frames every second and store them in the fpsStore[] array. The storeStats() is called every tick and if the 1 second interval (STAT_INTERVAL = 1000;) is not reached then it simply adds the number of frames to the existing count.
If the one second is hit then it takes the number of rendered frames and adds them to the array of FPSs. After this I just reset the counters for the current statistics cycle and add the results to a global counter. The average is calculated on the values stored in the last 10 seconds.
Line 171 logs the FPS every second while line 172 sets the avgFps value of the gamePanel instance to be displayed on the screen.
The MainGamePanel.java class’s render method contains the the displayFps call which just draws the text onto the top right corner of the display every time the state is rendered. It also has a private member that is set from the thread.
01// the fps to be displayed
02private String avgFps;
03public void setAvgFps(String avgFps) {
04    this.avgFps = avgFps;
05}
06 
07public void render(Canvas canvas) {
08    canvas.drawColor(Color.BLACK);
09    droid.draw(canvas);
10    // display fps
11    displayFps(canvas, avgFps);
12}
13 
14private void displayFps(Canvas canvas, String fps) {
15    if (canvas != null && fps != null) {
16        Paint paint = new Paint();
17        paint.setARGB(255, 255, 255, 255);
18        canvas.drawText(fps, this.getWidth() - 50, 20, paint);
19    }
20}
Try running it. You should have the FPS displayed in the top right corner.
FPS displayed

No comments:

Post a Comment