lunedì 23 marzo 2009

9) Xna Videogame Tutorial: Gestione delle scene e menu

Bentornati!!! Quello di questa sera sarà l'ultimo articolo attraverso il quale chiuderemo lo sviluppo del nostro primo videogame in 2d.
Sarà anche la lezione più complessa e corposa, proprio per questo motivo non potrò copiare l'intero codice come ho fatto nelle precedenti lezioni, ma serete comunque in grado di seguirmi, dato che prenderò in esame i punti più complessi e cercherò di presentarvi a priori la situazione che andremo a ricreare.
Vi faccio subito notare che tutto quello che avete prodotto fino ad ora non viene buttato via... semplicemente preferisco partire dal progetto già completo per aiutarmi nella spiegazione e darvi gli ultimi concetti per mettere insieme un gioco semplice ma almeno abbastanza completo! Molto di quello che avete scritto nelle lezioni precedenti lo ritroverete qui tale e quale e tutti i concetti appresi sono assolutamente necessari per proseguire e comprendere la lezione qui sotto :D

Anteprima Video (senza audio):


Quello che cercherò di spiegarvi questa sera è come utilizzare i GameComponents per ottenere dei piccoli micromoduli "autonomi" e riutilizzabili. Attraverso questi elementi creeremo una logica di navigazione delle scene, che vi permetterà di avere una schermata di menu, la scena dove si svolge il gameplay e una schermata di Vittoria- fine partita (in più lascerò una scena libera da utilizzare come meglio credete...).
Nella creazione di queste scene gestiremo anche l'audio! Non vi spiegherò l'utilizzo di XACT in questa lezione (prometto che lo farò più avanti) ma vi darò già i file prodotti da questo ottimo tool per poter essere direttamente utilizzati nel nostro videogame.
Ok...andiamo per gradi, prima di tutto scaricate il file zip contenente i codici... aprite il progetto e seguitemi :D

CODICE SORGENTE:

6.2dgame.zip

Come vedrete ho diviso i contenuti in cartelle, ecco qui uno screen della situazione:


Rispetto al solito vedete molti più file, molti dei quali sono i famosi "micromoduli" dei quali vi parlavo sopra.
Non vi preoccupate perchè li analizzeremo tutti e vedrete che non saranno assolutamente complicati da comprendere.
Un piccolo appunto su quest'ultima lezione:
Tutte le cose che vi spiegherò riguardanti la gestione delle scene e i menu le ho imparate dal libro "Beginning XNA 2.0 Game Programming: From Novice to Professional" , Ho seguito solo la prima parte dato che quando è iniziata la parte 3d ho preferito passare a learning xna 3.0 visto che l'ho trovato molto più semplice da seguire.

Facciamo una carrellata veloce sulle cartelle che compongono il progetto:
Nella cartella Core sono presenti i file centrali, dei quali ci serviremo per creare oggetti specifici. Tra questi conoscete già lo SpriteManager, mentre per quanto riguarda gli altri file li analizzeremo uno ad uno in seguito.
Nella cartella Sprite ritrovate semplicemente i tipi di sprite che avete visto nelle lezioni precedenti, quindi la classe astratta Sprite e le classi per gestire sprite controllabili dall'utente e sprite automatizzati.

Nella cartella Scene infine, trovate tutte le scene che comporranno la nostra navigazione durante il ciclo di vita del programma.

E' proprio da qui che partirei introducendovi la logica di game scene che ho utilizzato:

Le Scene di Gioco
Una scena di gioco non è altro che un'insieme di componenti e di informazioni utili in un determinato punto del programma. Per comprendere meglio quello che intendo dire, prendiamo come esempio un menu e il gioco che avete creato fino alla scorsa lezione. Parliamo dunque di due momenti del programma sconnessi tra di loro, la scena iniziale si occuperà di presentarvi il menu di gioco attraverso il quale potrete decidere di iniziare la partita o di visualizzare una schermata di help. La scena del gameplay invece, rappresenta il gioco vero e proprio, quindi esclude tutte le parti legate al menu e mostra unicamente gli elementi che compongono il gameplay.

Per quanto riguarda il nostro video game, produrremo le scene presentate in questa immagine:

























StartScene:
Dove viene presentato il menu

PlayScene:
Il gioco

