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 :
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 giocatoreIl secondo Step lo vediamo nella initialize dove settiamo la vita iniziale a 100, mentre nella Update carichiamo il font in gui_life
SpriteFont gui_life;//GUI
//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)));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)
player = new ControlledSprite(texp, posp, spd1);
for (int i = 0; i < spriteList.Count; 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)
Sprite s = spriteList[i];......
//Controllo se uno degli sprite nemici collide contro il giocatoreAvevamo 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)
if (s.collisionRect.Intersects(player.collisionRect)) {
//Tolgo una vita al player
--((Game1)Game).Life;
spriteList.RemoveAt(i);
}
--((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:
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...
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.
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.
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