1. Introduzione
  2. Iniziamo
  3. Nemici Multipli
  4. Game Over
  5. Punteggi e Orologio
  6. Tanti Piccoli Miglioramenti
  7. Usiamo la Tastiera
  8. Aggiungere un Preloader (Parte 1 - Parte 2)
  9. Aggiungere Musica ed Effetti Sonori
  10. Livelli Multipli
  11. Salvare e Caricare Informazioni
  12. Garbage Collection

Ed eccoci arrivati alla nostra ultima parte. Ormai abbiamo aggiunto tutte le nostre feature: siamo partiti da zero fino ad arrivare ad un gioco praticamente completo di tutto. Nemici, doppio sistema di controllo, livelli multipli, piccole ottimizzazioni qua e la. Insomma, la cosa inizia a farsi più seria di un gioco.

In quest’ultima lezione vorrei parlare di un concetto molto importante: la Garbage Collection. Prima però di effettuare una qualsiasi analisi approfondita, dobbiamo tornare indietro e parlare di qualcosa (in maniera più approfondita ovviamente) alla base di tutto.

Parliamo di variabili!

Nota: se state leggendo a partire da questo capitolo (si, può capitare) prendete i file alla fine della lezione precedente. Altrimenti sapete già cosa fare ;)


Variabili, conosciute o sconosciute?

Quando noi scriviamo qualcosa simile a:

var score:Number = 100;

potete pensare, intuitivamente, di aver creato una casella. Una casella con un “etichetta” (il nome della variabile) con dentro un valore. In questo caso il valore è 100.

AvoiderGame-Part12-D01

Diciamo che è un modo di pensare a questo concetto abbastanza buono, perlomeno per stringhe, booleani e i vari tipi numerici. Ma se noi invece scrivessimo:

var avatar:Avatar = new Avatar();

Presumibilmente, stiamo facendo la stessa cosa. Stiamo creando per caso una casella con scritto “avatar” in corrispondenza dell’etichetta e con un oggetto di tipo Avatar dentro? Beh, non proprio. Il discorso inizia a cambiare. Pensate come a queste due cose, la casella e l’avatar, separate. La casella riporta la sua etichetta per identificare il nome, mentre l’Avatar sembra un qualcosa di “gravitante” nello spazio. Come potete vedere nell’immagine di seguito, le due entità sono collegate:

AvoiderGame-Part12-D02

Cosa vuol dire tutto questo?

Ritorniamo un attimo ai nostri numeri. Considerate questo codice:

var score:Number = 100;
var newScore:Number = score;

Qui, la variabile “newScore” copierà in se stessa il valore della variabile “score”.

AvoiderGame-Part12-D03

Tornando invece a parlare degli avatar, guardate questo codice:

var avatar:Avatar = new Avatar();
var newAvatar:Avatar = avatar;

in questo caso, con il nostro oggetto “gravitante” cosa succede? L’immagine di seguito inizia a spiegare qualcosa, ma potremmo non avere le idee chiare:

AvoiderGame-Part12-D04

Questo può causare molta confusione se provassimo a cambiare dei valori di una delle due variabili. Proviamo il codice seguente (parlando dei nostri cari numeri):

newScore = 250;
trace( "score is:", score );
trace( "newScore is:", newScore );

le cose qui sono quasi ovvie. “newScore” è uguale a 250, “score” è uguale a 100 ed infatti il risultato sarà:
score is: 100
newScore is: 250

Esattamente quello che ci aspettavamo.

Proviamo a fare qualcosa del genere con i nostri avatar adesso, testando queste linee di codice che trovate di seguito.

avatar.scaleY = 1;
newAvatar.scaleY = 2;
trace( "avatar.scaleY is:", avatar.scaleY );
trace( "newAvatar.scaleY is:", newAvatar.scaleY );

(scaleY è una proprietà del Movie Clip, la classe base ;) )

Il codice attualmente scritto produrrà quest’output:

avatar.scaleY is: 2
newAvatar.scaleY is: 2

Mmmmm… c’è qualcosa che non va! Tuttavia, questi risultati hanno un senso se consideriamo la visuale dell’immagine poco sopra, con le due caselle “connesse” ad un unico oggetto. Infondo, stavamo parlando dello stesso oggetto, quindi le modifiche su uno hanno effetto anche sull’altro.

Andiamo avanti. Nelle prime parti di questa guida, avevo detto qualcosa riguardo al null, del fatto che in poche parole serve a cancellare una variabile. Come concetto per iniziare va bene ma quella che facevo era una grossolana semplificazione di un concetto più profondo. Per stringhe, numeri, booleani, la mia spiegazione è valida.

score = null;

che si traduce in

AvoiderGame-Part12-D07

e fino a qui ci siamo. Per altri tipi di oggetti però, come per esempio il nostro onnipresente avatar, le cose cambiano. Ciò che viene annullato è il famoso “collegamento” tra la casella e l’oggetto:

avatar = null;

potrà essere visto come:

AvoiderGame-Part12-D08

Qui la variabile “avatar” non è più collegata con il suo oggetto di classe Avatar, per cui effettuando il trace di una sua variabile (vedi avatar.scaleY). L’oggetto Avatar esiste ancora, così come la variabile “newAvatar”. Per cui, facendo stavolta il trace di “newAvatar.scaleY” avremo come risultato 2, come prima.

Questo può essere uno stratagemma utile, se viene compreso, nel caso dovessimo creare un oggetto in una classe o funzione per passarlo ad un altra classe (o funzione) per farci delle operazioni. È sicuramente meglio del prendere, copiare ed incollare codice magari cancellando anche l’originale.

Tuttavia, questo nostro ragionamento porta ad una interessantissima domanda: cosa succede se impostiamo anche newAvatar come null?

Il risultato può essere intuito come quello in figura:

AvoiderGame-Part12-D09

Ora, il nostro Avatar si ritrova in una terra di nessuno. Abbiamo distrutto tutti i suoi riferimenti e non possiamo accederci più in alcun modo. Questo non toglie che non esiste più: è ancora lì, nella memoria del nostro PC. Detta così sembra quasi una minaccia, anche se in effetti in questo caso non è. Pensate però ad un gioco come il nostro, in cui tutti i nemici potrebbero essere lasciati in memoria, insieme all’avatar, musiche, sfondi, effetti sonori e le varie schermate.

Le cose potrebbero diventare pesanti, ed il gioco inizierebbe ad accusare una forte lentezza. “Inizierebbe” perché abbiamo a disposizione uno strumento potente come il cosiddetto “Garbage Collector”.


Garbage Collection in Flash

Il Flash Player ha un tool incorporato che si occupa di questo compito: il Garbage Collector. Si occupa infatti di “pulire” rimuovendo dalla memoria questi oggetti morti, senza collegamenti. “Ogni tanto” esegue i dovuti controlli sugli oggetti interessati e in caso li elimina. Odio questo termine, “ogni tanto”, perché è decisamente vago: tuttavia, non sappiamo effettivamente di preciso quando questa operazione ha luogo.

Comunque sia, se l’oggetto non ha più collegamenti viene eliminato. In egual modo, se una variabile è collegata ad un oggetto, ma questa variabile è contenuta in un altra senza nessun collegamento, questa viene eliminata. Un caso di questo genere può essere proprio il nostro “avatar”, legato al “playScreen” a sua volta parte della classe documento.

AvoiderGame-Part12-D10

^ Click per Ingrandire ^

Quindi, rimuovendo il riferimento (collegamento) al playScreen in questo modo nella document class:

playScreen = null;

La conseguenza sarà

AvoiderGame-Part12-D12

^ Click per Ingrandire ^

Di conseguenza, il nostro playScreen perderà la sua referenza, ed allo stesso tempo anche il nostro avatar (nonostante sia collegato ad Avatar) viene rimosso dalla memoria in quanto il suo “contenitore” subisce la stessa sorte.

Questo ragionamento, come potete ben immaginare, va esteso idealmente all’infinito. Supponendo per esempio che “avatar” abbia un altro oggetto al suo interno, anche quest’ultimo verrà cancellato. Volendo essere più precisi, si può dire che qualsiasi oggetto non avente un collegamento diretto o indiretto con la classe documento verrà cancellato.

Per maggiori informazioni sull’argomento, posso consigliarvi questi fantastici post di Grant Skinner (in inglese) sul tema. Oltre alla creazione di variabili, ricordate anche che i collegamenti vengono creati anche dalle operazioni di addChild.

Ora, avviate il vostro gioco e tirate la finestra verticalmente, in modo tale da allungarla:

AvoiderGame_Part12_02

AvoiderGame_Part12_03

Esatto, così.

Iniziate a giocare, ricordandovi che il seguente codice viene eseguito:

public function onRequestStart( navigationEvent:NavigationEvent ):void
{
  playScreen = new AvoiderGame();
  playScreen.addEventListener( AvatarEvent.DEAD, onAvatarDeath );
  playScreen.x = 0;
  playScreen.y = 0;
  addChild( playScreen );

  menuScreen = null;
}

“menuScreen” viene impostato su null, e quindi:

AvoiderGame_Part12_04

Vedete? Il nostro menuScreen è rimasto ancora lì! Il Garbage Collector non ancora è stato mandato in esecuzione (testate comunque la sua efficacia continuando a giocare per qualche secondo ;) ). Quello che succede è presto spiegato: nonostante abbiamo “nullato” il nostro menuScreen, dobbiamo considerare che abbiamo eseguito l’operazione di addChild per il nostro menu. Questo vuol dire che ha ancora un collegamento alla classe documento.

Per ottimizzare il nostro lavoro, oltre a “nullare” il menuScreen, dobbiamo eseguire l’operazione contraria all’addChild. E, originalità a palate, indovinate come si chiama?