HelpScene:
Dove poter mostrare un'immagine di help

WinScene:
Schermata presentata dopo che dalla PlayScene si raggiunge un determinato punteggio

Avrei potuto aggiungere anche una endGameScene ma a livello dimostrativo sarebbe stato identico che creare la WinScene ... quindi non sarebbe stato molto utile per voi.

Ora che sappiamo cosa andremo a creare analizzerei la classe GameScene presente nella cartella Core, dato che utilizzeremo proprio questa classe come padre delle scene sopra descritte. Come vi dicevo non copierò tutti i codici, ma solo le parti fondamentali.... quindi aprite il file GameScene.cs e seguitemi da li.

GAMESCENE-------------------------------------------------------------------------------

Ogni scena avrà lo scopo di contenere uno o più componenti che a loro volta avranno determinati compiti.
La prima cosa che serve alla classe gamescene è dunque un contenitore lista per i vari componenti.
            components = new List<GameComponent>();

Un secondo elemento da analizzare è la presenza delle voci Visible e Enabled , questi sono due booleani utilizzati per far si che un componente venga mostrato e attivato, in poche parole se un componente non è visibile (Visibile=false) non viene preso in considerazione nella funzione Draw mentre se non è attivo (enabled=false) non viene considerato nell'Update.
            //Di default una scena non è visibile e non è attiva

Visible = false;
Enabled = false;
Per quanto riguarda le due funzioni del gameloop vediamo il corpo centrale della update
            // Update the child GameComponents

for (int i = 0; i < components.Count; i++)
{

if
(components[i].Enabled)
{

components[i].Update(gameTime);
}
}
Dove in poche parole scorriamo tutti i componenti della classe e se sono abilitati li aggiorniamo
e la draw:

            // Draw the child GameComponents (if drawable)

for (int i = 0; i < components.Count; i++)
{

GameComponent gc = components[i];
if
((gc is DrawableGameComponent) &&
((
DrawableGameComponent)gc).Visible)
{
((
DrawableGameComponent)gc).Draw(gameTime);
}
}
Dove renderizziamo i componenti della classe ( se sono di tipo drawable components naturalmente)
Questi sono i punti centrali della classe astratta GameScene.
A questo punto è necessario capire quali sono i componenti che dovranno formare le nostre scene,
Analizziamoli uno ad uno prima di entrare nelle altre scene....

Partiamo dal più semplice:

IMAGE COMPONENT:...............................................................
Componente utilizzato per la gestione delle immagini, è un componente molto semplice l'unica cosa da notare è il fatto che può stretchare un'immagine oppure posizionarla al centro dello schermo in base al parametro drawmode, vi basterà guardare il metodo draw di questo componente per capire come differenzia la scelta tra stretch e centered.

Una cosa che mi preme farvi notare è invece l'utilizzo dei Servizi! guardate come viene valorizzato lo spriteBatch
spriteBatch = (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));


Questo tipo di definizione dovrebbe esservi nuova, i servizi sono il sistema attraverso il quale potete aggiungere dei componenti al una lista di elementi facilmente riselezionabili.
In questo modo condividerete un unico servizio spritebatch in tutte le vostre classi/componenti senza doverne ricreare uno nuovo ogni volta. (Vedremo dopo nel file Game.cs come aggiungere un componente ai servizi).

AUDIO COMPONENT...............................................................
Componente per la gestione dell'audio. Nonostante non conosciate ancora la logica di gestione dell'audio di XNA (e ammetto che è una delle parti che mi annoia di più), potrete facilmente immaginare come utilizzare questo componente.
Prima di tutto creiamo gli oggetti utili al motore audio di XNA.
       private AudioEngine audioEngine;

private
WaveBank waveBank;
private
SoundBank soundBank;
Nella funzione initialize vedrete come vengono istanziati i valori presenti qui sopra.
      audioEngine = new AudioEngine("Content\\gameAudio.xgs");

