Questo “libro” web è un estratto di un’opera in corso di lavorazione modificato e semplificato per renderlo fruibile da un’utenza più generale. Spero che gradiate questa pubblicazione.
Non è cosa semplice trovare il tempo per continuare questa attività... abbiate pazienza e tenete d'occhio il sito.
Questa "pagina di copertina" contiene i link ai vari capitoli.
2. Il primo progetto
Assumendo che abbiate installato quanto vi serve siete ora in grado di creare un gioco XNA.
Attenzione! Dotatevi anche di un buon programma per le grafiche 2D, tipo
Paint.Net (è gratuito),
Corel Paint Shop Pro Photo X2 (economico ed eccellente, soprattutto la versione Ultimate),
Adobe Photo Shop CS4 Extended (eccellente ma veramente caro), oppure
GIMP un'applicazione poco conosciuta agli utenti Windows perchè nata per Unix, ma veramente molto efficace, direi che può essere paragonata a PhotoShop ma è gratuita e opersource.
Vi occorrerà anche (più avanti) un buon programma per la modellazione 3D. Qui diventa un problema serio poiché il solo programma decente gratuito è
Blender, ma ha problemi seri di esportazione compatibile con XNA (non conosco nessuno che sia riuscito ad ottenere qualche risultato serio con modelli di buona complessità) quindi la sola scelta effettivamente buona è quella di fare la sottoscrizione Premium all’
XNA CCO così (oltre che aver diritto a compilare sulla Xbox 360 e vendere i vostri giochi) da accedere all’offerta gratuita di
Autodesk SoftImage|XSI Mod Tool Pro (completo di librerie che semplificano l’uso nel gioco essendo
integrato con XNA Game Studio). Altri prodotti 3D sono seriamente cari e sempre di Autodesk (
Maya o
3ds Max, tra i due Maya è certamente il migliore).
Apriamo Visual Studio e creiamo un nuovo progetto per un Windows Game (3.1). Io l’ho chiamato PrimoProgetto, voi fate come volete, ma se volete che il Namespace sia lo stesso di quello che uso qui (per comodità di Copia & Incolla) allora chiamatelo così anche voi.
Come vedete il progetto appena creato contiene di base poche cose, quelle fondamentali:
· Le proprietà del progetto (Properties)
· Le referenze alle librerie usate (References)
· Un sotto-progetto per i contenuti audio, grafiche, modelli, effetti, ecc... (Content)
· Un file .ico che è l’icona assegnata all’eseguibile che sarà generato (Game.ico)
· Il programma del gioco vero e proprio (Game1.cs)
· La grafica di anteprima del gioco (GameThumbnail.png)
· Il pezzetto di codice di partenza (Program.cs)
Per questo primo progettino di prova non staremo a preoccuparci di rinominare la classe del gioco poiché il nostro obiettivo primario è capire come funziona la struttura e quali sono le principali manipolazioni con le quali dobbiamo familiarizzare.
2.1 La struttura della classe del gioco
Apriamo il file Game1.cs e vediamo cosa è stato creato per noi. Troviamo una classe chiamata Game1 che eredita dalla classe XNA Game. La nostra classe è composta di:
· Il suo costruttore: public Game1()
· Il metodo d’inizializzazione: protected override void Initialize()
· Il metodo che carica I contenuti: protected override void LoadContent()
· Il metodo che scarica I contenuti: protected override void UnloadContent()
· Il metodo di aggiornamento del gioco: protected override void Update(GameTime gameTime)
· Il metodo che disegna sullo schermo: protected override void Draw(GameTime gameTime)
Quando il programma viene eseguito la classe principale del gioco viene istanziata e le viene passato il controllo, in Program.cs troviamo infatti:
using (Game1 game = new Game1())
{
game.Run();
}
A questo punto cosa avviene esattamente quando viene chiamato il metodo Run (ereditato dalla classe Game di XNA) del gioco? Vediamolo di seguito in modo schematico. La classe in realtà non viene “eseguita” ma passata al Framework XNA che la prende in carico e la gestisce come segue:
1. Chiama il metodo Initialize()
2. Chiama il metodo LoadContent()
dopo di che entra in un ciclo infinito (che il gioco può interrompere chiamando il metodo Exit) a tempo fisso, cioè esattamente 60 volte al secondo esegue i metodi Update e Draw in sequenza. Se i metodi impiegano meno di 1/60 di secondo per terminare (cosa auspicabile) il Framework XNA attende prima di chiamarli di nuovo. C’è modo (lo vedremo più avanti) di svincolare il gioco dal passo fisso da 1/60 di secondo, ma ci sono molte controindicazioni per cui gli sviluppatori del mondo delle console (Xbox, PS, Wii, ecc...) preferiscono il passo fisso. In ambiente Windows molti preferiscono il passo libero.
Dunque quel che accade è che il Framework XNA esegue all’infinito
3. Chiama il metodo Update(GameTime gameTime)
4. Chiama il metodo Draw(GameTime gameTime)
Come potete notare a questi metodi viene passato un parametro contenente una serie di contatori di tempo (che vedremo poi in dettaglio).
Quando il gioco chiama il metodo Exit() per chiudere ed uscire XNA si comporta come segue:
5. Chiama il metodo UnloadContent()
6. Esce dal gioco.
Mi pare chiaro da quanto detto fino ad ora che il gioco vero e proprio, a parte le fasi di caricamento e scaricamento (all’inizio ed alla fine) si svolge al 100% in soli due metodi chiamati in sequenza con un passo temporale fisso di 1/60 di secondo: Update e Draw. Nel metodo Update metteremo tutta la logica di calcolo e gestione del nostro gioco, mentre nel metodo Draw metteremo tutta la logica di disegno a video delle nostre grafiche.
Semplicissimo no? La struttura logica è veramente di elementare comprensione, chiara e semplice, quindi commettere errori è abbastanza difficile a meno che non ci si complichi la vita (cosa però necessaria) usando processi paralleli (multi-threading) e componenti di gioco (GameComponent e DrawableGameComponent), ma di queste cose parleremo più avanti. Per adesso concentriamoci sui pilastri logici dello sviluppo di un semplice gioco, il resto verrà da sé e sarà di facilissima comprensione se avremo costruito delle solide fondamenta conoscitive.
2.2 Le variabili membro principali
Diamo adesso un’occhiata alle variabili membro della nostra classe:
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Queste sono di fatto il cuore gestionale del nostro gioco. La prima ci permette di gestire fisicamente le caratteristiche della nostra interfaccia con la scheda video, la seconda ci permette fisicamente di disegnare qualcosa sullo schermo. Vediamo come vengono inizializzate.
Il gestore dell’interfaccia grafica viene istanziato all’interno del costruttore stesso della classe:
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
Questo è un passo fondamentale perché ci permette di prendere il controllo dell’interfaccia video. Come vedete nel costruttore passiamo come parametro this che altro non è che l’istanza corrente della classe del gioco; questo lega il nostro gioco ad un determinato gestore logico della scheda grafica.
Notiamo anche come nel nostro costruttore venga anche inizializzata una proprietà della classe stessa:
Content.RootDirectory = "Content";
così facendo diciamo alla nostra istanza del ContentManager (il gestore dei contenuti grafici del nostro gioco) qual è il percorso radice (sul disco) dove troverà i contenuti. Questo ci permetterà in seguito di specificare i percorsi in maniera relativa alla radice, evitandoci lunghe stringhe ed errori di percorso. Nel nostro caso vediamo che lo stesso inizializzatore della radice è un percorso relativo (infatti non contiene c:\...\...) il che vuol dire che esiste una cartella Content proprio nel punto ove il nostro gioco è installato. Tale cartella altro non è che il risultato del nostro sotto-progetto Content.
Andando oltre troviamo l’inizializzazione dell’altro nostro membro, questa volta nel metodo LoadContent():
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
// TODO: use this.Content to load your game content here
}
La ragione per cui questo avviene nella LoadContent è che abbiamo dovuto lasciare il tempo al gestore della scheda video di inizializzarsi e legarsi alla nostra classe alimentando così la nostra proprietà GraphicsDevice che come vedete viene poi passata come parametro al costruttore della nostra istanza dello SpriteBatch.
Mano a mano, con il loro utilizzo concreto, vedremo cosa ci viene messo a disposizione dai tre oggetti che abbiamo così visto inizializzare, GraphicsDeviceManager, ContentManager e SpriteBatch.
2.3 Operazioni di base
Vediamo innanzi tutto da un punto di vista operativo quali logiche di gioco e di disegno sono state create per noi nel progetto di base. Seguendo lo stesso schema di pensiero di XNA cominciamo dal metodo Update.
protected override void Update(GameTime gameTime)
{
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
// TODO: Add your update logic here
base.Update(gameTime);
}
Come si nota esiste già un’istruzione che serve a permettere al gioco di terminare. L’istruzione è abbastanza chiara di per sé: se il tasto Back del Gamepad collegato sulla Porta Uno è premuto allora esci dal gioco. Qui chiaramente si assume che chi usa il gioco abbia collegato un GamePad Xbox 360 sulla porta 1... In futuro elimineremo del tutto questa pare di codice per scriverne una migliore e più efficiente, ma per il momento ci va bene così, ma magari ci conviene fare una piccola aggiunta tanto per essere un po’ più amichevoli con chi sul PC ha la tastiera ma non un GamePad Xbox 360 per PC! Penso che sia una buona idea (per adesso) definire che premendo il tasto Escape si ottenga lo stesso risultato, quindi modifichiamo il test come di seguito:
if ((GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) ||
Keyboard.GetState().IsKeyDown(Keys.Escape))
this.Exit();
Ora che abbiamo modo di uscire anche con la tastiera passiamo oltre. Che abbiamo nel Draw?
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
// TODO: Add your drawing code here
base.Draw(gameTime);
}
Nulla di speciale davvero! Semplicemente un’istruzione che dice al gestore della grafiica di cancellare l’area di lavoro a video e renderla uniformemente di un colore azzurrino. Avrete notato che sia Update che Draw terminano richiamando il proprio metodo omologo nella classe di base, cosa fondamentale per la corretta esecuzione del gioco.
Se ora eseguissimo il gioco cosa accadrebbe? Beh... come abbiam visto ogni sessantesimo di secondo il nostro gioco controlla se abbiamo premuto un tasto (Back sul Gamepad o Escape sulla tastiera) per uscire dal gioco, dopo di che si limita a disegnare uno sfondo azzurrino.
Se premete il tasto F5 (esecuzione del gioco in modalità di Debug) ecco cosa vedrete:

Ovviamente premendo Escape sulla tastiera il gioco si chiude. Direi che il tutto è chiaro e semplice fin qui. Notate che indipendentemente dalla risoluzione del vostro schermo avete ottenuto una finestra di gioco 800 per 600 (quindi un rapporto televisivo 4:3).
2.4 Capire il rapporto video e le risoluzioni - il BackBuffer
Qui diventa necessario (prima di proseguire) mettere qualche paletto fondamentale alla comprensione del modello logico di gestione grafica che c’è dietro XNA. Abbiamo visto che il nostro gioco in mancanza di altre specifiche assume una risoluzione 600p (800x600, quindi 4:3). Questa è la risoluzione predefinita del BackBuffer.
Questa è una cosa fondamentale da capire: il Backbuffer è di fatto lo schermo con cui lavoriamo. Quando disegnamo una texture (cioè una grafica 2D) la posizioniamo sempre in rapporto al BackBuffer in uso, non rispetto all’effettiva risoluzione dello schermo! Insomma, il nostro punto P(x, y) in cui (ad esempio) x=20 e y=10 si troverà fisicamente in posti diversi sullo schermo al variare della risoluzione del BackBuffer.
Questo è importante perché quando lavoriamo per la Xbox 360 dobbiamo ricordare che il nostro gioco verrà automaticamente scalato in base alla risoluzione video in uso sulla console dell’utente. È quindi importante sapere che se il framework dovrà scalare un gioco progettato in 4:3 (600p) su uno schermo in 16:9 il risultato sarà deformato, mentre se scriveremo il gioco in 16:9 impostando il BackBuffer a 720p (1280x720) e l’utente ha un televisore standard (quindi 480p, cioè 640x480) il framework rimpicciolirà il nostro gioco ed aggiungerà delle strisce nere sopra e sotto. Importante: se il gioco è progettato in 16:9 usando una risoluzione 1080p (1920x1080) e l’utente ha un SDTV (TV standard) il framework non riuscirà a scalarlo in 480p quindi il gioco non funzionerà!
Alcuni giochi sono giocabili se il sistema scala in 480p, altri no, tutto dipende dal tipo di gioco e di dettaglio necessario. Il solo modo per essere certi di non aver alcun problema è quello di preparare il nostro gioco usando un doppio set di grafiche (600p e 720p) e gestirle propriamente nel nostro gioco.
Di qui in avanti implementeremo questo approccio, facendo in maniera che il nostro gioco sia in grado di gestire tutte e due le situazioni (4:3 e 16:9) usando le due risoluzioni intermedie di progettazione consigliate da Microsoft: 600p per il 4:3 e 720p per il 16:9. Per fare questo dobbiamo aggiungere un test durante l’inizializzazione del gioco, in questo modo:
All’inizio della nostra classe aggiungiamo altre tre variabili membro dopo le dichiarazioni esistenti:
int LarghezzaPreferita;
int AltezzaPreferita;
bool Widescreen;
poi all’inizio della LoadContent aggiungiamo il codice per riconoscere il tipo di formato ed impostare il nostro BackBuffer di conseguenza. Questo può essere fatto in due modi.
Primo modo:
if (GraphicsAdapter.DefaultAdapter.IsWideScreen)
{
LarghezzaPreferita = 1280;
AltezzaPreferita = 720;
Widescreen = true;
}
else
{
LarghezzaPreferita = 800;
AltezzaPreferita = 600;
Widescreen = false;
}
Secondo modo:
if (graphics.GraphicsDevice.DisplayMode.Height >= 720)
{
LarghezzaPreferita = 1280;
AltezzaPreferita = 720;
Widescreen = true;
}
else
{
LarghezzaPreferita = 800;
AltezzaPreferita = 600;
Widescreen = false;
}
Dopo questo test (uno dei due modi) applichiamo le modifiche:
graphics.PreferredBackBufferWidth = LarghezzaPreferita;
graphics.PreferredBackBufferHeight = AltezzaPreferita;
graphics.ApplyChanges();
A questo punto abbiamo preso in carico il formato dello schermo. La differenza tra il primo ed is secondo modo è semplice: il primo modo imposta il 16:9 esclusivamente se la scheda video effettivamente è impostata in 16:9, indipendentemente dal fatto che potremmo avere un video di tipo differente (sul PC ovviamente) e quindi saremmo in grado di vedere in 720p anche se non abbiamo un monitor 16:9.
Il secondo metodo è un po’ più flessibile, e quindi imposta la visualizzazione in 16:9 (a 720p) semplicemente se la nostra scheda video è in grado di visualizzare in 1280x720, cioè se l’altezza massima del nostro schermo è minimo 720 pixel.
Nel nostro esempio io preferisco usare il secondo sistema poiché lo trovo appunto più flessibile. La variabile membro Widescreen ci servirà in seguito per sapere in quale dei due modi stiamo lavorando.
A questo punto il nostro metodo LoadContent nel suo complesso risulterà come segue:
protected override void LoadContent()
{
if (graphics.GraphicsDevice.DisplayMode.Height >= 720)
{
LarghezzaPreferita = 1280;
AltezzaPreferita = 720;
Widescreen = true;
}
else
{
LarghezzaPreferita = 800;
AltezzaPreferita = 600;
Widescreen = false;
}
graphics.PreferredBackBufferWidth = LarghezzaPreferita;
graphics.PreferredBackBufferHeight = AltezzaPreferita;
graphics.ApplyChanges();
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
// TODO: use this.Content to load your game content here
}
2.5 La nostra prima grafica (4:3 e 16:9) e miglioramento dell’Input – Parte 1
Nella nostra situazione attuale abbiamo un gioco capace di riconoscere il rapporto di grafica da usare e comportarsi di conseguenza, ma ora, prima di aggiungere la nostra prima grafica, sarà meglio che durante il Debug ci mettiamo in condizione di poter passare da 16:9 a 4:3 mentre il gioco è in funzione, così da poter testare le varie situazioni senza dover uscire dal gioco. Per ottenere questo dobbiamo aggiungere del codice nel nostro metodo Update. Diciamo che per cambiare risoluzione vogliamo usare il bottone Y del GamePad oppure il tasto F12 della tastiera. Ecco cosa aggiungeremo nel nostro metodo di update:
#if DEBUG
if ((GamePad.GetState(PlayerIndex.One).Buttons.Y == ButtonState.Pressed) ||
Keyboard.GetState().IsKeyDown(Keys.F12))
{
if (Widescreen)
{
LarghezzaPreferita = 800;
AltezzaPreferita = 600;
Widescreen = false;
}
else
{
LarghezzaPreferita = 1280;
AltezzaPreferita = 720;
Widescreen = true;
}
graphics.PreferredBackBufferWidth = LarghezzaPreferita;
graphics.PreferredBackBufferHeight = AltezzaPreferita;
graphics.ApplyChanges();
}
#endif
ATTENZIONE!!! Provate un po’ ad eseguire il codice (premendo F5) e provate a cambiare la risoluzione premendo F12 (o Y sul Gamepad)... che effetto vi fa? Da diventar matti direi... dovete riuscire a premere e rilasciare il tasto in 1/60 di secondo, altrimenti ve la cambia di nuovo... e di nuovo...
Come si ovvia a questo problema? Beh, dobbiamo introdurre il principio di “tasto premuto” e gestirlo nella nostra logica! Che vuol dire aver premuto un tasto? Semplice: prima era su ed ora è giù! Ok, quindi non basta che controlliamo che il tasto ora sia giù, ma dobbiamo accertarci che prima (1/60 di secondo prima) il tasto fosse su! Per fare questo dobbiamo aggiungere altre quattro variabili membro al nostro codice:
GamePadState statoCorrenteDelGamepad;
GamePadState statoPrecedenteDelGamepad;
KeyboardState statoCorrenteDellaTastiera;
KeyboardState statoPrecedenteDellaTastiera;
a cosa servono mi sembra chiarissimo, giusto? Ora cambiamo un po’ il contenuto della nostra Update per implementare questa cosa. Innanzitutto all’ingresso del metodo scriviamo:
statoCorrenteDelGamepad = GamePad.GetState(PlayerIndex.One);
statoCorrenteDellaTastiera = Keyboard.GetState();
(per ora continuiamo ad usare solo il gamepad 1, poi cambieremo questa cosa). Ed alla fine del metodo Update scriveremo:
statoPrecedenteDelGamepad = statoCorrenteDelGamepad;
statoPrecedenteDellaTastiera = statoCorrenteDellaTastiera;
in questo modo sapremo sempre quale fosse il precedente stato. Ora modifichiamo i nostri test prendendo in carico queste nuove informazioni:
#if DEBUG
if (((statoPrecedenteDelGamepad.Buttons.Y == ButtonState.Released) &&
(statoCorrenteDelGamepad.Buttons.Y == ButtonState.Pressed)) ||
(statoPrecedenteDellaTastiera.IsKeyUp(Keys.F12) &&
statoCorrenteDellaTastiera.IsKeyDown(Keys.F12)))
{
if (Widescreen)
{
LarghezzaPreferita = 800;
AltezzaPreferita = 600;
Widescreen = false;
}
else
{
LarghezzaPreferita = 1280;
AltezzaPreferita = 720;
Widescreen = true;
}
graphics.PreferredBackBufferWidth = LarghezzaPreferita;
graphics.PreferredBackBufferHeight = AltezzaPreferita;
graphics.ApplyChanges();
}
#endif
Se adesso eseguiamo il codice va molto meglio, vero? Passiamo ora al mettere una grafica sullo schermo. Cominciamo da qualcosa che possa farci capire meglio la questione delle proporzioni.
ATTENZIONE! Una volta che cominciamo ad avere delle grafiche il cambio di risoluzione se effettuato troppo in fretta provoca un errore generale. Lasciate passare un paio di secondi almeno tra un passaggio e l’altro!
Abbiamo qui (allegata a questa pagina) una grafica in 720p (di pubblico dominio). Create una cartella Textures all’interno del sotto-progetto Content e trascinate l’immagine in questa nuova cartella.
Nelle proprietà del file fate attenzione al nome (Asset Name): è il nome con il quale XNA identifica l’immagine, nel nostro caso l’immagine sfondo.jpg
Ora facciamo in modo di caricare lo sfondo. Creiamo una nuova variabile membro:
Texture2D sfondo;
e nel metodo LoadContent aggiungiamo:
sfondo = Content.Load<Texture2D>("Textures/sfondo");
mentre nel metodo UnloadContent scriveremo:
sfondo = null;
A questo punto la nostra immagine è stata caricata. Per metterla a video aggiungiamo quanto segue nel metodo Draw, subito dopo la cancellazione:
spriteBatch.Begin();
spriteBatch.Draw(sfondo, Vector2.Zero, Color.White);
spriteBatch.End();
Ora possiamo eseguire il programma. Notate che lo sfondo in 4:3 viene semplicemente tagliato?
2.5 La nostra prima grafica (4:3 e 16:9) e miglioramento dell’Input – Parte 2
Potremmo non voler rifare lo sfondo perché ci sta bene anche se è tagliato, ma se questo non fosse possibile perché magari l’intera immagine è necessaria al gioco? Allora avremmo 2 sole opzioni: 1) scalare l’immagine o 2) crearne una 800x600 che soddisfi le nostre necessità. Proviamo innanzi tutto ad investigare il funzionamento della prima opzione.
Il metodo Begin dello SpriteBatch ha una delle sintassi in cui è prevista la possibilità di passare una matrice rappresentante la scala di riproduzione degli oggetti da disegnare. Cominciamo con l’aggiungere una nuova variabile membro per la scala:
Matrix scala;
Per creare la matrice in maniera automatica ad ogni cambio di risoluzione la cosa migliore da fare è quella di creare un gestore per l’evento DeviceReset, come segue. Nel nostro metodo Initialize() aggiungiamo:
graphics.DeviceReset += new EventHandler(graphics_DeviceReset);
Visual Studio avrà creato per noi il metodo vuoto:
void graphics_DeviceReset(object sender, EventArgs e)
{
throw new NotImplementedException();
}
Che noi modificheremo come segue:
void graphics_DeviceReset(object sender, EventArgs e)
{
float scalaVideo = (float)graphics.GraphicsDevice.Viewport.Width / 1280f;
scala = Matrix.CreateScale(scalaVideo, scalaVideo, 1);
}
In pratica rapportiamo la risoluzione video corrente a 1280 che la nosta base di progettazione. Questa matrice la useremo per inizializzare le operazioni di disegno, quindi nella Draw cambieremo il spriteBatch.Begin() come segue:
spriteBatch.Begin(SpriteBlendMode.AlphaBlend,
SpriteSortMode.Immediate, SaveStateMode.None, scala);
Che cosa abbiamo ottenuto? Beh… ora l’immagine è scalata, quindi... ci ritroviamo con l’immagine correttamente a video ma una bella banda azzurrina (dello sfondo) nella fetta di schermo in basso perché ovviemente scalando l’immagine resta fuori un pezzo!
Questo sistema può essere buono per alcune grafiche, ma chiaramente abbiamo appena sperimentato che non si applica certo agli sfondi!
Esiste un’altra possibilità: ridimensionare senza scalare. Per fare questo invece di passare allo SpriteBatch un rapporto di scala passeremo alla sua Draw il rettangolo di destinazione dove vogliamo che l’immagine venga mostrata. Lo SpriteBatch ridimensionerà la grafica (non scalandola ma semplicemente facendocela stare!). Proviamo un po’ e vediamo cosa capita con questo approccio. Cambiamo ancora il nostro metodo Draw come segue:
spriteBatch.Begin();
spriteBatch.Draw(sfondo,
new Rectangle(0, 0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height),
Color.White);
spriteBatch.End();
Ok, cos’abbiamo otenuto ora? Più o meno quel che volevamo: l’immagine viene adattata alla risoluzione, ma è di conseguenza distorta. Nel caso dell’immagine che stiamo usando il problema è poco visibile quindi potremmo anche lasciare tutto così, ma purtroppo la maggior parte delle volte questo non è possibile e quindi non resta che preparare una grafica dedicata per ogni rapporto di risoluzione video, 4:3 e 16:9.
2.6 Qualche parola in più sulle Texture – Parte 1
Abbiamo visto che per caricare lo sfondo, che è un’immagine JPG in questo caso, l’abbiamo caricata in una variabile di tipo Texture2D. Vale la pena di spiegare un po’ meglio cosa è una Texture. Non starò qui ad approfondire tutti i formati (che son proprio tanti), ma certamente dobbiamo capire come sia rappresentato e trattato il formato più comune che ci servirà nel corso di questo primo progetto didattico.
Una Texture un insieme di unità grafiche minime (dette texel). Un texel, per farla semplice, è la descrizione completa di un pixel sullo schermo. Nel caso di immagini normali questa descrizione fondamentalmente espone (e ci permette di manipolare) una classe Color per ogni texel. A che ci serve saperlo? Ad un sacco di cose, dalla semplice manipolazione dell’immagine fino al rilevamento della collisione tra texture (per essere molto concisi!).
Allora a questo punto è il caso di fare un po’ di esperimenti pratici con la manipolazione delle texture. Per prima cosa impariamo a manipolare l’immagine in maniera semplice: vediamo come possiamo capovolgere l’immagine di fondo usata prima manipolando il contenuto della nostra variabile.
Cominciamo con il creare un nuovo metodo chiameremo CapovolgiSfondo (lo aggiungo dopo il metodo Draw):
private void CapovolgiSfondo()
{
int Altezza = sfondo.Height;
int Larghezza = sfondo.Width;
Color[] pixel0 = new Color[Altezza * Larghezza];
Color[] pixel1 = new Color[Altezza * Larghezza];
sfondo.GetData<Color>(pixel0);
for (int i = 0; i < pixel0.Length; i++)
{
pixel1[((Altezza * Larghezza) - 1) - i] = pixel0[i];
}
sfondo = new Texture2D(graphics.GraphicsDevice, Larghezza, Altezza);
sfondo.SetData<Color>(pixel1);
}
Cosa faccia questo metodo è abbastanza evidente, quello che va notato è che debbo creare una nuova Texture che assegno alla nostra variabile, questo perché quella esistente è già stata visualizzata e quindi non è più possibile usare il metodo SetData. Ora diciamo che premendo X (sulla tastiera o sul gamepad) vogliamo che l’immagine si capovolga. Aggiungiamo nella nostra Update, prima o dopo il test che attiva il cambio di risoluzione, questo codice:
if (((statoPrecedenteDelGamepad.Buttons.X == ButtonState.Released) &&
(statoCorrenteDelGamepad.Buttons.X == ButtonState.Pressed)) ||
(statoPrecedenteDellaTastiera.IsKeyUp(Keys.X) &&
statoCorrenteDellaTastiera.IsKeyDown(Keys.X)))
{
CapovolgiSfondo();
}
Ed ora tutto quel che dobbiamo fare è premere F5 per eseguire il nostro gioco e provare cosa capita premendo la X!
Continua...