public function onRequestStart( navigationEvent:NavigationEvent ):void
{
  playScreen = new AvoiderGame();
  playScreen.addEventListener( AvatarEvent.DEAD, onAvatarDeath );
  playScreen.x = 0;
  playScreen.y = 0;
  addChild( playScreen );

  removeChild( menuScreen );
  menuScreen = null;
}

Esattamente, removeChild. Il risultato a livello di gameplay? Eccolo!

AvoiderGame_Part12_05

Ora le cose vanno moooolto meglio, vero? Beh oddio fino ad un certo punto, considerando che abbiamo perso il controllo della nostra tastiera! Se clicchiamo sull’area di gioco torniamo ad averne il controllo. Cosa diamine e successo?

Ricordate la lezione sette? Avevamo parlato di input di tastiera e vi avevo spiegato come l’oggetto “stage” si occupava di gestire tutto ciò che riguarda l’input da tastiera. Nello specifico, lo stage conteneva una referenza ad un oggetto concentrato (oppure “con il focus”) sulla tastiera. Questo oggetto passava le informazioni di input allo stage e questo le processava di conseguenza.

Per rendere meglio l’idea, immaginate di aver sviluppato un applicazione in flash con delle textbox dove inserire del testo. Cliccando su ognuna di queste, di volta in volta, effettuiamo il “focus” su di esse. Un po’ come quando nel nostro sistema operativo clicchiamo su una finestra e poi su un altra, facendola arrivare in primo piano. Il concetto di focus è essenzialmente questo.

Quando noi abbiamo cliccato sul pulsante per far partire il gioco, abbiamo effettuato il “focus” sulla finestra di gioco. Rimosso l’oggetto dalla memoria, invece, abbiamo rimosso anche il focus dal nostro flash.

Fortunatamente, nonostante tutte queste spiegazioni, basta una sola riga di codice per risolvere il problema. Basta ridare il focus all’oggetto adatto, ovvero il nostro playScreen. L’istruzione stage.focus è quella che ci serve:

public function onRequestStart( navigationEvent:NavigationEvent ):void
{
  playScreen = new AvoiderGame();
  playScreen.addEventListener( AvatarEvent.DEAD, onAvatarDeath );
  playScreen.x = 0;
  playScreen.y = 0;
  addChild( playScreen );

  removeChild( menuScreen );
  menuScreen = null;

  stage.focus = playScreen;
}

Salviamo e testiamo tutto. C’è un piccolo problema, il nostro stage è circondato da un “bordo” giallo, decisamente brutto ed antiestetico.

AvoiderGame_Part12_08

È un modo per indicare il focus su quell’area. A noi non piace per cui lo rimuoviamo al volo. Aggiungiamo questa riga di codice alla classe documento, nel costruttore.

stage.stageFocusRect = false;

Ora, date queste nostre nuove conoscenze, non dobbiamo fare altro che applicarle in tutta la classe documento, per garantire un ottimizzazione precisa:
package
{
  //Avoider Game Tutorial, by Michael James Williams
  //http://gamedev.michaeljameswilliams.com

  import flash.display.MovieClip;
  import flash.events.Event;
  import flash.events.ProgressEvent;
  public class DocumentClass extends MovieClip
  {
    public var menuScreen:MenuScreen;
    public var playScreen:AvoiderGame;
    public var gameOverScreen:GameOverScreen;
    public var loadingProgress:LoadingProgress;

    public function DocumentClass()
    {
      loadingProgress = new LoadingProgress();
      loadingProgress.x = 200;
      loadingProgress.y = 150;
      addChild( loadingProgress );
      loaderInfo.addEventListener( Event.COMPLETE, onCompletelyDownloaded );
      loaderInfo.addEventListener( ProgressEvent.PROGRESS, onProgressMade );
      stage.stageFocusRect = false;
    }

    public function onCompletelyDownloaded( event:Event ):void
    {
      removeChild( loadingProgress );
      gotoAndStop(3);
      showMenuScreen();
    }

    public function onProgressMade( progressEvent:ProgressEvent ):void
    {
      loadingProgress.setValue( Math.floor( 100 * loaderInfo.bytesLoaded / loaderInfo.bytesTotal ) );
    }

    public function showMenuScreen():void
    {
      menuScreen = new MenuScreen();
      menuScreen.addEventListener( NavigationEvent.START, onRequestStart );
      menuScreen.x = 0;
      menuScreen.y = 0;
      addChild( menuScreen );

      stage.focus = menuScreen;
    }

    public function onAvatarDeath( avatarEvent:AvatarEvent ):void
    {
      var finalScore:Number = playScreen.getFinalScore();
      var finalClockTime:Number = playScreen.getFinalClockTime();

      gameOverScreen = new GameOverScreen();
      gameOverScreen.addEventListener( NavigationEvent.RESTART, onRequestRestart );
      gameOverScreen.x = 0;
      gameOverScreen.y = 0;
      gameOverScreen.setFinalScore( finalScore );
      gameOverScreen.setFinalClockTime( finalClockTime );
      addChild( gameOverScreen );

      removeChild( playScreen );
      playScreen = null;

      stage.focus = gameOverScreen;
    }

    public function onRequestStart( navigationEvent:NavigationEvent ):void
    {
      playScreen = new AvoiderGame();
      playScreen.addEventListener( AvatarEvent.DEAD, onAvatarDeath );
      playScreen.x = 0;
      playScreen.y = 0;
      addChild( playScreen );

      removeChild( menuScreen );
      menuScreen = null;

      stage.focus = playScreen;
    }

    public function onRequestRestart( navigationEvent:NavigationEvent ):void
    {
      restartGame();
    }

    public function restartGame():void
    {
      playScreen = new AvoiderGame();
      playScreen.addEventListener( AvatarEvent.DEAD, onAvatarDeath );
      playScreen.x = 0;
      playScreen.y = 0;
      addChild( playScreen );

      removeChild( gameOverScreen );
      gameOverScreen = null;

      stage.focus = playScreen;
    }
  }
}

