mercoledì 11 marzo 2009

5) Xna Videogame Tutorial : I GameComponents

Ciao a tutti!
Ecco la 2° parte di questa piccola guida per la creazione del nostro primo semplice videogame.
Questa settimana ci do un pò dentro cercando di scrivere il più possibile, dato che la prossima settimana con molta probabilità sarò fermissimo -.-

Allora, nello scorso post abbiamo visto come disegnare uno sprite e come animarlo nello spazio della finestra di gioco.
Come avrete visto, ci sono alcuni passaggi che risulterebbero essere un pò ripetitivi nel caso in cui volessimo gestire più di una o due astronavi.
Le chiamate delle funzioni spritebatch.draw e i vari aggiornamenti di posizione dell'Update andrebbero ripetuti per ogni astronave...il che sarebbe decisamente poco elegante e senza dubbio scomodo!

Lo scopo di questa lezione è sfruttare questa problematica per presentarvi i game components e fare un piccolo passaggio sulla programmazione ad oggetti... (non mi addentrerò nei meandri oscuri dell'OOP ma vi fornirò un paio di nozioni mano a mano che scrivo) .
A chi volesse dare una ripassata veloce all'argomento OOP, consiglio di fare un saltino qui .

Vi consiglio di creare un progetto nuovo, non utilizzate il precedente come punto di partenza.

Video Anteprima:


Prima di tutto cerchiamo di trattare lo sprite come un'entità separata dal corpo del programma, rendiamolo in sostanza un OGGETTO, questo permetterà il suo semplice riutilizzo! e pulirà il nostro codice aggiungendo un pò di logica e ordine.
Una volta che avremo definito al meglio l'oggetto sprite, ci occuperemo di trovare un elemento capace di gestire una lista di oggetti sprite e di inserirli nel game Loop... e indovinate un pò...per risolvere quest'ultimo problema utilizzeremo un Game Component.

Quindi gli step che seguiremo sono i seguenti:
1) Definizione dell'oggetto sprite e sue derivazioni
2) Definizione di un gamecomponent per la gestione degli sprites (sprite manager)

Oggetto sprite:
Tramite l'oggetto sprite dovremo gestire tutte le funzioni e le variabili che erano associate allo sprite inserito "manualmente" nel codice precedente.
Quindi ci servirà avere informazioni come Posizione, Texture, Velocità e avere dei metodi Update e Draw dedicati.
Oltretutto, dato che siamo troppo avanti ( o meglio... Aaron Reed è troppo avanti :P ) , non ci limiteremo a creare una Classe Sprite, ma la creeremo come classe astratta e andremo a definire delle classi derivati dalla Sprite.

Ripasso OOP Classi astratte:-------------------------------------------------
Una Classe Astratta, è una classe tramite la quale "non è possibile" istanziare nessun oggetto, viene utilizzata come base per lo studio di classi specializzate che andranno ad estendere ed ereditare le informazioni presenti nella classe padre (per l'appunto la classe astratta).

Esempio:
La classe Figura, ha un metodo CalcolaArea, un metodo SettaAltezza, un metodo SettaLarghezza, un parametro larghezza e un parametro altezza.
In questo esempio non avrebbe senso implementare il metodo CalcolaArea, perchè non sapremmo con quale formula calcolarla, quindi ci servirà solo come base, ricordandoci che nelle specializzazioni di questa classe dovremmo implementare una funzione Calcola
Area che faccia l'override del metodo padre (è un concetto molto simile al concetto di implementazione di interfaccia) definiremo questa funzione come "virtual", il che ci consente di reimplementare un metodo CalcolaArea in tutte le classi derivate dalla classe Figura. (merdacc ci vorrebbe un'oretta fatta bene per spiegare questi concetti... spero che vi servano almeno come ripasso... spiegarli da zero è veramente un lavoraccio).
A differenza del metodo CalcolaArea, potremmo però implemetare i metodi SettaAltezza e SettaLarghezza che si occuperanno di settare i valore altezza e larghezza verficando che siano maggiori di zero.
Questi metodi potrebbero a questo punto essere utilizzati dalle classi derivate senza doverne implementare una versione propria.
-----------------------------------------------------------------------------------------



Implementeremo questa gerarchia:


Sprite (Sprite.cs):
La classe astratta dalla quale erediteremo funzioni e che useremo come interfaccia.

Automa (AutoSprite.cs):
Una classe che permetterà agli sprite di muoversi autonomamente

Guidato (ControlledSprite.cs):
La classe che produrrà l'oggetto sprite pilotato dall'utente tramite tastiera (Nella prossima lezione)


