The XNA-Way #1: FPS Counter

Ho deciso di ripresentare in questo nuovo sito i vecchi tutorial della serie The XNA-Way che avevo scritto sul mio vecchio blog.
Perchè direte voi?

Da quando li ho scritti ho imparato nuove cose, quindi forse ora potrei ottimizzare il codice. Inoltre quando cominciai si utilizzava la versione 3 di XNA mentre ora siamo alla 4, quindi alcune cose non si possono più fare come si facevano prima, e vanno quindi aggiornate.
Inoltre il vecchio server ftp su cui avevo caricato i vari sorgenti non è più funzionante (vai a capire il perchè) quindi (dato che mi hanno fatto notare che i sorgenti non sono più disponibili) si rende necessario metterli di nuovo a disposizione (sperando che possano tornare utili a qualcuno).
E sfrutterò l’occasione anche per approfondire qualche parte, dato che una persona che ho conosciuto da poco mi aveva detto che gli sarebbe piaciuto imparare qualcosa di XNA.

Intanto ricominciamo con l’esempio del FPS_Counter.

Iniziamo con il creare e strutturare la soluzione da Visual Studio o Visual C# Express.
La soluzione conterrà al momento 2 progetti: la GameLibrary che servirà a contenere le varie classi e codice generico da utilizzare nei nostri progetti, ed il progetto di gioco, banalmente chiamato MyGame che sarà il vero e proprio gioco.
I progetti in verità sono 3 se consideriamo anche che serve a contenere e lavorare sugli Asset di gioco (cioè immagini, suono, modelli 3d, etc).

Dovremmo poi aggiungere un riferimento al progetto MyGame in modo che sia possibile utilizzare le classi create nella library nel nostro progetto.
Nel codice dovremmo poi aggiungere la corretta direttiva using per poter utilizzare correttamente le classi.

Cominciamo con il codice del FPS_Counter. Questo componente non deve far altro che contare il numero di frame per second che vengono disegnati dalla nostra applicazione.
In teoria un gioco XNA ha un valore di FPS sui 60 fps, ma nulla ci vieta di poter generare e disegnare più frame (NOTA: mi pare che per i giochi rilasciati su console o giochi definitivi sia richiesto che la frequenza di refresh sia ancorata ai 60 fps), oltre al fatto che generare più frame possibili può essere un modo per effettuare test di performance sul nostro progetto.

XNA permette di organizzare le “unità logiche e di funzionamento” di un progetto in blocchi chiamati Component. I Component sono classi che ereditano da GameComponent o da DrawableGameComponent. Anche l’oggetto principale del gioco, la classe Game, la possiamo vedere come un particolare Component.

Questi oggetti si caratterizzano per essere delle unità di esecuzione che verranno poi gestite in automatico dal framework durante l’esecuzione. Come ogni unità di esecuzione i Component possono essere creati, fermati, riavviati, distrutti, ma è possibile anche modificarne lo stato dall’esterno.
Il comportamento di un Component è definito da 2 metodi principali: il metodo Update ed il metodo Draw  (la differenza tra GameComponent e DrawableGameComponent è appunto che il primo non da la possibilità di fare l’override del metodo Draw).
I due metodi vengono chiamati in sequenza (Update->Draw->Update->Draw….) ed il tutto si chiama appunto ciclo Update-Draw.

Se vogliamo vedere la cosa da un punto di vista più formale e avvicinare il tutto ad un modello logico (qualcuno ha detto MVC da qualche parte?) durante la fase di Update si deve modificare lo stato del component o di quella parte dei dati del gioco che ci interessano al momento, mentre nel metodo Draw si deve solo effettuare il rendering della porzione dei dati in gestione a tale component.

Abbastanza semplice giusto?

Adesso una bella listata di codice, anche se decisamente semplice:

 

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
 
namespace GameLibrary.Component
{
    public class FPS_Counter : Microsoft.Xna.Framework.DrawableGameComponent
    {
        //contatore dei frame
        private int frameCount = 0;
        //indica l'intervallo di base del tmepo
        private float intervall; 
        //misura il tempo trascordo
        private float elapsedTime;
        //istante dlel'ultima chiamata
        private float timeLastDraw = 0.0f;
        //valore da visualizzare
        private float fpsValue;
 
        public FPS_Counter(Game game)
            : this(game, false)
        { }
 
        public FPS_Counter(Game game, bool active)
            : base(game)
        {
            GraphicsDeviceManager manager = (GraphicsDeviceManager)Game.Services.GetService(typeof(IGraphicsDeviceManager));
            if (active)
            {
                manager.SynchronizeWithVerticalRetrace = false;
                Game.IsFixedTimeStep = false;
            }
            intervall = 1.0f;
 
            //dice in che ordine deve essere eseguito il ciclo draw di questo componente rispetto agli altri component
            //in questo caso praticamente per ultimo
            DrawOrder = 100; 
        }
 