Come potete notare voi stessi, alla fine non sono molte le modifiche da fare. Ora possiamo continuare con la nostra ottimizzazione. Date un occhiata alla schermata di gioco:

AvoiderGame_Part12_06

^ Click per Ingrandire ^

Guardate questi nemici! Dopo essere caduti nella schermata rimangono oltre l’area normalmente visibile. Questo significa che nessuno di loro verrà coinvolto nella Garbage Collection fin quando il gioco non finisce. È assolutamente un male: se il giocatore riesce a resistere un sacco di tempo avrà dei problemi di rallentamento non indifferenti.

Per prima cosa, nel costruttore del nostro playScreen provvediamo a cancellare queste tre linee di codice:

var newEnemy = new Enemy( 100, -15 );
army.push( newEnemy );
addChild( newEnemy );

non abbiamo più bisogno di questo nemico, in quanto ce ne sono già centinaia che verranno generati casualmente.

E adesso cancelliamo i nemici dopo il loro uso.


Rimuovere oggetti dall’Array

Ecco un problema che, durante le prime volte in cui si tenta di risolverlo, può indurre in errore con facilità. Il nostro obiettivo, attualmente, è eliminare i nemici che vanno oltre il margine inferiore dello schermo: precisamente oltre i 350 pixel.

Per cui, intuitivamente, il codice viene da se:

for each ( var enemy:Enemy in army )
{
  enemy.moveABit();
  if ( PixelPerfectCollisionDetection.isColliding( avatar, enemy, this, true ) )
  {
    gameTimer.stop();
    avatarHasBeenHit = true;
  }
  if ( enemy.y > 350 )
  {
    //il codice per la rimozione va qui
  }
}

(attualmente ci troviamo nella funzione onTick)

Ebbene, dovete sapere che questo codice molto spesso porta ad enormi problemi. Come penso sappiate, gli array sono delle liste, ed ogni elemento di questa lista è un riferimento ad un oggetto. Per esempio, “army[0]” indica il primo nemico nell’array, “army[1]” il secondo e così via.

Quando usiamo il ciclo for each (var enemy:Enemy in army) è come dire “poni enemy = army[0], fai le operazioni descritte, dopodiché poni enemy = army[1], fai le operazioni descritte e.c.c.”. Fino a qui il nostro ragionamento non fa una piega.

Ma ecco che spunta il problema. Supponiamo di voler cancellare il nemico army[4], perché ha raggiunto la fine della schermata. Attualmente, quindi, l’indice sul quale lavoriamo è 4. Cancellando dalla lista il nemico di cui stiamo parlando, gli altri davanti, ovviamente, “scalano di posizione”: il 5 passa al 4, il 6 passa al 5 e così via fino alla fine.

Dopo le operazioni, il ciclo riparte (ci trovavamo al numero 4) e passa al numero 5. Rifletteteci, abbiamo saltato un elemento! È si giusto passare dal 4 al 5, ma dovremmo riprendere in considerazione ancora il 4, dato che l’altro è stato cancellato!

Può essere astruso come concetto, lo so. Per rendere meglio l’idea ecco un altro esempio più pratico. Supponiamo di dover preparare qualcosa per stasera: l’idea è una macedonia. Ci siamo fatti la nostra lista della spesa:

  1. Mele
  2. Arance
  3. Banane
  4. Pere
  5. Fragole

Iniziamo il nostro ciclo for each sulla lista della spesa. L’elemento selezionato attualmente sarà evidenziato in grassetto.

Prima tappa:

L’indice della lista è 1.

  1. Mele
  2. Arance
  3. Banane
  4. Pere
  5. Fragole

le Mele ci sono. Perfetto. Andiamo avanti.

L’indice della lista è 2.

  1. Mele
  2. Arance
  3. Banane
  4. Pere
  5. Fragole

anche le arance sono state trovate. Avanti.

