giovedì 12 marzo 2009

8) Xna Videogame Tutorial: Generazione Automatica Casuale

Woooow che titolone che ha questo post...
Sono veramente contento di essere arrivato fino a questo punto, pensavo che mi sarei rotto le palle mooooolto prima, invece vedere che si sono tutti i giorni dei lettori (grazie google Analytics) mi mette voglia di scrivere... lasciate qualche commento come segno di vita dai! fa venire voglia di scrivere sapere che c'è qualcuno interessato :D

Comunque, tornando a noi, ci terrei ad inserire subito qui sotto il video di questa lezione perchè a mio parere fa venire voglia di leggerla e concluderla :D

Anteprima Video


L'argomento principale come da mega titolo è la generazione casuale di sprite, se avete seguito le altre lezioni saprete che ad ogni collisione vengono decrementati punti vita, che siamo in grado di pilotare la nostra astronave e che abbiamo uno spritemanager per gestire le grafiche 2d.
Quello che aggiungeremo ora sarà un sistema temporizzato che permetterà la generazione in posizioni casuali di altri sprite. Oltretutto produrremmo due tipologie di sprite in base alla velocità delle astronavi.

Come potete vedere gli sprite che ho utilizzato sono differenti da quelli presenti negli altri tutorial, volevo dare un pò più di gusto a questa lezione e ho deciso di utilizzare delle astronavi di Battlestar Galactica così a qualche mio amico verrà sicuuuramente voglia di seguire questi turorial.... :P( ne approfitto per ringraziare martin studio sito dal quale ho reperito questi ottimi sprite http://martinstudio.kings-field.com/ sono fatti veramente bene!!! ).

Per modificare le immagini aggiunte nelle precedenti lezioni basta andare nel punto dove avete salvato il progetto (solitamente Documenti\Visual Studio 2008\Projects\) arrivare alla cartella content del progetto e sostituire le immagini mantenendo gli stesis nomi (per evitare di dover modificare il codice). Le immagini le trovate nei sorgenti a fondo pagina o direttamente al link segnalato sopra.

A questo punto partirei spedito con il codice senza perdere altro tempo!
Partiamo dal file Game1.cs qui ho aggiunto un'immagine di sfondo e il riferimento all'oggetto rnd di tipo Random sul quale faremo affidamento per generare i nostri valori casuali.
Come al solito copio tutto il codice per darvi una visibilità magigore di quello che ho modificato (segno in grasseto le modifiche)

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
Texture2D tex_universe;//Background universo

//Proprietà per Random
public Random rnd { get; private set; }

public
Game1()
{

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

}

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);
tex_universe= Content.Load<Texture2D>(@"universe");
gui_life = Content.Load<SpriteFont>("Arial");
}


protected
override void UnloadContent()
{
}



protected
override void Update(GameTime gameTime)
{

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.Draw(tex_universe,
new
Rectangle(0,0,Window.ClientBounds.Width,Window.ClientBounds.Height),Color.White);
spriteBatch.DrawString(gui_life, "LIFE: " + life, new Vector2(10, 10), Color.White);
spriteBatch.End();


base.Draw(gameTime);
}


public
int Life {
get { return life; }
set {
life = value;
if
(life == 0) {
Exit();
}
}
}


Non mi dilungo troppo sull'inserimento dell'immagine di sfondo, semplicemente aggiungete un contentuto immagine nei content e lo inserite tramite spritebatch prima di disegnare la stringa della vita (in questo modo verrà stampato prima di quest'ultima che si posizionerà al livello superiore).
Per quanto riguarda l'oggetto rnd, molto semplicemente ho aggiunto una proprietà set e get in testa e ho istanziato l'oggetto nel costruttore, come già anticipato utilizzeremo le funzionalità della classe random per generare numeri casuali.

Ora entriamo nello spritemanager dove si presentano le vere modifiche!
Eccovi il codice:

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 posp;

//tempi di attesa minimi e massimi per lo spawn dei nemici
int minEnemySpawn = 100;//Millisecondi
int maxEnemySpawn = 600;//Millisecondi
int nextSpawn = 0;

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

// TODO: Construct any child components here
}

public
override void Initialize()
{

//Lasciamo solo la posizione iniziale del player
posp = new Vector2(500.0f, 400.0f);

//impostiamo il prossimo spawn
resetTimer();
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);

player = new ControlledSprite(texp, posp, new Vector2(3.0f, 3.0f));
base.LoadContent();
}


public
override void Update(GameTime gameTime)
{

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

//Sotraggo il tempo passato dallo spawn time
nextSpawn -= gameTime.ElapsedGameTime.Milliseconds;
//Se lo spawn time è sceso sotto zero, genero il nuovo nemico
if (nextSpawn < 0) {
spawnEnemy();
}


//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)) {
//Esco dal gioco
--((Game1)Game).Life;
spriteList.RemoveAt(i);
--
i;
}


if
(s.OutOfView(Game.Window.ClientBounds)){
spriteList.RemoveAt(i);
--
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();
}


