giovedì 12 marzo 2009

7) Xna Videogame Tutorial: Testo e Collisioni

Questa settimana veramente a bomba.... 3 articoletti in due sere.. e se non sono distrutto dopo ne scrivo un altro! Tutto questo perchè settimana prossima non potrò scrivere e dato che inizio a vedere che qualche lettore mi fa spesso visita... cerco di non farlo scappare dando un pò di carne frescaaaaaaaaaaaaa ceh lo tenga occupato per un pò di giorni -.-

Ehm....dicevo......

In questo post tratterò due argomenti assolutamente interessanti, in primo luogo vi spiegherò come visualizzare delle stringhe di testo. Dunque utilizzeremo queste stringhe per tenere traccia dei punti vita del giocatore che diminuiranno se la nostra astronave colliderà contro una delle astronavi generate e "guidate" dal computer.
Potete partire dal codice dell'articolo precedente e come sempre trovate i sorgenti finali a fondo post.

Anteprima Video


Andiamo per gradi. Il primo passo è l'inserimento nei nostri Contents di un elemento chiamato spriteFont, che non è nient'altro che un XML con informazioni riguardanti un tipo di Font.
Anche in questo caso il buon Team di XNA è venuto incontro alle nostre esigenze, per inserire questo contenuto fate click sulla voce Content (nel frame di sinistra) quindi Add->new Item e scegliete SpriteFont, date un nome (io l'ho chiamato Arial) confermate e lo troverete magicamente disponibile tra i nostri contenuti (non spaventatevi se non vedete icone particolari... a me appare l'icona di undefined file..) .
Ora cliccate due volte sul file creato e si aprirà un bellissimo XML... arrivate fino alla riga con il tag : e scrivete Arial tra i tag come riportato qui sotto.
Arial
In questo modo abbiamo creato un riferimento al font Arial utilizzabile tramite XNA.

Ora vediamo come stamparedelle stringhe a monitor, copio qui sotto il codice del file Game1.cs
modificato per le nostre esigenze, analizziamolo:

GAME1

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;
using
Microsoft.Xna.Framework.Net;
using
Microsoft.Xna.Framework.Storage;

namespace
input
{

public class
Game1 : Microsoft.Xna.Framework.Game
{

GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;

int
life;//Vite del giocatore
SpriteFont gui_life;//GUI

public
Game1()
{

graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";

}

protected
override void Initialize()
{

SpriteManager spritemanager = new SpriteManager(this);
Components.Add(spritemanager);
life = 100;

base.Initialize();
}


protected
override void LoadContent()
{

// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);

//Carichiamo il font!
gui_life = Content.Load<SpriteFont>("Arial");
}


protected
override void UnloadContent()
{
}



protected
override void Update(GameTime gameTime)
{

// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this
.Exit();

base.Update(gameTime);
}


protected
override void Draw(GameTime gameTime)
{

GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
spriteBatch.DrawString(gui_life, "LIFE: " + life, new Vector2(10, 10), Color.White, 0, Vector2.Zero, 1,SpriteEffects.None,1);
spriteBatch.End();

// TODO: Add your drawing code here

base.Draw(gameTime);
}


//Gestisce le vite del player, se arrivano a zero esce
public int Life {
get { return life; }
set {
life = value;
if
(life == 0) {
Exit();
}
}
}

}
}


Prima di tutto noterete che ho agigunto due variabili di classe, un intero e un oggetto SpriteFont che servirà appunto per stampare la nostra stringa:
        int life;//Vite del giocatore

SpriteFont gui_life;//GUI
Il secondo Step lo vediamo nella initialize dove settiamo la vita iniziale a 100, mentre nella Update carichiamo il font in gui_life
            //Carichiamo il font!

gui_life = Content.Load<SpriteFont>("Arial");


Nella Draw utilizziamo uno SpriteBatch per stampare la stringa
spriteBatch.DrawString(gui_life, "LIFE: " + life, new Vector2(10, 10), Color.White);



