Torna alla home page XNA
 
TutorialsProgettiNewsForum
XNA Tutorials RSS

Realizzazione del primo esempio di programmazione

XNA | 08 marzo 2007 | Paolo Bragonzi


Scarica l'esempio allegato

Bene, dopo una prima introduzione dove abbiamo visto una panoramica sul nuovo ambiente di sviluppo Microsoft per la realizzazione di videogiochi e abbiamo visto quanto necessario per installarlo ed essere operativi, è giunto finalmente il momento di "sporcarsi le mani col codice"!


Pur trattandosi di un ambiente di sviluppo che ha tra i suoi punti di forza l'immediatezza e la semplicità di utilizzo, prima di cimentarsi nella programmazione vera e propria occorre "rispettare i requisiti minimi", ovvero conoscere il linguaggio di programmazione. Ma non vi spaventate, imparare la programmazione di C# .NET è facilitato dal molto materiale presente in rete, sia da un parco libri molto vasto (nel forum collegato a questa sezione è possibile approfondire l'argomento).

Perfetto, cominciamo a creare il nostro primo videogioco!

HelloWorld, normalmente è un semplice programma che visualizza la scritta "Hello World" (ma va?), la nostra creazione neonata che saluta il mondo. Noi ci atterremo alla tradizione, ma cercheremo di farlo... in stile XNA.