Qui sotto riporto il codice della classe SPRITE:
(per aggiungere una classe al progetto, andate nella barra di destra di visual c# cliccate con il destro in un'area libera e Add->New Item ----> Visual c# Item -> e aggiungete un'elemento Class compilando il form

SPRITE--------------------------------
using System;

using
System.Collections.Generic;
using
System.Linq;
using
System.Text;
using
Microsoft.Xna.Framework;
using
Microsoft.Xna.Framework.Graphics;

namespace
sprite_manager2
{

abstract class Sprite
{

protected
Texture2D texture;
protected
Vector2 position;
protected
Vector2 speed;

public Sprite(Texture2D new_texture, Vector2 new_position,Vector2 new_speed)
{

this
.texture = new_texture;
this
.position = new_position;
this
.speed = new_speed;
}


public virtual
void Update(GameTime gameTime) {

}


public virtual
void Draw(GameTime gameTime, SpriteBatch spriteBatch){
spriteBatch.Draw(texture, position, Color.White);
}


public
Rectangle collisionRect {
get {
return
new Rectangle((int)position.X, (int)position.Y, texture.Width, texture.Height);
}
}
}
}

Un piccolo appunto, creando una classe base di visual c# mancheranno i vari Using dedicati a XNA... dovrete aggiungerli voi, e un'altra cosa... il namespace che vedete dipenderà da come avrete chiamato il progetto quindi non fate troppo caso ai miei...

Analiziamo velocemente il codice:
In testa abbiamo le variabili di classe che descrivono i parametri di nostro interesse, quindi texture, posizione e velocità.

Per quanto riguarda il costruttore, imposta semplicemente le variabili di classe con quanto passato in fase di creazione dell'oggetto.

L'update è una virtual (che quindi potrà essere implementata nelle classi che deriveranno dalla Sprite) senza contenuto.

Nella Draw, anch'essa virtual, chiameremo la draw dell'oggetto spriteBatch che ci viene passato come parametro inviando i dati texture e posizione.

In aggiunta notate la variabile collisionRect implementata tramite la proprietà get (andate qui per approffondire il d iscorso proprietà get / set, è fottutamente semplice e comodo) che restituira il rettangolo occupato dall'oggetto Sprite (come potete immaginare sarà utilissimo per calcolare le collisioni).

Ok dunque abbiamo creato la nostra classe base, ora occupiamoci della prima derivazione, una classe dalla quale istanziare oggetti in grado di muoversi in maniera autonoma.

AUTOSPRITE-----------------------
using System;

using
System.Collections.Generic;
using
System.Linq;
using
System.Text;
using
Microsoft.Xna.Framework;
using
Microsoft.Xna.F
ramework.Graphics;

namespace
sprite_manager2
{

class
AutoSprite : Sprite
{


public AutoSprite(Texture2D new_texture, Vector2 new_position, Vector2 new_speed)
:
base(new_texture,new_position,new_speed)
{ }


public
override void Update(GameTime gameTime)
{

position += speed;
base.Update(gameTime);
}

}
}

Per creare questa classe seguite lo stesso passaggio seguito per la Sprite.
La prima cosa alla quale dovete far caso è la definizione della classe, e la sua derivazione dalla classe Sprite tramite l'operatore ":" .
Il costruttore si occupa semplicemente di chiamare il costruttore padre tramite l'operatore base e passando tutti i paramentri ricevuti in fase di creazione dell'oggetto.
La funzione Update come noterete utilizza la parola chiave override, qualche riga sopra nell'update della sprite se vi ricordate abbiamo usato la parola virtual per definire che questo metodo potrebbe essere sovrascritto nelle classi figlie. Ed è proprio quello che abbiamo fatto nella AutoSprite definendo che nell'update modificheremo le informazioni di posizione e poi richiameremo l'update della classe padre (questa cosa non penso sia obbligatoria a dire il vero..ma vedo ceh è una pratica ripetuta in molti tutorial.. quindi la seguo da bravo caprone, ho provato a non inserire la chiamata all'update padre.. e funziona tutto correttamente).

Dove siamo ora??
A questo punto abbiamo creato una classe in grado di generare oggetti sprite in maniera abbastanza rapid a, saremmo già in grado di aggiungere degli sprite in game1.cs, dovremmo istanziare un'oggetto di tipo AutoSprite e nella classe update (di game1) e draw (sempre di game1 ) chiamare i metodi update e draw del nostro oggetto AutoSprite.
Sincera mente salterò questo passaggio e vi porterò direttamente alla creazione di sprite tramite uno sprite manager!