waveBank = new WaveBank(audioEngine, "Content\\Wave Bank.xwb");
if
(waveBank != null)
{

soundBank = new SoundBank(audioEngine, "Content\\Sound Bank.xsb");
}
Se Aprite il file gameAudio.xap presente nella folder dei contenuti potrete vedere il file che viene generato dal tool XACT, si tratta di un file scritto in un linguaggio simile al C, dove vengono descritti AudioEngine con dei parametri che sinceramente ignoro, WaveBank dove viene preso il riferimento ai file wav ai quali ci appoggeremo, e soundBank dove vengono identificati gli elementi cue che ci serviranno per riprodurre direttamente i file audio caricati. ( per ora accontentatevi di queste poche righe di spiegazione.. e concentratevi più che altro sulla logica delle scene.. sull'audio ritorneremo).
I riferimenti cue audio presenti in gameAudio.xap sono i seguenti :
rpg - suono di un'esplosione
sigla - la mitica sigla iniziale di Battlestar Galactica
electro - suono digitale utilizzato per il menu

TEXTCOMPONENT....................................................
Questo componente verrà utilizzato per stampare semplici stringhe di testo, in testa alla classe trovate alcuni parametri utili a definire le informazioni di queste stringhe, come colore, testo da stampare, posizione e naturalmente lo spritefont da utilizzare. Il componente ci tornerà utile per stampare qualche elemento della semplice gui grafica, che presenterà la vita e il punteggio.
        //Testo da stampare

protected String text = "";
// Fonts
protected readonly SpriteFont font;
// Colors
protected Color color = Color.White;
// Menu Position
protected Vector2 position = new Vector2();

SPRITEMANAGER...................................................
Questo direi che lo conosciamo abbastanza bene :P , è l'elemento che gestisce gli sprite presenti nel gioco e alcune logiche come le collisioni tra sprite.
Tra le modifiche apportate a questa classe noterete che è stato aggiunto un'oggetto AudioComponent che servirà per riprodurre il suono delle esplosioni a seguito di una collisione, ecco qui sotto svelato come riprodurre suoni tramite l'AudioComponent:

        audioComponent.PlayCue("rpg");

La funzione PlayCue del componente audio accetta una stringa che rappresenta il nome del Cue da riprodurre (in questo caso l'esplosione).

TEXTMENUCOMPONENT..........................................
Q
uesto componente viene utilizzato per creare un menu testuale, utilizza una string collection (menuItems) per rappresentare le varie voci.

E' interessante notare come in questo componente si utilizzi un riferimento alla content pipeline per caricare contenuti direttamente dal componente, in questo modo può essere facilmente riutilizzato in altri programmi senza dover aggiungere ulteriori informazioni nella logica già esistente. Se non avessimo utilizzato questo metodo, avremmo dovuto caricare i contenuti a livello di Game1.cs nella load content, impostandoli inoltre come variabili di classe per poterli raggiungere dai vari componenti.
            ContentManager myCont = game.Content;


//Setta i font utilizzati
regularFont = myCont.Load<SpriteFont>("Arial");
selectedFont = myCont.Load<SpriteFont>("ArialBig");


La logica attraverso la quale viene resa "selezionata" una voce di menu è l'incrementale selectedIndex che a seconda della pressione dei tasti freccia su o freccia giù aumenta o diminuisce di uno, e in entrambi i casi genera un suono (funzione update) . Notate che quando si arriva a fondo menu premendo la freccia "giù" si torna alla prima voce e se ci si trova in testa e si preme la freccia "su" si passa all'ultima voce.

            bool down, up;


// Check input tastiera

down = (oldKeyboardState.IsKeyDown(Keys.Down) &&
(
keyboardState.IsKeyUp(Keys.Down)));

up = (oldKeyboardState.IsKeyDown(Keys.Up) &&
(
keyboardState.IsKeyUp(Keys.Up)));

if
(down || up)
{

audioComponent.PlayCue("electro");
}


if
(down)
{

selectedIndex++;
if
(selectedIndex == menuItems.Count)
{

selectedIndex = 0;
}
}

if
(up)
{

selectedIndex--;
if
(selectedIndex == -1)
{

selectedIndex = menuItems.Count - 1;
}
}

Per quanto riguarda la funzione draw viene scorsa la lista menuItems, quando ci si trova a stampare la voce attualmente selezionata si stampa con i parametri "selected", altrimenti con i parametri "regular".
Da notare il trucchetto utilizzato per stampare delle ombre del testo, stampando due volte la stessa voce e facendo si che la stampa a livello inferiore sia leggermente spostata rispetto a quella superiore e quindi non interamente coperta, producendo un effetto ombra.

            //Scorre tutti gli elementi

for (int i = 0; i < menuItems.Count; i++)
{

SpriteFont font;
Color theColor;

//Quando trova l'elemento selezionato associa il font e il color per l'elemento selezionato
if (i == SelectedIndex)
{

font = selectedFont;
theColor = selectedColor;
}

else

{

font = regularFont;
theColor = regularColor;
}


// Disegna l'ombra dell'elemento menu
spriteBatch.DrawString(font, menuItems[i],
new
Vector2(position.X + 1, y + 1), Color.Red);

// Disegna l'elemento del menu
spriteBatch.DrawString(font, menuItems[i],
new
Vector2(position.X, y), theColor);

y += font.LineSpacing;
}



Ora che conosciamo i vari componenti torniamo alla descrizione delle scene!

STARTSCENE----------------------------------------------------------------------------
Descrizione: E' la scena iniziale dove, tramite un menu, vengono presentate le azioni disponibili. Questa scena ha un sottofondo musicale e un'immagine di sfondo.

Componenti utilizzati:

ImageComponent, TextMenuComponent, AudioComponent

Analizziamo il costruttore

        public StartScene(Game game)

:
base(game)
{

ContentManager startCont = game.Content;

//Add sfondo tramite componente di tipo Image
Texture2D background = startCont.Load<Texture2D>("battlestar");
Components.Add(new ImageComponent(game, background,ImageComponent.DrawMode.Center));

// Crea le voci di menu e aggiungo il menu con un componente di tipo TextMenu
string[] items = { "Inizia la partita", "Help", "Esci dal gioco" };
menu = new TextMenuComponent(game);
menu.SetMenuItems(items);
Components.Add(menu);

//Recupero lo spritebatch dai servizi
spriteBatch = (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));

//Recupero audiocomponente dai servizi
audioComponent = (AudioComponent)Game.Services.GetService(typeof(AudioComponent));
}

Concentriamoci sul metodo attraverso aggiungiamo componenti alla scena, come potete notare utilizziamo
la funzione add della lista components che viene ereditata direttamente dalla classe GameScene.
(Chiamamiamo Components.Add e non components.add perchè abbiamo definito la proprietà get sempre nella classe GameScene).
A questo punto i componenti aggiunti alla lista components verranno ciclati nei metodi Update e Draw, che nel caso della scena startScene fanno riferimento ai metodi della classe padre, senza alcuna modifica.

Le funzioni show e hide si occupano semplicemente di modificare i parametri visible e enabled del menu e di far partire o bloccare l'audio.


PLAYSCENE----------------------------------------------------------------------------------
Descrizione:
scena dove viene gestito il gioco vero e proprio
Componenti:
TextComponent, ImageComponent, SpriteManager, AudioComponent

Analizziamo il costruttore
        public PlayScene(Game game)

:
base(game)
{

ContentManager startCont = game.Content;
score_gui = new TextComponent(game);
life_gui = new TextComponent(game);

//Aggiunge lo sfondo
Texture2D background = startCont.Load<Texture2D>("universe");
Components.Add(new ImageComponent(game, background,ImageComponent.DrawMode.Center));

//Aggiunge il gestore degli sprite
Components.Add(new SpriteManager(game));

//Aggiunge i testi per la vita e il punteggio
Components.Add(life_gui);
Components.Add(score_gui);

//Modifica qualche parametro dell'interfaccia grafica
score_gui.Position = new Vector2(200, 0);
score_gui.Color = Color.Red;

//Recupera lo spriteBatch dai servizi
spriteBatch = (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));
}