L’indice della lista è 3.

  1. Mele
  2. Arance
  3. Banane
  4. Pere
  5. Fragole

cavolo, le banane non ci sono! Rimuoviamole dalla lista e rimettiamo i numeri al posto giusto (dato che così funziona un array ;) )

L’indice della lista è ora 4.

  1. Mele
  2. Arance
  3. Pere
  4. Fragole

le Fragole ci sono. Abbiamo finito la nostra spesa. Torniamo a casa e ci accorgiamo che ci siamo completamente scordati le pere!!! La macedonia non si potrà fare e voi stasera morirete di fame.

Ok, torniamo alla programmazione. Come possiamo risolvere questo increscioso problema? Semplice, basta lavorare al contrario: facendo un ciclo che prende in considerazione (uno alla volta) tutti gli elementi dell’array a partire dall’ultimo per arrivare al primo, questo problema non ci tocca minimamente.

Proviamo con la nostra lista della spesa.

L’indice della lista è 5.

  1. Mele
  2. Arance
  3. Banane
  4. Pere
  5. Fragole

le fragole ci sono, andiamo avanti.

L’indice della lista è 4.

  1. Mele
  2. Arance
  3. Banane
  4. Pere
  5. Fragole

anche le pere ci sono, avanti!

L’indice della lista è 3.

  1. Mele
  2. Arance
  3. Banane
  4. Pere
  5. Fragole

anche stavolta le Banane non ci sono. Cancelliamo questo frutto maledetto e aggiorniamo la numerazione della lista.

  1. Mele
  2. Arance
  3. Pere
  4. Fragole

L’indice ora è 2.

  1. Mele
  2. Arance
  3. Pere
  4. Fragole

le arance ci sono. Non farò l’ultimo passaggio, il succo del ragionamento l’ho affrontato. Dovrebbe essere chiaro anche per voi ora! Ora, non possiamo forzare il funzionamento di un ciclo for each. Per definizione questi partono dal primo elemento per arrivare all’ultimo.

Cambiamo quindi istruzione. Usiamo un bel while!

var i:int = army.length - 1;    //int significa "Numero Intero"
var enemy:Enemy;
while ( i > -1 )
{
  enemy = army[i];
  //altro codice qui.
  i = i - 1;
}

Il codice che vedete è facile da capire. Ma vi voglio bene e spiegherò comunque tutto.

  • var i:int indica una variabile numerica, che consente l’uso di valori esclusivamente interi. (1 e 5 ok, 2.38 non è intero);
  • army.lenght – 1 indica la grandezza del nostro array. Il “-1” che andiamo a sottrarre serve perché con il nostro ciclo arriviamo ad army[0], e non army[1]!
  • var enemy:Enemy è la variabile che useremo sulla quale fare le operazioni. Non ancora le diamo una referenza;
  • while ( i > –1) è il ciclo che scandaglierà il nostro array elemento per elemento, a partire dall’ultimo elemento fino ad arrivare allo 0 (l’ultimo numero maggiore di –1 :) )
  • i = i – 1 è invece l’operazione di decremento che ci serve per tornare indietro nel nostro array. Se vi scordate questa istruzione flash inizierà a gestire un loop infinito che durerà 30 secondi, mandando poi il gioco in crash;

Vediamo come appare la nostra funzione onTick, adesso.

var i:int = army.length - 1;
var enemy:Enemy;
while ( i > -1 )
{
  enemy = army[i];
  enemy.moveABit();
  if ( PixelPerfectCollisionDetection.isColliding( avatar, enemy, this, true ) )
  {
    gameTimer.stop();
    avatarHasBeenHit = true;
  }
  if ( enemy.y > 350 )
  {
    //codice per la rimozione del nemico
  }
  i = i - 1;
}

Bene, l’ultima cosa che ci rimane da fare è trovare il codice necessario alla rimozione del nostro nemico. L’istruzione, che metteremo nel blocco commentato poco sopra, sarà:
if ( enemy.y > 350 )
{
  army.splice( i, 1 );
}

Splice è la funzione adatta. Il primo parametro indica il punto di inizio per “l’eliminazione” e la seconda cifra (nel nostro caso 1) indica il numero di elementi da cancellare a partire dal punto i.

Salvate e testate il vostro gioco, allungando verticalmente la finestra, ed iniziate a giocare.

AvoiderGame_Part12_07

I nostri nemici vengono rimossi dall’array, ma non dallo schermo! (la rimozione è testimoniata dal fatto che, essendo rimossi dall’array, non vengono più mossi sullo schermo, fermandosi in basso) Questo è ovvio, dato che li abbiamo precedentemente addChild-ati.

Per cui, rimuoviamoli!

if ( enemy.y > 350 )
{
  removeChild( enemy );
  army.splice( i, 1 );
}

Et voilà, il vostro gioco ora funzionerà perfettamente e non soffrirà dei rallentamenti per questa svista. Continuiamo con la nostra ottimizzazione.


Continuiamo la nostra Pulizia