        public override void Draw(GameTime gameTime)
        {
            elapsedTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
            frameCount++;
            timeLastDraw += elapsedTime;
            if (timeLastDraw > intervall)
            {
                fpsValue = frameCount / intervall;
#if XBOX && DEBUG
                System.Diagnostics.Debug.WriteLine("FPS: " + fpsValue.ToString());
#elif WINDOWS && DEBUG
                Game.Window.Title = fpsValue.ToString();
#endif
                frameCount = 0;
                timeLastDraw -= intervall;
 
            }
            base.Draw(gameTime);
        }
    }
}

[warning]Una cosa che voglio far notare subito è il fatto che il mio Component non si trova direttamente all’interno del namespace della libreria, ma ho invece creato una subdirectory per i Component. In questo modo, mano a mano che andrà avanti con lo sviluppo di nuove funzionalità il codice sarà molto più organizzato e non si dovrà impazzire per andare a cercare una classe o un file.
[/warning]

Il codice risulta molto semplice:
nel costruttore si recupera il riferimento al service (li vedremo poi questi) del GraphicsDevice, e a seconda che il valore della variabile active modifica il lo stato del Graphics Device (in parole povere disattiva il vertical Sync) e lo stato del Game (fa si che il ciclo Update-Draw non venga eseguito ad intervalli di tempo fissi ma che venga eseguito quante più volte è possibile).
Il significato dei campi dalla classe mi pare chiaro. Se così non fosse voglio solo dire che intervall sta ad indicare l’unità base del tempo con cui rapportiamo i frame, in questo caso 1.0 significa 1 secondo (nessuno vi vieta di mettere 10 e di calcolarvi i frame ogni 10 secondi o altre cose del genere.

Nel Draw invece quello che facciamo è (nell’ordine) recuperare il valore del tempo trascorso dall’ultimo ciclo di aggiornamento, aggiorniamo il count ed il tempo trascorso dall’ultima chiamata del draw. Se il tempo è maggiore dell’unità di tempo da noi stabilita (in questo caso controlliamo se è passato più di un secondo) aggiorniamo fpsValue (che sarà usata per visualizzare i dati), azzeriamo il count e decrementiamo il valore del time.
Le righe di codice dopo il simbolo # sono dette direttive di precompilazione.
Per farla breve si possono esprimere delle condizioni che vengono valutate dal compilatore (e quindi a tempo di compilazione e non a tempo di esecuzione) e a seconda che queste condizioni siano vere o false il codice che è contenuto all’interno delle direttive sarà compilato o meno (questo vuol dire che nel programma che verrà eseguito tale codice non sarà proprio presente se la condizione è falsa).
Nel nostro caso la prima condizione dice che il codice deve essere compilato solo se il progetto verrà compilato per Xbox e se siamo in debug (quindi nella versione release non ci sarà), mentre la seconda condizione afferma che deve essere considerato solo se stiamo compilando su Windows e se siamo in debug.
Si tratta di funzionalità molto comode perchè permettono di scrivere in un solo file tutto il codice (sia quello che dovrà essere eseguito quando si compila per la console sia quello per quando si compila pe ril PC) senza creare problemi di incompatibilità. In questo modo non dovremo stare a modificare pesantemente e troppe volte i sorgenti quando dovremo creare più versioni dello stesso progetto.

Detto questo abbiamo visto come creare un semplice component, anche se in questo caso fa veramente poco. Ma come possiamo istanziarlo e farlo funzionare?
Andiamo nel file Game1.cs del nostro gioco, e troviamo il costruttore. Questa classe rappresenta il nucleo di un gioco XNA, ed è da qua che possiamo (ed in teoria dobbiamo) creare tutti componenti principali del nostro progetto.
Come avevo accennato prima, il framework gestirà per conto suo tutti i vari Component che aggiungeremo al gioco, e tutti i Component del progetto sono contenuti in una collezione di oggetti della classe Game chiamata Components.
Dato che è un campo della classe Game, e dato che la nostra classe Game1 eredita da Game essa potrà accedere a tale oggetto.
Nel costruttore basterà infatti scrivere

Components.Add(new FPS_Counter(this, true));

per istanziare ed aggiungere un nuovo FPS_Counter alla collezione dei Component gestiti dal framework.

[warning]Ricordatevi che dovete sia aver aggiunto il riferimento alla libreria dal progetto del gioco, sia aver scritto la giusta direttiva using nel file Game1.cs, altrimenti non potrete utilizzare il codice della library[/warning]

Ora quando eseguiremo il nostro gioco vedremo un numerino nella barra del titolo della finestra, e tale numerino rappresenta il numero di rendering per secondo che la nostra applicazione riesce a fare.
Non male vero?

Il codice e la logica di tutto questo mi sembrano abbastanza semplici quindi non starò a rimarcarci troppo sopra.
Qua sotto trovate il link ai sorgenti
IndieGearLab_01.rar
se ci sono dubbi o domande contattatemi o lasciate scritto nei commenti.

Alla prossima!

Lascia un commento

Your email address will not be published.

We use cookies to ensure that we give you the best experience on our website.
Ok