Come per la startScene qui aggiungiamo i vari componenti che farrano parte della scena, impostiamo due istanze del componente che si occupa delle stringhe di testo, che utilizzeremo per stampare punti vita e punteggio totale, aggiungiamo un background e lo sprite manager.... niente di difficile.
Nell'update invece ci occupiamo di tenere tracica di eventuali modifiche nel punteggio o nella vita cambiando il parametro Text dei due oggetti TextComponent.
            life_gui.Text = "Life: " + ((Game1)Game).Life;

score_gui.Text = "Score: " + ((Game1)Game).Score; ;


WINSCENE-------------------------------------------------------------------------------
Descrizione:
Scena con un messaggio di vittoria
Componenti:TextComponent, ImageComponent

Nel file Game1.cs, come vedremo più avanti, viene gestita la logica del punteggio.Quando questo raggiunge un determinato valore si passa ad una schermata di vittoria costrutira tramite la winScene.
Molto semplicemente in questa scena aggiungiamo un'immagine di sfondo e un testo...

        public WinScene(Game game)

:
base(game)
{

ContentManager startCont = game.Content;
Texture2D background = startCont.Load<Texture2D>("battlestar");
winText = new TextComponent(game);
winText.Text = "Congratulazioni! Hai Vinto!";
winText.Position = new Vector2(Game.Window.ClientBounds.Width / 2-50, Game.Window.ClientBounds.Height / 2 +30);
//Add sfondo
Components.Add(new ImageComponent(game, background,
ImageComponent.DrawMode.Center));
Components.Add(winText);

// Get the current spritebatch
spriteBatch = (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));
}