Per la descrizione dei parametri di questa funzione vi rimando alla definizione della funzione, in breve vi dico che il primo parametro identifica lo spritefont da utilizzare (il nostro Arial di prima), il secondo paramentro indica cosa stampare, il terzo da un'indicazione di posizione della stringa e infine definiamo il colore con il quale stampare.
Tanto che siete su questo codice, date un'occhiata alla definizione di Life , tramite i paramtri set e get restituisce il valore di life .. e nel caso in cui il valore sia uguale a zero butta fuori dal programma (una sorta di grezzo gameover..) bene...a breve vedremo dove utilizzare questa definizione.

Compilando dovreste già essere in grado di vedere il testo in alto a destra :D

Ora passiamo al discorso collisioni!
Quello che vogliamo è far si che quando la nostra astronave collide contro una astronave generata, quest'ultima scompaia e i nostri punti vita diminuiscano.
Per far si che questo avvenga dobbiamo mettere mano unicamente allo spritemanager:
Ecco qui il nuovo codice per intero

SPRITEMANAGER
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;
using
Microsoft.Xna.Framework.Net;
using
Microsoft.Xna.Framework.Storage;


namespace
input
{

public class SpriteManager : Microsoft.Xna.Framework.DrawableGameComponent
{

SpriteBatch spriteBatch;
List<Sprite> spriteList = new List<Sprite>();

ControlledSprite player;

Texture2D tex1, tex2, texp;
Vector2 pos1, pos2, posp;
Vector2 spd1, spd2;

public
SpriteManager(Game game)
:
base(game)
{
}


public
override void Initialize()
{

// TODO: Add your initialization code here
pos1 = new Vector2(10.0f, 1.0f);
pos2 = new Vector2(30.0f, 2.0f);
//Aggiugngo una posizione iniziale per il giocatore... altrimenti collide da subito con gli sprite
posp = new Vector2(500.0f, 400.0f);
spd1 = new Vector2 (2.0f,1.0f);
spd2 = new Vector2(0.0f, 2.0f);

base.Initialize();
}


protected
override void LoadContent()
{

tex1 = Game.Content.Load<Texture2D>("enemy");
tex2 = Game.Content.Load<Texture2D>("enemy2");
texp = Game.Content.Load<Texture2D>("spaceship");
spriteBatch = new SpriteBatch(Game.GraphicsDevice);
//ho estratto il giocatore dalla lista di sprite
//spriteList.Add(new ControlledSprite(texp, new Vector2(0.0f, 0.0f)));
player = new ControlledSprite(texp, posp, spd1);
spriteList.Add(new AutoSprite(tex1,pos1,spd1));
spriteList.Add(new AutoSprite(tex1, pos2, spd2));
spriteList.Add(new AutoSprite(tex2, new Vector2(300.0f,0.0f), new Vector2 (1.0f,3.0f)));
base.LoadContent();
}


public
override void Update(GameTime gameTime)
{

//Aggiorno il giocatore
player.Update(gameTime);

//Aggiorno tutti gli elementi della lista, al posto del foreach usiamo un for
for (int i = 0; i < spriteList.Count; i++) {
Sprite s = spriteList[i];

s.Update(gameTime);
//Controllo se uno degli sprite nemici collide contro il giocatore
if (s.collisionRect.Intersects(player.collisionRect)) {
//Tolgo una vita al player
--((Game1)Game).Life;
spriteList.RemoveAt(i);
}
}

base.Update(gameTime);
}


public
override void Draw(GameTime gameTime)
{

spriteBatch.Begin();
//Disegno il giocatore
player.Draw(gameTime,spriteBatch);

//Disegno tutti gli elementi della lista
foreach (Sprite s in spriteList)
{

s.Draw(gameTime,spriteBatch);
}

base.Draw(gameTime);
spriteBatch.End();
}

}
}