Quello che abbiamo fatto fino ad adesso, per quanto possa sembrare poco, è un bel lavoro di ottimizzazione del nostro codice. Lo rende decisamente più corretto, ma c’è ancora un po’ di roba da fare. Stavolta parliamo di eventi.

Quando aggiungiamo un EventListener, infatti, le funzioni vengono (in un certo senso) viste come degli oggetti. Fin quando l’EventListener esiste anche l’oggetto associato a quella funzione, quindi, esisterà. Fortunatamente, c’è un modo semplice e veloce per risolvere questo problema: si chiama la “weak reference”, in Italiano “riferimento debole”.

In poche parole, invece di rimanere in memoria, una funzione che ha un sacco di weak reference viene rimossa. A livello di codice bisogna solo modificare l’istruzione addEventListener:

Da questo formato

addEventListener( TipoEvento.EVENTO, unaFunzione );

a questo
addEventListener( TipoEvento.EVENTO, unaFunzione, false, 0, true );  //weak ref

Ignorate il terzo ed il quarto parametro, per ora non è necessario capirli. Il quinto parametro, se impostato su “true”, provvede all’uso della weak reference.

Nota: se volete più informazioni sui due parametri, date un occhiata Qui.

Come Michael e Grant Skinner, raccomando l’uso di queste weak reference, in quanto più spazio c’è in memoria e meglio è. Non c’è bisogno di sovraccaricare un sistema se non ci sono determinate necessità, vero?

Ecco la nostra document class dopo il “restyling”:

package
{
  //Avoider Game Tutorial, by Michael James Williams
  //http://gamedev.michaeljameswilliams.com

  import flash.display.MovieClip;
  import flash.events.Event;
  import flash.events.ProgressEvent;
  public class DocumentClass extends MovieClip
  {
    public var menuScreen:MenuScreen;
    public var playScreen:AvoiderGame;
    public var gameOverScreen:GameOverScreen;
    public var loadingProgress:LoadingProgress;

    public function DocumentClass()
    {
      loadingProgress = new LoadingProgress();
      loadingProgress.x = 200;
      loadingProgress.y = 150;
      addChild( loadingProgress );
      loaderInfo.addEventListener( Event.COMPLETE, onCompletelyDownloaded, false, 0, true );
      loaderInfo.addEventListener( ProgressEvent.PROGRESS, onProgressMade, false, 0, true );
      stage.stageFocusRect = false;
    }

    public function onCompletelyDownloaded( event:Event ):void
    {
      removeChild( loadingProgress );
      gotoAndStop(3);
      showMenuScreen();
    }

    public function onProgressMade( progressEvent:ProgressEvent ):void
    {
      loadingProgress.setValue( Math.floor( 100 * loaderInfo.bytesLoaded / loaderInfo.bytesTotal ) );
    }

    public function showMenuScreen():void
    {
      menuScreen = new MenuScreen();
      menuScreen.addEventListener( NavigationEvent.START, onRequestStart, false, 0, true );
      menuScreen.x = 0;
      menuScreen.y = 0;
      addChild( menuScreen );

      stage.focus = menuScreen;
    }

    public function onAvatarDeath( avatarEvent:AvatarEvent ):void
    {
      var finalScore:Number = playScreen.getFinalScore();
      var finalClockTime:Number = playScreen.getFinalClockTime();

      gameOverScreen = new GameOverScreen();
      gameOverScreen.addEventListener( NavigationEvent.RESTART, onRequestRestart, false, 0, true );
      gameOverScreen.x = 0;
      gameOverScreen.y = 0;
      gameOverScreen.setFinalScore( finalScore );
      gameOverScreen.setFinalClockTime( finalClockTime );
      addChild( gameOverScreen );

      removeChild( playScreen );
      playScreen = null;

      stage.focus = gameOverScreen;
    }

    public function onRequestStart( navigationEvent:NavigationEvent ):void
    {
      playScreen = new AvoiderGame();
      playScreen.addEventListener( AvatarEvent.DEAD, onAvatarDeath, false, 0, true );
      playScreen.x = 0;
      playScreen.y = 0;
      addChild( playScreen );

      removeChild( menuScreen );
      menuScreen = null;

      stage.focus = playScreen;
    }

    public function onRequestRestart( navigationEvent:NavigationEvent ):void
    {
      restartGame();
    }

    public function restartGame():void
    {
      playScreen = new AvoiderGame();
      playScreen.addEventListener( AvatarEvent.DEAD, onAvatarDeath, false, 0, true );
      playScreen.x = 0;
      playScreen.y = 0;
      addChild( playScreen );

      removeChild( gameOverScreen );
      gameOverScreen = null;

      stage.focus = playScreen;
    }
  }
}

