- Introduzione a XNA Game Studio -
 
COSA SERVE PER QUESTO TUTORIAL
Download | Chiedi sul FORUM | Glossario cognizioni basiche di C#
Il primo videogioco con XNA

COME È STRUTTURATO UN GIOCO PER XNA
Update, Draw, caricamento delle risorse e gestione dell'interazione.

In questo primo tutorial ci occuperemo di XNA, un framework per la creazione di videogiochi in linguaggi .Net (C# primo fra tutti) che si basa sulle DirectX. Vediamo prima di tutto quali sono gli strumenti necessari per creare il nostro primo, semplice, videogioco. L'ambiente di sviluppo preferibile per servirsi delle librerie XNA è XNA Game Studio, un IDE basato su Visual Studio che offre alcune facilitazioni, principalmente per i template di tipi di file già pronti. Se non avete una versione di Visual Studio già installata potete procurarvi l'edizione Express (ovvero gratuita) di Visual C#; in questo articolo verrà presa in considerazione l'ultima versione correntemente disponibile di XNA, ovvero la 3.1, che supporta Visual Studio 2005 e Visual Studio 2008. Installato Visual Studio, scaricate ed installate XNA Game Studio 3.1 e siamo pronti per iniziare.
Creiamo un nuovo progetto, il template dei progetti XNA si trovano sotto Visual C#, XNA Game Studio 3.x. Create un Windows Game per XNA 3.0 e chiamatelo PallinaGame. Creato il progetto in Solution Explorer troverete una serie di elementi, vediamoli in dettaglio per capire come si struttura un videogame creato con XNA.

  • Properties e Refernces, tipiche voci di ogni progetto C#;
  • Content, un contenitore (in realtà una cartella), in cui andranno posizionate tutte le risorse del videogioco come suoni, sfondi, sprite, animazioni, modelli tridimensionali, caratteri e così via;
  • Game.ico, l'icona del videogioco come si può supporre;
  • Program.cs, file di codice dove risiede il Main dell'eseguibile, che si limita a creare un'istanza del gioco e a lanciarla;
  • GameThumbnail.png, un'immagine con un'anteprima del videogioco per la distribuzione;
  • PallinaGame.cs, il file di codice principale dove si trova il cuore vero e proprio del gioco;

Nel gioco di esempio vogliamo creare un semplice personaggio che sia in grado di muoversi nell'area di gioco (tramite le frecce direzionali della tastiera) e di variare la propria velocità e dimensione. Avremo bisogno di due risorse: un'immagine per lo sfondo e un'immagine per il nostro personaggio (non troppo grande, 16 per 16 pixel sarebbe l'ideale). Per aggiungerle al progetto fare click destro su Content in Solution Explorer, Add Existing Item e selezionare le due immagini dalla macchina locale, quindi rinominate lo sfondo in sfondo.jpg (o sfondo.png nel caso abbiate scelto una PNG) e il personaggio in spidey.jpg (o spidey.png).
Vediamo dunque come è strutturato il file PallinaGame.cs.


public class PallinaGame : Microsoft.Xna.Framework.Game
{
    GraphicsDeviceManager graphics;
    SpriteBatch spriteBatch;

    Texture2D pallina, bg;
    Vector2 posizionePallina = new Vector2(0);
    int speed = 1;
    int size = 1;

    public PallinaGame()
    {
        // Permettiamo il ridimensionamento della finestra
        this.Window.AllowUserResizing = true;

        // Inizializziamo il GraphicsDeviceManager e impostiamo le dimensioni
        // desiderate dell'area di disegno
        graphics = new GraphicsDeviceManager(this);
        graphics.PreferredBackBufferWidth = 800;
        graphics.PreferredBackBufferHeight = 600;

        // Impostiamo la sottocartella con le risorse
        this.Content.RootDirectory = "Content";
    }
        

Per prima cosa notiamo che la classe eredita da Microsoft.Xna.Framework.Game, infatti ogni gioco XNA deve avere una classe simile che costituisce la base del gioco; esso permette ad esempio di terminare il gioco con il metodo Exit, oppure di abilitare il ridimensionamento della finestra del gioco tramite la proprietà Window.AllowUserResizing. Game espone inoltre la proprietà Content, tramite la quale è possibile interagire con le risorse contenute nella cartella Content di cui si parlava poco fa. Content.RootDirectory imposta la sottocartella (rispetto all'eseguibile) che contiene le risorse, in questo caso sarà dunque la sottocartella "Content".
Troviamo poi la dichiarazione di un oggetto di tipo GraphicsDeviceManager che permette di interagire con il dispositivo grafico del gioco, lo schermo. È possibile impostare le dimensioni desiderate dell'area di disegno tramite le proprietà PreferredBackBufferHeight e PreferredBackBufferWidth. Grazie a questo oggetto è anche possibile selezionare la modalità schermo intero tramite la proprietà IsFullScreen. Vi è poi un oggetto di tipo SpriteBatch che ci permetterà di disegnare personaggio e sfondo nel metodo Draw.
Oltre a queste due dichiarazioni presenti di default nei progetti XNA, aggiungiamo due oggetti di tipo Texture2D, pallina e bg, che conterranno rispettivamente l'immagine del personaggio e lo sfondo. Dichiariamo quindi un Vector2 (ovvero una struttura che indica un punto in uno spazio bidimensionale) per la posizione della pallina (il nostro personaggio) e due variabili intere per velocità e dimensione.
Abbiamo poi la funzione LoadContent in cui dobbiamo caricare in memoria tutte le risorse, nel nostro caso lo sfondo e il personaggio:


// Carichiamo le risorse in memoria
protected override void LoadContent()
{
    // Inizializziamo lo SpriteBatch
    spriteBatch = new SpriteBatch(this.GraphicsDevice);

    // Carichiamo sfondo e personaggio
    bg = this.Content.Load<Texture2D>("sfondo");
    pallina = this.Content.Load<Texture2D>("pallina");

}

In LoadContent prima di tutto creiamo una nuova istanza di spriteBatch relativo al GraphicsDevice del nostro gioco, quindi inizializziamo le variabili bg e pallina caricando le risorse sfondo e pallina. Si noti che l'identificativo della risorsa è il nome del file privato dell'estensione.

A questo punto abbiamo concluso la parte preparatoria del nostro gioco e possiamo iniziare a vedere le due funzioni che gestiscono la logica e l'output grafico: Update e Draw. Esse vengono in sostanza chiamate di continuo in successione in modo che tramite Update si possa rilevare ad esempio la pressione di un tasto, cambiare le variabili di gioco e lasciare che poi il metodo Draw ridisegni l'area di gioco di conseguenza. Ma vediamole in dettaglio:


// Gestiamo la logica di gioco
protected override void Update(GameTime gameTime)
{
    if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.Escape))
        this.Exit();