private
void resetTimer() {
//Imposta il tempo del prossimo spawn
nextSpawn = ((Game1)Game).rnd.Next(minEnemySpawn, maxEnemySpawn);

}


//Genera un nuovo nemico se il tempo di spawn è passato
private void spawnEnemy() {
int
xpos = ((Game1)Game).rnd.Next(0, 600);
int
xspeed = ((Game1)Game).rnd.Next(-3, 3);
int
yspeed = ((Game1)Game).rnd.Next(2, 7);
Texture2D texture;

if
(yspeed > 5)
{

texture = tex2;
}

else
{
texture = tex1;
}


Vector2 speed = new Vector2(xspeed,yspeed);
Vector2 position = new Vector2(xpos,-50);

spriteList.Add(new AutoSprite(texture, position, speed));
resetTimer();
}

}
}

Prima di tutto vi spiego la logica che vogliamo seguire.
Definiamo che esiste un tempo minimo e un tempo massimo entro il quale creiamo una nuova astronave.
Generiamo un numero casuale
nextSpawn
compreso tra questi due parametri, e ad ogni loop di gioco sottraiamo da questo numero il tempo trascorso dal ciclo successivo.
Quendo il numero nextSpawn sarà minore di 0, creiamo una nuova astronave e resettiamo nextSpawn ripescando un numero casuale tra i paramentri sopra... e così via.

In testa, come variabili di classe settiamo i parametri
        //tempi di attesa minimi e massimi per lo spawn dei nemici

int minEnemySpawn = 100;//Millisecondi
int maxEnemySpawn = 600;//Millisecondi
int nextSpawn = 0;

Che rappresentano il tempo minimo e il tempo massimo di spawn dei nemici e inizializiamo nextSpawn.

Nella initialize lanciamo la funzione resetTimer che come vedete si occupa di trovare un numero casuale utilizzando l'oggetto rnd che abbiamo creato in game1.cs. Utilizza la funzione
nextSpawn = ((Game1)Game).rnd.Next(minEnemySpawn, maxEnemySpawn); 
e prende i due parametri min e max come estremi del range entro il quale pescare il numero casuale.
Nella Update grazie all'oggetto gameTime (finalmente ho un esempio di utilizzo per questo valore ) sottraiamo il tempo trascorso dal precedente ciclo a nextSpawn attuale, se nextSpawn è sceso sotto zero, lanciamo la funzione spawnEnemy.
            //Sotraggo il tempo passato dallo spawn time

nextSpawn -= gameTime.ElapsedGameTime.Milliseconds;
//Se lo spawn time è sceso sotto zero, genero il nuovo nemico
if (nextSpawn < 0) {
spawnEnemy();
}

Per quanto riguarda la spawnEnemy genera una posizione e una velocità casuale, utilizzando sempre l'oggetto rnd
a questo punto in base alla velocità generà un oggetto AutoSprite con due differenti texture. Se la velocità è superiore a 5 genera Cylone minacciosissimo :P e più difficile da schivare. In entrambi i casi aggiunge l'astronave alla lista di Sprite della spriteManager.

Un'ultima nota, dato che a questo punto si genereranno centinaia di astronavi, è bene occuparsi anche della memoria e fare in modo che quando le astronavi lasciano l'area di gioco, queste vengano cancellate dalla lista di sprite dato che sarebbe inutile continuare a occuparsi della loro posizione e fare check di collisioni! Sempre nella update trovate questo codice utile allo scopo sopra descritto:
                if(s.OutOfView(Game.Window.ClientBounds)){   

spriteList.RemoveAt(i);
--
i;
}
la funzione OutOfView va inserita a fondo della classe sprite.cs eccola qui...
        //lo sprite è nella finestra?

public bool OutOfView(Rectangle myRect){
if
(position.X < 0 ||
position.X > myRect.Width ||
position.Y > myRect.Height)
{

return
true;
}

else return
false;
}


Signori... anche questa volta ci siamo riusciti... abbiamo finito e il nostro gioco inizia a prendere forma.

Codici Sorgenti Finali:
5.generatore.zip

8 commenti:

Bruno ha detto...

forse c'è un errore nel testo

la funzione OutOfView va inserita a fondo della classe Sprite.cs e non della classe Game1.cs.

Yari ( ImparandoXNA ) ha detto...

Assolutamente vero correggo subito ! grazie per la segnalazione

AdminLuke ha detto...

Pian piano imparo anche io grazie a te Yari...e grazie alla scelta degli sprite di BSG ;)

Anonimo ha detto...

public bool OutOfView(Rectangle myRect){
return (position.X < 0 ||
position.X > myRect.Width ||
position.Y > myRect.Height);
}

Unknown ha detto...

Ciao ma in tutto questo come si integra l'audio????? Sono curiosissimo!! Vorrei farti i complimenti er la tua guida la trovo molto semplice e funzionale!!!!!

fabio.dearcangelis ha detto...

Grazie, sei un mito!

Ovel ha detto...

Grande, articolo molto interessante! Continua così, continuerò sicuramente a leggere!

Aforismi Piloti e Progettisti ha detto...

Grazie 1000 ! Ne serve tanta di gente come te :)

Posta un commento