HELPSCENE---------------------------------------------------------------------------
Descrizione: Scena volutamente vuota... creata per i vostri esperimenti!
Componenti: per ora solo ImageComponent

Questa scena è assolutamente inutile.... ho aggiunto solo l'immagine di sfondo presente già nella PlayScene... potrete dunque sfruttarla per i vostri test.




Per quanto riguarda le scene è tutto, a questo punto passiamo al file Game1.cs dove verranno effettivamente caricate e gestite le scene viste sopra.
In testa al file istanziamo alcuni oggetti/variabili
        //Init----------------------------------------

GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
AudioComponent audioComponent;

//Variabili globali---------------------------
int life = 30;
int
score = 0;
int
winScore = 10;
protected
KeyboardState oldKeyboardState;
protected
GamePadState oldGamePadState;

//Scene--------------------------------------
StartScene startscene;
PlayScene playscene;
HelpScene helpscene;
WinScene winscene;
GameScene activescene;
Nelle ultime righe qui sopra vedete che istanziamo gli oggetti che rappresentano le varie scene che ci serviranno e che creiamo una scena in più chiamata activescene di tipo GameScene e quindi potrà essere associata a qualsiasi scena.
Nella funzione initialize inizializiamo il componente audio, e vediamo finalmente come si aggiunge un componente ai servizi usando la funzione AddService:
        protected override void Initialize()

{

audioComponent = new AudioComponent(this);
Services.AddService(typeof(AudioComponent), audioComponent);

Components.Add(audioComponent);
base.Initialize();
}
Una piccola nota sul Components.Add(): In questo caso non state usando la stessa Components.Add che utilizzavate per le scene.... in questo caso stiamo usando la collection di base di XNA che serve appunto per registrare i nostri componenti nel gameloop.
Per quanto riguarda la funzione LoadContent, aggiungiamo lo spritebatch ai servizi, istanziamo le scene , registriamo i componenti e rendiamo attiva la scena StartScene (quella con il menu) associandola alla scena activeScene.

Date una sbirciatina alla funzione update... vedrete che richiama la funzione HandleScenesInput();
Lo scopo di questa funzione è quello di gestire l'input in maniera differente a seconda della scena nella quale ci si trova. (non gestisce gli spostamenti dell'astronave però... quelli li gestiamo nello spritemanager)
        //Gestisce gli input delle scene

private void HandleScenesInput()
{

// Handle Start Scene Input
if (activescene == startscene)
{

HandleStartSceneInput();
}

// Handle Help Scene Input
else if (activescene == helpscene)
{

if
(CheckEnterA())
{

ShowScene(startscene);
}
}

// Handle Action Scene Input
else if (activescene == playscene)
{

HandleActionInput();
}
}

In base alla scena nella quale ci troviamo viene richiamata una funzione piuttosto che un'altra...
Le funzioni da prendere in considerazione sono le seguenti:
handleStartSceneInput(), che tramite la funzione checkEnterA() controlla se viene premuto il tasto invio o il tasto A del pad... e in questo caso a seconda della voce di menu momentaneamente attiva manda a una scena definita. La gestione della pressione dei tasti freccia su e freccia giù è impostata all'interno del componente TextMenuComponent.
HandleActionInput(); che durante la fase di gioco controlla se viene premuto il tasto "esc" e in tal caso riporta alla start scene.