    if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.Delete))
    {
        speed = 1;
        size = 1;
        posizionePallina.X = 0;
        posizionePallina.Y = 0;
    }

    if (speed >= 1 && Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.PageDown))
        speed--;
    if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.PageUp))
        speed++;

    Rectangle gameArea = new Rectangle(0, 0, graphics.GraphicsDevice.Viewport.Width, 
        graphics.GraphicsDevice.Viewport.Height);

    if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.F11) && size >= 1)
        size--;
    if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.F12) && 
        gameArea.Contains(this.GetPallinaRectangle(0, 0, 1, 1)))
        size++;
    

    if (Keyboard.GetState(PlayerIndex.One).IsKeyDown(Keys.Up))
        if (gameArea.Contains(this.GetPallinaRectangle(0, -speed, 0, 0)))
            posizionePallina.Y -= speed;
        else
            posizionePallina.Y = 0;

    // [...]
    // Gestione delle altre direzioni
    // [...]

    base.Update(gameTime);
}

Il metodo Update è in questo caso completamente dedicato alla gestione degli input dell'utente, infatti tramite Keyboard.GetState possiamo controllare se un tasto è stato premuto o meno ed agire di conseguenza. Il tasto Esc causa la chiusura del gioco, il tasto Canc la reimpostazione della variabili di gioco, Pagina Su e Pagina Giù regolano la velocità, F11 e F12 la dimensione e le frecce direzionali il movimento. Il movimento consiste sostanzialmente nel verificare che non si stia uscendo dall'area di gioco e quindi incrementare o decrementare la coordinata X o Y (a seconda del tasto premuto) della pallina di un valore pari alla variabile della velocità.
Veniamo ora al metodo Draw.


// Gestiamo l'output grafico istante per istante
protected override void Draw(GameTime gameTime)
{
    // Puliamo l'area di disegno
    GraphicsDevice.Clear(Color.CornflowerBlue);
    
    spriteBatch.Begin();

    // Disegnamo lo sfondo
    spriteBatch.Draw(bg, new Rectangle(0, 0, this.GraphicsDevice.Viewport.Width, 
        this.GraphicsDevice.Viewport.Height), Color.White);
    // Disegnamo il personaggio
    spriteBatch.Draw(pallina, this.GetPallinaRectangle(), Color.White);

    spriteBatch.End();

    base.Draw(gameTime);
}

In sostanza nel metodo di disegno puliamo l'area di disegno e iniziamo a disegnare lo sprite. Prima chiamiamo il metodo Draw di SpriteBatch passandogli l'oggetto in cui avevamo caricato la sfondo (bg) e il rettangolo su cui estenderlo, da in alto a sinistra per tutta la larghezza dell'area di disegno. Quindi disegniamo il personaggio nella posizione indicata dal vettore posizionePallina e con le dimensioni pari a size-volte quella originale:


private Rectangle GetPallinaRectangle(int dx, int dy, int dw, int dh)
{
    return new Rectangle((int)posizionePallina.X + dx, (int)posizionePallina.Y + dy,
        pallina.Width * (size + dw), pallina.Height * (size + dh));
}

private Rectangle GetPallinaRectangle()
{
    return GetPallinaRectangle(0, 0, 0, 0);
}

Nel progetto allegato è anche presente un componente (vedremo prossimamente di cosa si tratta e come crearne) che mostra in alto a sinistra il numero di chiamate a Update e Draw per secondo.

 

<< INDIETRO by VeNoM00