Come prima cosa dobbiamo creare il progetto che conterrà tutti i file del gioco, cartelle, etc. Quindi apriamo XNA Game Studio Express (d'ora in poi XNA GSE) e clicchiamo sul menù File -> New -> Project.

Nella finestra che appare abbiamo tutti i tipi di progetto utilizzabili. Nel tutorial precedente abbiamo utilizzato lo "Starter Kit" di SpaceWar, questa volta serviamoci del "Windows Game". Poco sotto decidiamo il Nome del progetto (es. HelloWorld) e in "location" la posizione dove verranno fisicamente salvati i files.

Notiamo subito una lieve differenza rispetto all'ambiente che ci propinava lo starter kit, ovvero, nel Solution Explorer mancano un bel po' di file, ma non preoccupatevi, c'è una spiegazione: i file non ci sono semplicemente perchè... li dobbiamo creare noi!

Comunque, seppur poco, analizziamo il materiale disponibile:

  • Cartella "Properties" con all'interno il file AssemblyInfo.cs, che contiene le informazioni di "impacchettamento" dell'applicazione (autore, copyright, versione e altre informazioni che adesso possiamo tralasciare);
  • Cartella "Reference" che contiene una copia delle librerie di cui ci serviremo;
  • Game.ico, la magnifica icona del joypad Xbox 360 che darà un volto all'eseguibile del nostro gioco;
  • Program.cs, il vero e proprio "entry point" del gioco, il punto dove tutto ha inizio;
  • Game1.cs, il "campo" su cui si svolgerà tutto il gioco.

Oltre a questi file, per il nostro esempio ne andremo a creare altri, ma vale la pena esaminare prima quelli che troviamo già:

Program.cs

using System;

namespace HelloWorld
{
  static class Program
  {
     /// <summary>
     /// The main entry point for the application.
     /// </summary>
     static void Main(string[] args)
     {
       using (Game1 game = new Game1())
       {
         game.Run();
       }
     }
   }
}

Nella prima parte troviamo, preceduti dalla parola chiave "using", le librerie del framework che utilizzeremo all'interno di questo file di classe. In questo caso, System è l'unico componente referenziato, ma come vedremo in seguito, più operazioni compiremo e più saranno le librerie di cui ci serviremo.

Il namespace determina il "gruppo" di classi a cui apparterrà la nostra classe. Noterete che tutte le classi utilizzeranno lo stesso namespace.
Viene dichiarata quindi la nostra classe, "static class Program", e il punto di partenza, ovvero il "Main".
Nel punto di partenza viene creata una nuova istanza dell'oggetto game di tipo "game1", che adesso andremo a vedere, e ne viene lanciata la funzione "Run", che darà vita a tutto.

Game1.cs

La classe Game1 la si può definire come il ciclo di vita del gioco. Ciclo che, una volta avviato tramite l'istruzione Run (lanciata su una sua istanza in Program.cs), continuerà a "looppare" fino a che non verrà deciso di interromperlo e il gioco terminerà.

Esaminiamo velocemente anche il suo contenuto. Come in precedenza, in cima a tutto c'è la regione "using statements" (#region serve semplicemente a raggruppare in modo ordinato il codice) dove dichiariamo che tecnologie (Namespaces), tra quelli già collegati al nostro progetto, devono essere "riconosciute". Possiamo già notare che oltre ad essere aumentate, sono anche più specifiche e appartenenti al framework di XNA per la gestione dell'audio, delle grafica, delle periferiche di input, etc.

Utilizzando anche quì il namespace "HelloWorld" si farà in modo che tutte queste classi, una volta compilato il gioco, vengano inserite all'interno dello stesso gruppo di classi.
Dichiariamo quindi la classe Game1 indicandone il tipo come Microsoft.Xna.Framework.Game che è la classe principale del framework che conterrà la logica del gioco ed il codice di rendering.

Nella prima parte della classe viene "piazzata" la dichiarazione di due componenti essenziali che ci accompagneranno in ogni gioco che realizzeremo con XNA: un GraphicsDeviceManager incaricato della gestione del rendering degli oggetti grafici e un ContentManager, componente run-time che si occupa di prendere gli oggetti come sono stati dichiarati (tradotti in binario al momento della compilazione) e renderli fruibili al run-time (momento in cui il gioco è in esecuzione).

Quest'ultimo oggetto si occupa anche del processo di vita di tale oggetto, ovvero ne gestisce il carico e lo scarico dalla memoria.
Nel costruttore della classe creiamo due nuove istanze dei suddetti oggetti in modo da renderli disponibili.
Il codice che rimane sono cinque funzioni che eseguono l'override di altrettante funzioni già esistenti nella classe-madre Game (e che la nostra, di conseguenza, ha ereditato); l'override letteralmente è la sovrascrittura di queste (ovvero verranno eseguite queste nuove funzioni al posto di quelle "originali").

La funzioneInitialize viene eseguita come prima funzione che viene chiamata subito dopo la creazione della classe di tipo Game e dopo la creazione di un GraphicsDevice (in questo specifico caso non fa altro che "annullare" se stessa, richiamando la funzione originale: base.Initialize(); dove, con la parola chiave "base", si fa esplicito riferimento alla classe "madre", ovvero Microsoft.Xna.Framework.Game e quindi alla sua funzione "Initialize" e non all'omonima in cui ci troviamo).

LoadGraphicsContent viene utilizzata per specificare nuovi oggetti grafici e le eventuali caratteristiche. Ora non carica nulla, ma per la personalizzazione del nostro HelloWorld andremo a "farcirla" a dovere, mentre la UnloadGraphicsContent servirà per far pulizia degli oggetti precedentemente caricati.

Update, come suggerisce la parola, servirà ad aggiornare tutto il "mondo virtuale" del nostro videogame richiamando ad esempio le funzioni di Update delle proprietà degli sprite, oggetti 3D, suoni, etc.

Funziona similmente anche la funzione Draw all'interno della quale si richiameranno le funzioni prettamente di aggiornamento grafico.

Con questa breve, e molto teorica, panoramica abbiamo visto i punti essenziali del ciclo vita di un videogioco. Ma quindi, la spontanea domanda che ci poniamo è: il nostro videogioco è già possibile eseguirlo?

Facciamo rispondere il fidato compilatore XNA andando a premere la freccetta verde del debug... ecco... cominciamo con le odiate schermate blu di Windows... dobbiamo aver sbagliato qualcosa...

Però in effetti, non è il solito blu... e non ci sono messaggi d'errore... ma quindi? Semplicemente stiamo vedendo la rappresentazione di ciò che abbiamo realizzato: un videogioco che non fa esattamente niente (ah... su sfondo azzurro...)!
Non ci credete, vero? Andiamo a provare con mano allora; se è vero che il gioco è in esecuzione in debug, come vediamo specificato nella parte bassa della barra degli strumenti dell'ambiente di sviluppo, vuol dire che è possibile debuggare. Proviamo...

Senza chiudere l'applicazione in esecuzione HelloWorld, torniamo a visualizzare il file Game1.cs e nella funzione Draw mettiamo un break point (posizioniamo il cursore in corrispondenza della riga interessata e premiamo F9). Notiamo subito che la linea viene evidenziata in giallo, questo sta a significare che l'esecuzione dell'applicativo è ora fermo esattamente in quel punto.

Il fatto che si sia istantaneamente fermato in quel punto fa intuire che questa funzione Draw sia utilizzata in modo molto assiduo dal programma. A conferma di ciò possiamo ricliccare sul tasto verde del debug, in modo da far ripartire il programma. Pare non sia accaduto nulla... in realtà è stato compiuto un'altro "ciclo" completo per l'applicazione ed è ritornata all'esatto punto in cui abbiamo inserito il break point. Non ne siete ancora convinti? Eseguiamo un'operazione che convincerà anche i più scettici e ci illustrerà meglio le potenzialità del debug e le operazioni a run-time.

La riga su cui abbiamo "stoppato" il nostro programma serve a ripulire lo schermo dagli oggetti presenti (che verranno poi ridisegnati con le eventuali nuove caratteristiche) e come parametro è possibile passargli un colore d'ambiente, ora è impostato un .CornflowerBlue (o, come uso definirlo io, "azzurro fastidio"). Proviamo a cambiare questo colore con Color.Brown, poi togliamo il break point e facciamo ripartire il debug. Ora il colore di fondo si è modificato.

Entriamo nel vivo...

A questo punto, aggiungeremo tutto quello che ci occorre per terminare il nostro primo gioco.

Il nostro obiettivo è quello di creare uno sprite (oggetto a due dimensioni) che rappresenti graficamente la scritta "HelloWorld" su uno sfondo sempre a due dimensioni e magari un po' meno anonimo del quadrato monocromatico che abbiamo adesso.

Cominciamo partendo dal file Game1.cs.
Oltre agli oggetti GraphicsDevice e il ContentManager, dobbiamo quindi creare un oggetto che servirà a gestire uno sprite e lo creiamo grazie a questa dichiarazione:

SpriteBatch spriteBatch;

Un oggetto di tipo SpriteBatch ci permette di gestire gruppi di sprite.
Poi dichiariamo un oggetto Texture2D che utilizzeremo come sfondo e un Rectangle sul quale "applicheremo" quest'ultimo:

Texture2D background;
Rectangle backgroundRectangle;

Come ultima cosa manca lo sprite, oggetto di rappresentazione 2D, protagonista del nostro primo progetto. Essendo un oggetto a tutti gli effetti, il modo più elegante per crearlo è quello di costruire una classe che ne contenga tutto il "comportamento".

Andiamo dunque a cliccare col tasto destro sul progetto "Helloworld" nel "solution explorer", poi su "add" -> "new item", selezioniamo "class" e diamogli un nome, ad esempio "Sprite.cs".
Includendo anch'essa nel namespace "Helloworld" creiamo delle proprietà essenziali dello sprite, ovvero posizione e velocità, e per colorare un po' il nostro sprite aggiungiamo anche il colore.
Tutte le proprietà sono riferite ad un oggetto bidimensionale, quindi ambedue verranno dichiarate di tipo "vector2", ovvero vettore bidimensionale.

private Vector2 _position;

public Vector2 Posizione
{
  get
  {
    return _position;
  }
  set
  {
    _position = value;
  }
}

private Vector2 _velocity;

public Vector2 Velocità
{
  get
  {
    return _velocity;
  }
 set
  {
    _velocity = value;
  }
}

private Color _color;

public Color Colore
{
  get
  {
     return _color;
  }
  set
  {
    _color = value;
  }
} 

Nota: utilizzo modi distinti per nominare proprietà pubbliche e private, dove le private sono scritte con la prima lettera minuscola e preceduti da "_", mentre le pubbliche con la prima lettera maiuscola. Per rimarcare questa differenza, le variabili interne alla classe hanno un nome in inglese mentre quelle pubbliche in italiano, in modo che si possa notare come i metodi esposti dalla classe "parlano" italiano, mentre internamente la classe dialoga in inglese. Ovviamente questo è solo a scopo didattico, in produzione è corretto mantenere una "naming convention" univoca. Questo argomento lo approfondiremo nei prossimi tutorial.

Prima di poter istanziare gli oggetti dichiarati, mancano ancora 2 elementi che sono un oggetto texture2D che verrà caricato a runtime di volta in volta e un oggetto di tipo Viewport, che rappresenta il nostro"punto di vista". Notate che le ultime 2 proprietà sono dichiarate come "static", quindi "pronte all'uso", non faranno riferimento solo ad un'instanza di un oggetto di questa classe ma saranno comuni a tutte le istanze di questa classe.

Ora passiamo ad utilizzare quanto dichiarato.

Creiamo il costruttore e ne approfittiamo per definire il valore di default di alcune proprietà:

public Sprite()
 {
  _color = new Color(0, 0, 0, 128); 
  _position = new Vector2(15f, 0f);
  _velocity = new Vector2(1f, 1f);
 }

_color, di tipo color definito secondo gli standard RGB (Red, Green, Blue), il quarto parametro è l'alpha che ne gestisce la trasparenza. Per ogni parametro i valori vanno da 0 a 255 compreso: ad esempio, per la massima trasparenza impostiamo 0 alpha, mentre 255 per avere colore pieno, definito dall'RGB (nel nostro caso 0,0,0 cioè "nero").

_position, come abbiamo detto in precedenza, è un vettore a 2 dimensioni, i cui parametri, rispettivamente l'asse x e y, due float (virgola mobile). Per capire meglio cosa significa il valore 15, 0 osserviamo lo schema qui sotto:

Il punto 0,0 è il punto più alto a sinistra dello schermo.
Lo sprite quadrato ha posizione 400, 200 che si riferisce al suo punto più alto a destra. 400 è la distanza lungo l'asse X (orizzontale) dal lato sinistro del bordo dello schermo, mentre 200 la distanza dello stesso punto ma lungo l'asse Y (verticale) rispetto al bordo alto dello schermo.

800, 600 è il punto più lontano (il più in basso a destra) dal punto d'origine e coincide con la risoluzione dello schermo (800x600 appunto).

_velocity è anch'esso un vettore bidimensionale i cui valori x e y verranno sommati ai valori x e y della nostra posizione. Questa somma vettoriale rappresenterà il "passo" del nostro sprite, ovvero la distanza minima che percorrerà ad ogni suo update.

Abbiamo parlato di update? Andiamo a vedere allora cosa faremo accadere quando verrà richiamata la void update:

public void Update()
{
  _position = _position + _velocity;
  //Se lo sprite è oltre il lato sx dello schermo o se è oltre il 
  //lato dx (calcolando anche la larghezza dello sprite)
  if(_position.X < 0 || _position.X > GraphicsViewport.Width - Texture.Width)
  {
    _velocity.X = -_velocity.X;
    _position.X = _position.X + _velocity.X;
  }
  //Se lo sprite andato oltre il lato superiore dello schermo o se andato 
  //oltre il lato inferiore (calcolando anche l'altezza dello sprite)
  if(_position.Y < 0 || _position.Y > GraphicsViewport.Height - Texture.Height)
  {
    _velocity.Y = -_velocity.Y;
    _position.Y = _position.Y + _velocity.Y;
  }
}

Come spiegato nei commenti, andiamo a sommare il vettore velocità (dove necessario con valore in modo negativo, con un "-"). Come abbiamo osservato nel grafico di prima, la posizione di uno sprite sullo schermo è compreso tra il punto 0, 0 e il punto "di fondo scala", che corrisponde alla risoluzione dello schermo, ma questi coincidono anche alle misure dell'oggetto viewpoint del nostro programma. Perciò possiamo recuperare i valori "di fondo scala" da questo oggetto.

Ricordiamoci di passare questi valori alla proprietà statica GraphicsViewport all'interno della nostra classe.

Da notare che, differentemente da quando andiamo a controllare che il valore x della _position non sia inferiore a 0, ovvero che non abbia oltrepassato il lato sinistro dello schermo, per quanto riguarda il lato destro occorre tenere conto anche "dell'ingombro" dello sprite stesso.
Bene, ci manca solo di implementare la funziona draw e la nostra classe è pronta per il primo step:

public void Draw(SpriteBatch _spriteBatch)
{
  _spriteBatch.Draw(Texture, Posizione, Colore);
}

Il parametro _spriteBatch di tipo SpriteBatch implementa la il metodo Draw per disegnare il nostro sprite con i seguenti parametri:
"Texture" che ne rappresenta la parte figurativa (proprietà statica che valorizzeremo nella classe richiamante), "_position" e _color già viste in precedenza.

Ok, la nostra classe Sprite è pronta. Torniamo nella classe Game1.cs.

Come prima cosa, creiamo l'istanza della neonata classe:

Sprite MioSprite = new Sprite();

passiamo alla LoadGraphicsContent

protected override void LoadGraphicsContent(bool loadAllContent)
{
  if(loadAllContent)
  {
    MioSpriteBatch = new SpriteBatch(this.graphics.GraphicsDevice);
    Sprite.Texture = content.Load<Texture2D>(@"images\Hello_World2");
    Sprite.GraphicsViewport = graphics.GraphicsDevice.Viewport; 
  }

  // TODO: Load any ResourceManagementMode.Manual content
}

Nuova istanza per uno SpriteBatch e lo chiamiamo MioSpriteBatch.

Vi ricordate le proprietà statiche della classe Sprite.cs? Bene, le valorizziamo qui: a Texture viene caricata un'immagine di tipo .png che abbiamo preventivamente inserito nel progetto in una nuova cartella "images", mentre a GraphicsViewport vengono associati i valori del Viewport dell'applicazione.

Dopo aver caricato le parti grafiche preoccupiamoci di aggiornare lo stato dello sprite in modo che venga aggiornata ad ogni ciclo la sua posizione con questa semplice istruzione all'interno della funzione Update:

MioSprite.Update();

Ultima operazione, come abbiamo fatto per la classe sprite, terminiamo con la funzione Draw che disegnerà tutta la scena dopo l'Update:

protected override void Draw(GameTime gameTime)
{
  MioSpriteBatch.Begin();
  graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
  MioSprite.Draw(MioSpriteBatch);
  MioSpriteBatch.End();
  base.Draw(gameTime);
}

Tra l'istruzione di Begin dello SpriteBatch e la funzone End, eseguiamo prima la pulizia di tutto lo schermo e poi il disegno dello sprite.

Verifichiamone il risultando eseguendo il programma.

Ecco, il nostro "Helloworld" ha preso vita!

Con quello che abbiamo imparato in questo tutorial (e con qualche riga in più di codice) possiamo produrre qualche effetto carino come l'effetto ombra (che non è altro che uno sprite nero semitrasparente che si muove "sotto" all'originale spostato di pochi pixel), la visualizzazione di un background, sprite multipli su schermo, colori random e anche il suono (questo argomento sarà trattato nei prossimi tutorial). Il risultato ottenuto potrebbe assomigliare a questo:

Il codice del progetto, sia per Windows sia per Xbox 360, è scaricabile cliccando sul link seguente:

Scarica l'esempio allegato