e qui invece potete osservare la classe AvoiderGame rimessa a nuovo:
package
{
  import flash.display.MovieClip;
  import flash.utils.Timer;
  import flash.events.TimerEvent;
  import flash.ui.Mouse;
  import flash.events.KeyboardEvent;
  import flash.ui.Keyboard;
  import flash.events.Event;
  import flash.media.SoundChannel;

  public class AvoiderGame extends MovieClip
  {
    public var army:Array;
    public var enemy:Enemy;
    public var avatar:Avatar;
    public var gameTimer:Timer;
    public var useMouseControl:Boolean;
    public var downKeyIsBeingPressed:Boolean;
    public var upKeyIsBeingPressed:Boolean;
    public var leftKeyIsBeingPressed:Boolean;
    public var rightKeyIsBeingPressed:Boolean;
    public var backgroundMusic:BackgroundMusic;
    public var bgmSoundChannel:SoundChannel;  //bgm for BackGround Music
    public var enemyAppearSound:EnemyAppearSound;
    public var sfxSoundChannel:SoundChannel;  //sfx for Sound FX
    public var currentLevelData:LevelData;

    public function AvoiderGame()
    {
      currentLevelData = new LevelData( 1 );
      setBackgroundImage();

      backgroundMusic = new BackgroundMusic();
      bgmSoundChannel = backgroundMusic.play();
      bgmSoundChannel.addEventListener( Event.SOUND_COMPLETE, onBackgroundMusicFinished, false, 0, true );
      enemyAppearSound = new EnemyAppearSound();

      downKeyIsBeingPressed = false;
      upKeyIsBeingPressed = false;
      leftKeyIsBeingPressed = false;
      rightKeyIsBeingPressed = false;

      useMouseControl = false;
      Mouse.hide();
      army = new Array();

      avatar = new Avatar();
      addChild( avatar );
      if ( useMouseControl )
      {
        avatar.x = mouseX;
        avatar.y = mouseY;
      }
      else
      {
        avatar.x = 200;
        avatar.y = 250;
      }

      gameTimer = new Timer( 25 );
      gameTimer.addEventListener( TimerEvent.TIMER, onTick, false, 0, true );
      gameTimer.start();

      addEventListener( Event.ADDED_TO_STAGE, onAddToStage, false, 0, true );
    }

    public function onBackgroundMusicFinished( event:Event ):void
    {
      bgmSoundChannel = backgroundMusic.play();
      bgmSoundChannel.addEventListener( Event.SOUND_COMPLETE, onBackgroundMusicFinished, false, 0, true );
    }

    public function onAddToStage( event:Event ):void
    {
      addEventListener( KeyboardEvent.KEY_DOWN, onKeyPress, false, 0, true );
      addEventListener( KeyboardEvent.KEY_UP, onKeyRelease, false, 0, true );
    }

    public function onKeyPress( keyboardEvent:KeyboardEvent ):void
    {
      if ( keyboardEvent.keyCode == Keyboard.DOWN )
      {
        downKeyIsBeingPressed = true;
      }
      else if ( keyboardEvent.keyCode == Keyboard.UP )
      {
        upKeyIsBeingPressed = true;
      }
      else if ( keyboardEvent.keyCode == Keyboard.LEFT )
      {
        leftKeyIsBeingPressed = true;
      }
      else if ( keyboardEvent.keyCode == Keyboard.RIGHT )
      {
        rightKeyIsBeingPressed = true;
      }
    }

    public function onKeyRelease( keyboardEvent:KeyboardEvent ):void
    {
      if ( keyboardEvent.keyCode == Keyboard.DOWN )
      {
        downKeyIsBeingPressed = false;
      }
      else if ( keyboardEvent.keyCode == Keyboard.UP )
      {
        upKeyIsBeingPressed = false;
      }
      else if ( keyboardEvent.keyCode == Keyboard.LEFT )
      {
        leftKeyIsBeingPressed = false;
      }
      else if ( keyboardEvent.keyCode == Keyboard.RIGHT )
      {
        rightKeyIsBeingPressed = false;
      }
    }

    public function onTick( timerEvent:TimerEvent ):void
    {
      gameClock.addToValue( 25 );
      if ( Math.random() < currentLevelData.enemySpawnRate )
      {
        var randomX:Number = Math.random() * 400;
        var newEnemy:Enemy = new Enemy( randomX, -15 );
        army.push( newEnemy );
        addChild( newEnemy );
        gameScore.addToValue( 10 );
        sfxSoundChannel = enemyAppearSound.play();
      }
      if ( useMouseControl )
      {
        avatar.x = mouseX;
        avatar.y = mouseY;
      }
      else
      {
        if ( downKeyIsBeingPressed )
        {
          avatar.moveABit( 0, 1 );
        }
        else if ( upKeyIsBeingPressed )
        {
          avatar.moveABit( 0, -1 );
        }
        else if ( leftKeyIsBeingPressed )
        {
          avatar.moveABit( -1, 0 );
        }
        else if ( rightKeyIsBeingPressed )
        {
          avatar.moveABit( 1, 0 );
        }
      }

      var avatarHasBeenHit:Boolean = false;
      var i:int = army.length - 1;
      var enemy:Enemy;
      while ( i > -1 )
      {
        enemy = army[i];
        enemy.moveABit();
        if ( PixelPerfectCollisionDetection.isColliding( avatar, enemy, this, true ) )
        {
          gameTimer.stop();
          avatarHasBeenHit = true;
        }
        if ( enemy.y > 350 )
        {
          removeChild( enemy );
          army.splice( i, 1 );
        }
        i = i - 1;
      }
      if ( avatarHasBeenHit )
      {
        bgmSoundChannel.stop();
        dispatchEvent( new AvatarEvent( AvatarEvent.DEAD ) );
      }

      if ( gameScore.currentValue >= currentLevelData.pointsToReachNextLevel )
      {
        currentLevelData = new LevelData( currentLevelData.levelNum + 1 );
        setBackgroundImage();
      }
    }

    public function setBackgroundImage():void
    {
      if ( currentLevelData.backgroundImage == "blue" )
      {
        backgroundContainer.addChild( new BlueBackground() );
      }
      else if ( currentLevelData.backgroundImage == "red" )
      {
        backgroundContainer.addChild( new RedBackground() );
      }
    }

    public function getFinalScore():Number
    {
      return gameScore.currentValue;
    }

    public function getFinalClockTime():Number
    {
      return gameClock.currentValue;
    }
  }
}