Bene, ho un pò di novità da farvi notare (le ho evidenziate sopra in Grassetto)
Prima di tutto ho estrapolato il giocatore dalla lista di sprite dello spritemanager, preferisco gestirlo a parte (questo tornerà utilie quando dovremo fare il chekc della lista sprite per vedere se ci sono collisioni).
In secondo luogo ho modificato la sua posizione di partenza nella initialize
posp = new Vector2(500.0f, 400.0f);
Nella load content ho commentato la riga dove inserivo il player nella lista di sprite e l'ho sostituita con una nuova che associa all'oggetto "Player", definito in testa alla classe, il nuovo oggetto ControlledSprite
//spriteList.Add(new ControlledSprite(texp, new Vector2(0.0f, 0.0f)));

player = new ControlledSprite(texp, posp, spd1);
Nella update sostituiamo il precedente foreach che percorreva la lista con un for (sarà più comodo per fare riferimento alla posizione degli elementi nella lista)
for (int i = 0; i < spriteList.Count; i++) {

Sprite s = spriteList[i];......
All'interno del for a questo punto troverete il blocco che si occupa di verificare se uno degli sprite collide con il player ( che abbiamo appositamente estrapolato da questa lista)

                //Controllo se uno degli sprite nemici collide contro il giocatore

if (s.collisionRect.Intersects(player.collisionRect)) {
//Tolgo una vita al player
--((Game1)Game).Life;
spriteList.RemoveAt(i);
}
Avevamo definito tramite collisionRect nella classe Sprite.cs (riporta semplicemente la posizione occupata dallo sprite), e utilizzando la funzione intersect di XNA controlleremo se avviene l'intersezione con il nostro player... in tal caso facciamo un bel casting (almeno credo)
--((Game1)Game).Life;
e raggiungiamo la Life che vi ho mostrato prima e sfruttando la proprietà set decrementiamo punti dall'intero life.
a quest opunto rimuoviamo l'elemento con il quale è avvenuta la collisione dalla lista di sprite da stampare.

Signori e signori... abbiamo già finito...
potrete vedere che muovendo la vostra astronave contro uno sprite questo sparirà e il vostro punteggio vita si abbasserà!

Codici sorgenti finali:
4.collisioni.zip

4 commenti:

arema ha detto...

Ho capito quasi tutto (credo), l'unica difficoltà ce l'ho nel cercare di capire il comando:

--((Game1)Game).Life;

Finora ho vista fare casting "singoli", questo non riesco a capire come funziona e se è l'unico modo per poter decrementare il valore di Life...

Francesco_96 ha detto...

Complimenti per il tutorial, veramente bellissimo! Una nota, invece di utilizzare quel casting molto complesso si può fare in un altro modo, e cioè dichiarare la variabile pubblica e statica, così:

public static int Life;

e modificarla così:

--Game1.Life;

Ciao.

Anonimo ha detto...

Ottimo tutorial.
Unica nota: non dovrebbe essere lo sprite manager a modificare il valore del campo Life, almeno non direttamente. Logicamente dovrebbe farlo il game. Lo sprite manager dovrebbe scatenare un evento del game (che potrebbe chiamarsi CollisionDetected) che si preoccuperà, nel corpo del metodo collegato all'evento, di diminuire il valore di Life.

Anonimo ha detto...

E' giusto quello che dice "Anonimo". Teoricamente una classe (secondo i dettami dell'OOP) fornisce l'interfaccia di utilizzo ma maschera il suo stato interno. Quindi si sa come utilizzare un'istanza di una classe ma non ne possono modificare i valori dei suoi membri a meno che non mi fornisca un metodo per farlo (da cui l'importanza delle properties). Inoltre sarebbe auspicabile che classi diverse siano quanto più disaccoppiate possibili, così sebbene sia possibile implementare il metodo "AggiornaPtVita" nella classe Game1 ed richiamarlo dall'istanza di "SpriteManager", sarebbe bene non farlo (per usare la classe SpriteManager in un altro progetto, dovrei assicurarmi che esista la classe Game1 e che questa abbia un membro chiamato Life).
Saluti.

Posta un commento