Home > Guide e tutorial per Android > Sviluppare un Gioco per Android – Lezione 7: Misurare il numero di FPS

Sviluppare un Gioco per Android – Lezione 7: Misurare il numero di FPS

programmazione-android7Nella precedente lezione abbiamo visto come sviluppare un ciclo di gioco che tenga conto di FPS e UPS. Tuttavia, giunti a questo punto, una domanda sorge spontanea: come possiamo misurare il numero di FPS nel nostro gioco? Quest’oggi, vedremo come trovare la risposta a questa domanda modificando la classe MainThread.java. La lezione di oggi, infatti, sarà dedicata completamente a questa attività. Per prima cosa, dovremo dichiarare un bel po’ di variabili aggiuntive, di seguito potete vedere quali sono:

// Statistiche */
	private DecimalFormat df = new DecimalFormat("0.##");  // 2 dp
	// Leggeremo le statistiche ogni secondo
	private final static int    STAT_INTERVAL = 1000; //ms
	// la media sarà calcolata memorizzando
	// l'ultimo valore di FPS
	private final static int    FPS_HISTORY_NR = 10;
	// L'ultima volta che lo stato è stato memorizzato
	private long lastStatusStore = 0;
	// contatore dello stato
	private long statusIntervalTimer    = 0l;
	// numero di fram saltati da quando il gioco è iniziato
	private long totalFramesSkipped         = 0l;
	// numero di frame saltati in un ciclo di memorizzazione (1 sec)
	private long framesSkippedPerStatCycle  = 0l;
	// numero di frame visualizzati in un intervallo
	private int frameCountPerStatCycle = 0;
	private long totalFrameCount = 0l;
	// gli ultimi valori FPS
	private double  fpsStore[];
	// numero di volte che le statistiche sono state lette
	private long    statsCount = 0;
	// media FPS da quando il gioco è iniziato
	private double  averageFps = 0.0;

A questo punto, ci dovremmo chiedere come modificare la classe MainThread.java affinché sia possibile visualizzare le statistiche da noi desiderate. Per farlo, adotteremo principalmente due metodi e modificheremo leggermente il metodo run(). Gli altri due metodi che utilizzeremo saranno rispettivamente storeStats() per memorizzare i dati e initTimingElements() per inizializzare gli elementi utili al timer per le statistiche. Di seguito potete dare un’occhiata ai tre metodi:

public void run() {
		Canvas canvas;
		Log.d(TAG, "Starting game loop");
		// inizializza gli elementi utili ai timer per le statistiche
		initTimingElements();

		long beginTime;		// il tempo quando inizia il ciclo
		long timeDiff;		// tempo impiegato per il ciclo
		int sleepTime;		// ms per lo stato di sleep (può essere <0)
		int framesSkipped;	// numero di frame saltati

		sleepTime = 0;

		while (running) {
			canvas=null;
			//Proviamo a bloccare la "tela" per la modifica dei pixel sulla superficie
			try {
				canvas = this.surfaceHolder.lockCanvas();
			    synchronized (surfaceHolder) {
			    	beginTime = System.currentTimeMillis();
					framesSkipped = 0;	// resettiamo i frame saltati
				    // Aggiornamento dello stato di gioco
			    	this.gamePanel.update();
				    // disegna la "tela" (canvas) nel pannello
				    this.gamePanel.onDraw(canvas);
				    // Calcoliamo quanto tempo prende il ciclo
					timeDiff = System.currentTimeMillis() - beginTime;
					// Calcoliamo sleepTime
					sleepTime = (int)(FRAME_PERIOD - timeDiff);

					if (sleepTime > 0) {
						// Caso ottimale se > 0
						try {
							// mandiamo il thread a dormire per un breve periodo
							// molto utile per risparmiare batteria
							Thread.sleep(sleepTime);	
						} catch (InterruptedException e) {}
					}

					while (sleepTime < 0 && framesSkipped < MAX_FRAME_SKIPS) {
						// Abbiamo bisogno di recuperare
						this.gamePanel.update(); // aggiornamento senza rendering
						sleepTime += FRAME_PERIOD;	// aggiungere frame period per il controllo del frame successivo
						framesSkipped++;
					}

					if (framesSkipped > 0) {
	                    Log.d(TAG, "Skipped:" + framesSkipped);
	                }
				    // per le statistiche
				    framesSkippedPerStatCycle += framesSkipped;
				    // memorizza le statistiche
				    storeStats();
			    }
			} finally {
				// Se scatta l'eccezione la superficie non viene lasciata
			    // in uno stato incoerente
				if (canvas != null) {
				   surfaceHolder.unlockCanvasAndPost(canvas);
			    }
		    } //fine finally
		}
	}

	private void storeStats() {
		frameCountPerStatCycle++;
		totalFrameCount++;
		// controlla il tempo attuale
		statusIntervalTimer += (System.currentTimeMillis() - statusIntervalTimer);

		if (statusIntervalTimer >= lastStatusStore + STAT_INTERVAL) {
			// calcola i frames attuali per ogni intervallo
		    double actualFps = (double)(frameCountPerStatCycle / (STAT_INTERVAL / 1000));

		    //memorizza l'ultimo valore FPS nell'array
		    fpsStore[(int) statsCount % FPS_HISTORY_NR] = actualFps;
		    // incrementa il numero di volte che le stat vengono calcolate
		    statsCount++;
		    double totalFps = 0.0;
		    // somma i valore memotizzati nell'array
		    for (int i = 0; i < FPS_HISTORY_NR; i++) {
		       totalFps += fpsStore[i];
		    }

		    // otteniamo la media
		    if (statsCount < FPS_HISTORY_NR) {
		        averageFps = totalFps / statsCount;
		    } else {
		        averageFps = totalFps / FPS_HISTORY_NR;
		    }
		    // salviamo il numero totale dei frame saltati
		    totalFramesSkipped += framesSkippedPerStatCycle;
		    // resettiamo i contatori dopo la memorizzazione di uno stato (1 sec)
		    framesSkippedPerStatCycle = 0;
		    statusIntervalTimer = 0;
		    frameCountPerStatCycle = 0;
		    statusIntervalTimer = System.currentTimeMillis();
		    lastStatusStore = statusIntervalTimer;
                    Log.d(TAG, "Average FPS:" + df.format(averageFps));
		    gamePanel.setAvgFps("FPS: " + df.format(averageFps));
		 }
	}

	private void initTimingElements() {
		 // inizializziamo gli elementi per il timer
		 fpsStore = new double[FPS_HISTORY_NR];
		 for (int i = 0; i < FPS_HISTORY_NR; i++) {
		    fpsStore[i] = 0.0;
		 }
		 Log.d(TAG + ".initTimingElements()", "Timing elements for stats initialised");
	}

La funzione non è molto difficile da capire. Essa si basa sul conteggio dei fotogrammi e sulla relativa memorizzazione nell’array fpsStore[]. Ad ogni intervallo di 1 secondo, viene chiamato storeStats(). Se l’intervallo di un secondo non viene raggiunto, aggiunge semplicemente il numero dei fotogrammi al conteggio attuale, altrimenti prende il numero di fotogrammi visualizzati e li aggiunge all’array, dopodiché azzera i contatori. La media viene calcolata sui valori memorizzati negli ultimi dieci secondi.

A questo punto, l’ultimo passo è implementare i metodi setAvgFps e displayFps per la visualizzazione della media sul display in alto a destra. Di seguito potete dare un’occhiata alle modifiche da apportare alla classe MainGamePanel.java:

private String avgFps: 

protected void onDraw(Canvas canvas) {
		 //Riempiamo la "tela" di nero
		 canvas.drawColor(Color.BLACK);
		 droid.draw(canvas);
		 displayFps(canvas, avgFps);
}

public void setAvgFps(String avgFps) {
		 this.avgFps = avgFps;
}

private void displayFps(Canvas canvas, String fps) {
		 if (canvas != null && fps != null) {
		     Paint paint = new Paint();
		     paint.setARGB(255, 255, 255, 255);
		     canvas.drawText(fps, this.getWidth() - 50, 20, paint);
		 }
}

Una volta effettuate le modifiche in maniera corretta, potete avviare l’applicazione e vi dovreste trovare dinanzi qualcosa di molto simile al seguente:

Screenshot from 2014-02-10 15:35:12

A questo punto, non ci rimane che darci appuntamento alla prossima settimana. Nelle prossime lezioni metteremo da parte il progetto Droid per dare risalto ad altre attività relative allo sviluppo di un gioco per Android, come le animazioni Sprite e non solo.