Le altre classi da rimettere a nuovo sono GameOverScreen e MenuScreen. Penso tu sia capace di farlo da solo, anche perchè c’è da gestire solo un EventListener per classe :)

E così abbiamo gestito la nostra GarbageCollection! Ora l’ultimo ritocco ed è finita.


Dichiariamo Manualmente l’istanza sullo Stage

Vi ricordate taaaaaanto, ma tanto tempo fa, durante il nostro primo incontro, quando vi dissi che dovevamo controllare di aver selezionato “Dichiara automaticamente le istanze sullo stage” tramite il menù “File > Impostazioni Pubblicazione > Flash > Impostazioni”?  Bene, dopo ben 12 lezioni stiamo andando a deselezionarlo.

Ma a che cosa serve?  Date un occhiata al simbolo “PlayScreen”. Contiene un oggetto Score, un oggetto Clock. Nessun Avatar. Se diamo un occhiata alla classe corrispondente, AvoiderGame, noterete che ha il codice “public var avatar:Avatar”, mentre non troviamo nulla di corrispondente per Score e Clock.

Dunque, selezionando “Dichiara automaticamente le istanze sullo stage” facciamo in modo che flash inserisca le linee di codice”public var clock:Clock;” e “public var score:Score;” automaticamente, quando faremo partire il gioco. Deselezionandolo, invece, dovremo dichiarare tutto da noi.

Perché, quindi, annullare questo aiuto e assegnarci del lavoro extra? Semplicemente perché l’editor che ci mette a disposizione Flash per lavorare è decisamente scarso, rispetto ad altri in circolazione. Ci sono programmi come FlashDevelop, per esempio, che fanno dei lavori allucinanti: import automaticamente inseriti, eventi creati in base alle necessità degli utenti e tante altre feature interessanti.

Per trarre il massimo dall’uso di questi programmi però, dobbiamo evitare di far dichiarare a flash le istanze. Per questo deselezionate la voce e provate a salvare tutto per poi testare. Come potete immaginare riceverete un sacco di errori.

AvoiderGame_Part12_11

Quello che dovete fare è semplice. Fare il doppio click su ognuno degli errori e per l’oggetto interessato modificare il codice della classe nel quale veniamo portati, aggiungendo un “public var e.c.c.”. Per esempio il primo errore che otteniamo noi è:

1120: Access of undefined property gameClock.

Doppio-clicchiamo e ci ritroviamo nella classe AvoiderGame, linea 122.
gameClock.addToValue( 25 );

Andiamo quindi all’inizio del file ed aggiungiamo al posto giusto la dichiarazione di quest’altra variabile.
public class AvoiderGame extends MovieClip
{
  public var gameClock:Clock;

Ripetete il processo per tutti gli errori e non avrete più problemi! Non ci metterete molto, non vi preoccupate.

E così, eccoci giunti alla fine. Le idee per ampliare il nostro gioco sono tante, e sul sito originale di Michael James Williams ne sono arrivate a palate. Quello che vi consiglio, quindi, è di iniziare a leggere qualcosa anche in inglese, dato che nel bene e nel male è sempre la lingua dei programmatori.

È stato un piacere ed un onore curare questa traduzione e spero che non sia rivelata troppo difficile. In certi casi ho esposto dei concetti che, affrontati la prima volta, sono tosti da mandare giù. Nonostante tutto ho cercato di essere il più chiaro possibile e di fare esempi altrettanto cristallini.

Potete trovare i file di questa lezione Qui, come al solito.

Vi saluto, promettendovi altre guide (spero altrettanto interessanti) sull’argomento!

Grazie dell’ascolto!

  • Share/Bookmark