Lo scopo dello sprite Manager:
Principalmente creeremo uno sprite manager per incapsulare la logica di creazione degli sprite in un sistema dedicato unicamente a questo scopo. Questo ci permetterà di riutilizzare lo spriteManager anche in altre occasioni e in altre nostre creazioni.
Per aggiungere lo spritemanager dovremmo creare una classe derivata da un GameComponent .. anzi, a dire il vero da un DrawableGameComponent dato che dovremo sovrascrivere la funzione Draw.
Quello che dovrete fare è aggiungere un nuovo file .. solito passaggio Add->new Item e questa volta scegliere XNA Game Studio come categoria e GameComponent come scelta finale. Chiamatelo SpriteManager. A questo punto avrete del codice in parte pronto, come prima modifica ricordatevi di sostituire
public class SpriteManager : Microsoft.Xna.Framework.GameComponent


Con

public class SpriteManager : Microsoft.Xna.Framework.DrawableGameComponent


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
sprite_manager2
{

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

SpriteBatch spriteBatch;
List<Sprite> spriteList = new List<Sprite>();
Texture2D tex1, tex2;
Vector2 pos1, pos2;
Vector2 spd1, spd2;

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


public
override void Initialize()
{

pos1 = new Vector2(10.0f, 1.0f);
pos2 = new Vector2(30.0f, 2.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");
spriteBatch = new SpriteBatch(Game.GraphicsDevice);
//Aggiungiamo nuovi sprite----------------------
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)
{

foreach (Sprite s in spriteList) {
s.Update(gameTime);
}

base.Update(gameTime);
}


public
override void Draw(GameTime gameTime)
{

spriteBatch.Begin();
foreach (Sprite s in spriteList)
{

s.Draw(gameTime,spriteBatch);
}

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

}
}

Vediamo cosa abbiamo creato...
    SpriteBatch spriteBatch;

List<Sprite> spriteList = new List<Sprite>();
Texture2D tex1, tex2;
Vector2 pos1, pos2;
Vector2 spd1, spd2;

Fondamentalmente creiamo uno spritebatch dedicato unicamente al questo componente (utile per utilizzare il componente in altri programmi indipendentemente dalla loro struttura).
Una lista dove inseriremo i vari oggetti di tipo sprite (o suoi derivati) e poi abbiamo due differenti texture, due informazioni di posizione e due velocità. Valorizzeremo questi elementi in seguito in diversi punti del codice, nella initialize ci occupiamo di dare un valore a posizioni e velocità.
Nella load content caricheremo le texture dei nemici (incollo qui sotto le immagini da scaricare) e creiamo lo spritebatch dedicato (aggiungete le texture dei nemici ai contenuti... vi ricordo Add->exixting Item e scegliete le immagini).

Infine aggiungiamo alla lista di sprite 3 nuovi AutoSprite, ai primi due daremo le info settate nella initialize e il terzo lo valorizzermo direttamente dal costruttore. Nella Update e nella Draw ci occuperemo semplicemente di ciclare tutti gli oggetti della lista sprite e di chiamare i metodi Draw e Update degli oggetti (che sono appunto i metodi che abbiamo definito nelle classi Sprite e AutoSprite).

Perfetto.... e ora?? come faremo rientrare le funzioni dello SpriteManager nel ciclo del programma?
Qui arriva la parte goduriosa... bastano solo 2 righe di codice!
Andate nella Initialize di Game1.cs e aggiungete in testa alla funzione queste due righe

SpriteManager spritemanager = new SpriteManager(this);

Components.Add(spritemanager);

Questo passaggio fa si che venga creato un nuovo SpriteManager e venga aggiunto ai gamecomponents, da questo momento automaticamente entrerà nel ciclo di vita del programma, per intenderci ad ogni update di game1.cs ci sarà un update del nostro SpriteManager.
Come potete vedere, nel file game1.cs non c'è traccia dei tre sprite che abbiamo incapsulato nello spriteManager e il tutto a questo punto è assolutamente indipendete e ordinato!

Se non ho perso nessun pessaggio a questo punto dovreste poter compilare tranquillamente il codice e godervi le nostre 3 astronavi nemiche che svolazzeranno verso il basso, se ci fosse qualche errore dovuto ai copia e incolla dei codici... qui sotto trovate i sorgenti!

Qui sotto i codici sorgenti:
2.spriteManager.zip

3 commenti:

Black7th ha detto...

qua il discorso si fa + complesso, credo che prima di assimilare questa lezione dovrò fare delle prove diverse dal tutorial, cmq great!

Imparando XNA ha detto...

Ti consiglio di fare il tutto con i codici davanti... è molto più semplice :D
Se hai bisogno di spiegazioni in punti dove sono stato troppo rapido chiedi senza problemi!

Andrea ha detto...

Complimenti !
E' proprio quello che mi serviva per far capire ai miei studenti che tutta la teoria OOP che ci siamo sciroppati quest'anno serve veramente !!
;-)

Ottimo lavoro, molto chiaro e ben strutturato.
Grazie.

Posta un commento