Per attivare una scena viene usata la funzione helper ShowScene(scenadaattivare); che si occupa di disattivare la scena attiva e rendere attiva quella passata alal funzione.

Le ultime funzioni da analizzare sono le propiretà get e set Life e Score, entrambe vengono richiamate dal componente SpriteManager durante il ciclo di Update nel caso in cui ci siano collisioni (decrementando Life e Score) e nel caso in cui un'astronave nemica esca dallo spazio di gioco (incrementando Score).
La Score definisce che se il punteggio superà winScore (definito in testa a Game1.cs) allora bisogna attivare la scena winScene.

A questo punto penso che abbiate in testa un marasma di informazioni.... Faccio un breve riassunto che spero vi possa aiutare per focalizzare le informazioni:

Lo scopo della lezione è creare una navigazione tra diverse scene del programma,
fondamentalmente 3 scene - Un menu - Un gamePlay - Una schermata finale.
Ogni scena è composta da diversi componenti ( menu, immagini, sprite, testi, suoni) che vengono aggiornati e disegnati unicamente quando la scena è attiva. L'attivazione di una scena è definita nel file game1.cs grazie a un gestore dell'input (che per alcune scene come PlayScene StartScene viene anche controllato direttamente dal componente textmenu e spritemanager) e grazie a una logica di gioco che fa si che ad un determinato punteggio venga attivata la scena di fine partita.

GAME ---------->LOOP
|---------- SCENA ATTIVA---------->LOOP COMPONENTI SCENA
|-----------Singolo Componente


Con questo post vi saluterò per un pò, dato che preparerò un tutorial come questo per la creazione di un videogame 3d.... spero di postare i primi articoli sul 3d tra un mesetto circa, comunque passerò spesso dal blog, se avete bisogno chiarimenti ho se volete segnalarmi delle correzioni io sono qui :D

Ciao a tutti !!
A prestissimo!!!

CODICE SORGENTE:
6.2dgame.zip


9 commenti:

Anonimo ha detto...

Ciao,molto ben scritto sia il tutorial che il codice!

Anonimo ha detto...

ho trovato difficolta in quest'ultimo capitolo, di certo tutte le altre lezioni da te spiegate sono state molto più semplici ed intuitive, le classi erano spiegate una per volta mentre qui sei andato troppo veloce, sei passato da una lezione all'altra aggiungendo troppe cose senza spiegarle passo a passo ( mi riferisco alla "imagecomponent e a qualche altra clsse).
Non per questo è una cattiva guida, il mio è un messaggio costruttivo e di complimenti per lo splendido lavoro che hai fatto, continua così anche per il 3d :)

Yari ( ImparandoXNA ) ha detto...

ciao! effetivamente qui ci sono parecchie informazioni, purtroppo per mostrarvi la gestione delle scene ho dovuto abbandonare in parte il filo delle lezioni precedenti, perchè non avrei potuto incastrare quello che volevo farvi vedere. Non spiego tutto il codice come negli altri post perchè effettivamente c'è troppa roba :S

Ti ringrazio per i vari commenti e per le osservazioni :D

Anonimo ha detto...

Complimenti, spero che affronterai altri argomenti sul 2D, mi sto affacciando anch'io alla programmazione e mi interesserebbe molto capire come gestire alla perfezione gli Sprite 2D animati.

;)

Bruno ha detto...

troppa roba in questa ultima parte, peccato perchè altrimenti la guida sarebbe stata praticamente perfetta se fosse stata divisa in almeno due parti. D'altronde non si può avere tutto e se una cosa piace bisogna anche sapersi arrangiare.
Complimenti per questo tutorial.

Anonimo ha detto...

Complimenti! Ottimo lavoro

Davide ha detto...

potresti chiarirmi perchè si è scritto

public List Components
{
get {return components;}
}

Francesco ha detto...

@Davide: Ti consiglio di guardarti la guida su C#, trattasi delle cosi dette "properties". Sinceramente credo che quelli di C# potevano risparmiarsi di introdurre questa "utility" che non c'entra veramente nulla con i principi di OOP comuni a tutti gli altri linguaggi. Trattasi infatti di una alternativa ai classici getter e setter.
Non ti spaventare!

Anonimo ha detto...

Ottima guida bravissimo.

Posta un commento