Creare un Gioco in Flash – Parte 6: Tanti Piccoli Miglioramenti
- Introduzione
- Iniziamo
- Nemici Multipli
- Game Over
- Punteggi e Orologio
- Tanti Piccoli Miglioramenti
- Usiamo la Tastiera
- Aggiungere un Preloader (Parte 1 - Parte 2)
- Aggiungere Musica ed Effetti Sonori
- Livelli Multipli
- Salvare e Caricare Informazioni
- Garbage Collection
Salve a tutti! In questo nostro capitolo della guida faremo un piccolo “break” per rilassarci un po’ e mettere a punto qualche piccolo miglioramento per il nostro gioco. Nelle lezioni precedenti abbiamo fatto davvero tanto, per questo è ora che vi riposiate un po’
Questi piccoli miglioramenti riguardano vari aspetti del gioco e anche, perché no, della programmazione. Accorgimenti estetici, oppure scelte legate alla scrittura del codice. Insomma, un po’ di tutto. La scelta di questi, inoltre, non è assolutamente casuale.
Il nostro sesto articolo è stato scritto infatti a seguito di svariate segnalazioni e richieste per queste piccole features. Caratteristiche che dovevano essere assolutamente spiegate.
Se volete iniziare da qua la serie di tutorials, prelevate i file alla fine dell’articolo precedente. Altrimenti usate i vostri file di backup.
Mouse: ora ti vedo, ora non ti vedo
Ecco una delle caratteristiche più richieste in assoluto: nascondere quel maledetto cursore del mouse. Esatto, te lo ritrovi lì e ti da un fastidio maledetto.
Anti-estetico, davvero.
Come si può fare per toglierlo quando non serve? Insomma, sembra quasi che il mouse stia reggendo il nostro avatar per il naso! Fortunatamente, cambiare le cose è veramente facile. Apriamo il file “AvoiderGame.as” e aggiungiamo la seguente linea di codice al costruttore:
public function AvoiderGame()
{
Mouse.hide();Ricordate, inoltre, che “Mouse” è una di quelle parole chiave da “importare”. Per cui:
package
{
import flash.display.MovieClip;
import flash.utils.Timer;
import flash.events.TimerEvent;
import flash.ui.Mouse;Salviamo tutto e mandiamo in esecuzione. Ecco cosa otterremo:
Decisamente meglio, quasi perfetto! Esatto, quasi, perché c’è qualcosa che ci da fastidio ancora. Provate ad urtare contro un nemico ed andare alla schermata del Game Over. Il cursore del mouse ovviamente non ci sarà e sarà impossibile cliccare sul pulsante di reset!
Mettiamo in ordine anche questo piccolo dettaglio. Apriamo il file GameOverScreen.as e modifichiamo il costruttore aggiungendo:
public function GameOverScreen()
{
Mouse.show();Ovviamente, dovremo importare ancora una volta “flash.ui.Mouse” per fare si che tutto funzioni senza problemi in fase di esecuzione. Inoltre, nonostante non sia necessario, è buona norma fare la stessa cosa anche per la classe MenuScreen (file MenuScreen.as).
Teoricamente non sarebbe indispensabile perché attualmente è impossibile accedere al menù dopo l’avvio della schermata di gioco (il Game Over lo riavvia solamente). Non potendo prevedere quali altre modifiche faremo successivamente, però, è buona cosa effettuare anche questa modifica.
Ed una cosa l’abbiamo fatta
Commenti, Commenti e Commenti
Al contrario di quanto si possa pensare agli inizi, i commenti sono uno strumento indispensabile e potente per ogni programmatore. Il concetto alla base che dobbiamo tenere presente è che flash ignorerà qualsiasi stringa nel codice che inizierà per “//”.
Per esempio:
//Flash ignora questa riga var demo:Number = 7; //Flash imposta la variabile "demo" con il valore 7, ma ignora il testo che stai leggendo
Lavorando nell’ide Flash o in qualsiasi altro editor dotato di riconoscimento della sintassi, i commenti verranno sempre evidenziati con colori differenti, in modo tale da renderne più facile ancora il riconoscimento da parte del programmatore.
A questo punto molti di voi si chiederanno: perché mai devo commentare il codice che io stesso faccio?
- per ricordarsi delle note relative ad una funzione o variabile precisa (scrivendo la nota vicino a quello che vogliamo nel codice)
- commentare del codice ne impedisce l’esecuzione, per cui possiamo usare i commenti anche per fare dei test
Vi sembrano due motivi idioti?
Potremmo fare un esempio con il codice del nostro gioco. Apriamo il file “AvoiderGame.as”: alla linea
gameTimer = new Timer( 25 );commentiamo (dato che ho il vizio di confondere sempre i numeri quando si parla di tempo)
gameTimer = new Timer( 25 ); //un tick avviene ogni 25ms = 0.025s | 40 volte al secondoOra non dovrò fare ogni volta i calcoli. Mi sento una persona migliore.
C’è anche la possibilità di commentare più linee del codice. Al posto di usare “//” per ogni riga (che sarebbe veramente fastidioso, la soluzione è usare “/*” all’inizio del blocco da commentare e “*/” alla fine di questo. Di seguito l’esempio:
/* Questo è un commento, lungo, molto lungo. Può facilmente contenere la spiegazione di come lavora una funzione, oppure una classe. Quel che volete, insomma. */
Un altro uso dei commenti, inoltre, è il firmare il proprio codice con nome ed il vostro sito web
Non Essere Hard-Coded!
Nel codice del nostro gioco, spesso, ci sono state delle aree “hard-coded”. In poche parole, ho scritto dei valori in un modo tale da risultare fastidiosi da modificare in caso di necessità. Molto spesso può capitare: abbiamo scritto qualcosa e più avanti scopriamo che non ci piace più. Per andare a cambiarla magari facciamo qualche casino o ci mettiamo molto tempo.
Prendiamo il file Enemy.as, dove possiamo trovare un esempio (piuttosto semplice ad esser sinceri) di ciò che ho detto:
public function moveDownABit():void
{
y = y + 3;
}Innanzitutto parliamo del nome della funzione “moveDownABit” (letterlamente “muovi in giù di un pochino”): diamine è un nome terribile! Un metodo del genere è sicuramente molto limitato, anche perché eliminiamo (sia in modo logico con il nome che a livello di codice con questo) qualsiasi possibilità alternativa di movimento.
Per esempio, se volessimo muovere il nostro nemico verso l’alto anziché verso il basso? Dovremmo riscrivere un’altra funzione. Molto spesso però questa non è la pratica più corretta, anche perché il codice non potrebbe essere così semplice e corto!
Inoltre, ho scritto il numero 3 direttamente nel codice. Questa è un’altra cosa veramente da evitare. Il motivo può essere dei più svariati: magari vogliamo aggiungere un power-up per far andare il nemico più veloce. E cosa dovremmo fare, una nuova funzione per gestire il movimento più veloce? Non ha senso!
Ecco quindi come possiamo ottimizzare il nostro metodo:
public function moveABit():void
{
var xSpeed:Number = 0; //pixels moved to the right per tick
var ySpeed:Number = 3; //pixels moved downwards per tick
x = x + xSpeed;
y = y + ySpeed;
}Con un metodo del genere alterare il movimento dei nostri nemici è facilissimo e veloce. Basta cambiare la variabile xSpeed (velocità sull’asse x) ed ySpeed (velocità sull’asse y) e verrà ricalcolato il movimento da effettuare.
Ma il miglioramento non finisce qui! Dalla funzione portiamo questo ragionamento a livello di classe. Le cose verranno modificate e la classe Enemy apparirà così:
package
{
import flash.display.MovieClip;
public class Enemy extends MovieClip
{
public var xSpeed:Number; //pixels moved to the right per tick
public var ySpeed:Number; //pixels moved downwards per tick
public function Enemy( startX:Number, startY:Number )
{
x = startX;
y = startY;
xSpeed = 0;
ySpeed = 3;
}
public function moveABit():void
{
x = x + xSpeed;
y = y + ySpeed;
}
}
}Con questo codice il movimento e la velocità sono determinati nel costruttore e i parametri di velocità possono essere letti dappertutto nella classe. Sarà ancora più facile quindi creare delle eventuali nuove funzioni che fanno uso di queste variabili.
Volendo, per esempio, potreste creare una funzione che, prendendo in input la posizione del nostro avatar, permette di muovere il nostro nemico verso la nostra direzione. Questa funzione provate a farla da soli, non è molto difficile
Un altra modifica divertente inoltre potrebbe essere la generazione casuale dei valori di velocità: in questo modo i nemici andrebbero a velocità differenti tra di loro e quindi potremmo aggiungere al nostro gioco un nuovo fattore di sfida.
Cambiamo quindi
xSpeed = 0; ySpeed = 3;
in
xSpeed = Math.random(); ySpeed = Math.random();
Altro piccolo esercizio: con queste istruzioni i nemici tenderanno ad andare verso l’angolo in basso a destra dello schermo. Perchè? Riesci a capire da cosa dipende e come quindi “aggiustare” le cose?
Inoltre, dovete notare che abbiamo cambiato il nome del metodo moveDownABit in moveABit, dato che adesso non contempliamo più solo il movimento in basso
Di conseguenza dobbiamo modificare il richiamo al metodo anche nella classe AvoiderGame (file AvoiderGame.as).
Da
for each ( var enemy:Enemy in army )
{
enemy.moveDownABit();a
for each ( var enemy:Enemy in army )
{
enemy.moveABit();Poi è sicuramente possibile trovare altre parti di codice “hard-coded”. Provate a cercarle da voi, magari per fare altro esercizio di ottimizzazione!
Si Muore Solo una Volta
Che titolo poeticoso. Comunque, veniamo a noi: nella classe AvoiderGame.as c’è un altro bug che si deve assolutamente risolvere. Vediamo il codice:
for each ( var enemy:Enemy in army )
{
enemy.moveABit();
if ( avatar.hitTestObject( enemy ) )
{
gameTimer.stop();
dispatchEvent( new AvatarEvent( AvatarEvent.DEAD ) );
}
}Apparentemente va tutto bene. Inoltre, testando il nostro gioco, funziona senza problemi. Però c’è qualcosa che può capitare durante il gioco: il nostro avatar entra in contatto contemporaneamente con due o più nemici. Si lo so, è raro, ma può capitare.
Giustamente vi chiederete: embè?
Provate ad immaginare cosa succederebbe se al posto di una singola vita ne avessimo, non so, 3. Di conseguenza il nostro codice verrebbe adattato diversamente, ipotizzando il togliere una vita in caso di contatto e poi controllare se le vite sono pari a zero.
Toccheremmo quindi due nemici nello stesso tick, contemporaneamente, e quale sarebbe il risultato? Anzichè togliere una vita, il gioco ne leverebbe due, una per ogni nemico. Dobbiamo assolutamente risolvere questo gravoso problema.
La soluzione al problema è la seguente: controllare se il giocatore (nel tick corrente) viene in contatto con uno o più nemici. A fine ciclo, in caso affermativo, l’evento viene sparato. Altrimenti tutto rimane com’è.
E a livello di codice? Basta una variabile in più
Useremo infatti una variabile di controllo di tipo booleano: ovvero una variabile che può assumere solo due valori: vero o falso. Nulla più, nulla meno.
var avatarHasBeenHit:Boolean = false;
for each ( var enemy:Enemy in army )
{
enemy.moveABit();
if ( avatar.hitTestObject( enemy ) )
{
gameTimer.stop();
avatarHasBeenHit = true;
}
}
if ( avatarHasBeenHit )
{
dispatchEvent( new AvatarEvent( AvatarEvent.DEAD ) );
}avatarHasBeenHit è la nostra variabile che, ovviamente, viene impostata all’inizio come false. Viene quindi eseguito il ciclo foreach per gestire ogni singolo nemico nell’armata. Ed ecco che, nel caso di contatto, la variabile viene impostata su true.
Dopo la fine del ciclo, quindi, viene eseguito il controllo sulla variabile stessa: siamo stati colpiti oppure no? Se si allora verrà eseguito il dispatch dell’evento e non perderemo il doppio delle vite
Perfect-Pixel Hit Collision
Dato che abbiamo iniziato questo nostro articolo con una feature molto richiesta lo termineremo con un’altra caratteristica altrettanto blasonata. L’immagine di seguito mostra una collisione, secondo gli standard del flash.
Commentando la linea di codice in cui si “spara” l’evento AvatarEvent, facendo in modo di bloccare il gioco come se fosse una pausa, potete constatare quel che dico.
Perché questa è considerata una collisione? Se guardate bene, in realtà, i due oggetti non si toccano minimamente, anzi c’è anche un po’ di spazio tra di loro! Il sistema, infatti, non considera la zona “disegnata”, bensì la “bounding box” dell’oggetto.
E che diamine è la bounding box dell’oggetto?
Per definizione, la bounding box è il più piccolo rettangolo che può contenere quell’oggetto. Guardate l’immagine di seguito per vederne un esempio:
Al contrario delle zone disegnate, le due bounding box vengono tranquillamente in contatto! Può essere scoraggiante da dire, ma questo è stato sempre uno dei peggiori ostacoli allo sviluppo dei giochi in flash, tanto che c’è gente che c’ha fatto articoli sull’argomento (Stephen Calender per esempio).
Oltre questi articoli però, c’è anche gente che ha risolto il problema (con tanta matematica, of course). Una di queste persone è Troy Gilbert, programmatore che ha messo a disposizione una classe interamente dedicata alla collisione per ogni singolo pixel.
Il codice originale è rintracciabile sul suo sito, nella pagina dedicata all’argomento. Potete, alternativamente, riprendere il testo della classe da qui sotto:
package
{
import flash.display.BitmapData;
import flash.display.BitmapDataChannel;
import flash.display.BlendMode;
import flash.display.DisplayObject;
import flash.display.DisplayObjectContainer;
import flash.geom.Matrix;
import flash.geom.Point;
import flash.geom.Rectangle;
public class PixelPerfectCollisionDetection
{
/** Get the collision rectangle between two display objects. **/
public static function getCollisionRect(target1:DisplayObject, target2:DisplayObject, commonParent:DisplayObjectContainer, pixelPrecise:Boolean = false, tolerance:Number = 0):Rectangle
{
// get bounding boxes in common parent's coordinate space
var rect1:Rectangle = target1.getBounds(commonParent);
var rect2:Rectangle = target2.getBounds(commonParent);
// find the intersection of the two bounding boxes
var intersectionRect:Rectangle = rect1.intersection(rect2);
if (intersectionRect.size.length> 0)
{
if (pixelPrecise)
{
// size of rect needs to integer size for bitmap data
intersectionRect.width = Math.ceil(intersectionRect.width);
intersectionRect.height = Math.ceil(intersectionRect.height);
// get the alpha maps for the display objects
var alpha1:BitmapData = getAlphaMap(target1, intersectionRect, BitmapDataChannel.RED, commonParent);
var alpha2:BitmapData = getAlphaMap(target2, intersectionRect, BitmapDataChannel.GREEN, commonParent);
// combine the alpha maps
alpha1.draw(alpha2, null, null, BlendMode.LIGHTEN);
// calculate the search color
var searchColor:uint;
if (tolerance <= 0)
{
searchColor = 0x010100;
}
else
{
if (tolerance> 1) tolerance = 1;
var byte:int = Math.round(tolerance * 255);
searchColor = (byte <<16) | (byte <<8) | 0;
}
// find color
var collisionRect:Rectangle = alpha1.getColorBoundsRect(searchColor, searchColor);
collisionRect.x += intersectionRect.x;
collisionRect.y += intersectionRect.y;
return collisionRect;
}
else
{
return intersectionRect;
}
}
else
{
// no intersection
return null;
}
}
/** Gets the alpha map of the display object and places it in the specified channel. **/
private static function getAlphaMap(target:DisplayObject, rect:Rectangle, channel:uint, commonParent:DisplayObjectContainer):BitmapData
{
// calculate the transform for the display object relative to the common parent
var parentXformInvert:Matrix = commonParent.transform.concatenatedMatrix.clone();
parentXformInvert.invert();
var targetXform:Matrix = target.transform.concatenatedMatrix.clone();
targetXform.concat(parentXformInvert);
// translate the target into the rect's space
targetXform.translate(-rect.x, -rect.y);
// draw the target and extract its alpha channel into a color channel
var bitmapData:BitmapData = new BitmapData(rect.width, rect.height, true, 0);
bitmapData.draw(target, targetXform);
var alphaChannel:BitmapData = new BitmapData(rect.width, rect.height, false, 0);
alphaChannel.copyChannel(bitmapData, bitmapData.rect, new Point(0, 0), BitmapDataChannel.ALPHA, channel);
return alphaChannel;
}
/** Get the center of the collision's bounding box. **/
public static function getCollisionPoint(target1:DisplayObject, target2:DisplayObject, commonParent:DisplayObjectContainer, pixelPrecise:Boolean = false, tolerance:Number = 0):Point
{
var collisionRect:Rectangle = getCollisionRect(target1, target2, commonParent, pixelPrecise, tolerance);
if (collisionRect != null && collisionRect.size.length> 0)
{
var x:Number = (collisionRect.left + collisionRect.right) / 2;
var y:Number = (collisionRect.top + collisionRect.bottom) / 2;
return new Point(x, y);
}
return null;
}
/** Are the two display objects colliding (overlapping)? **/
public static function isColliding(target1:DisplayObject, target2:DisplayObject, commonParent:DisplayObjectContainer, pixelPrecise:Boolean = false, tolerance:Number = 0):Boolean
{
var collisionRect:Rectangle = getCollisionRect(target1, target2, commonParent, pixelPrecise, tolerance);
if (collisionRect != null && collisionRect.size.length> 0) return true;
else return false;
}
}
}Scommetto che la reazione che avete avuto è stata mista tra stupore e bestemmia. Stupore per la genialità e bestemmia per il fattore “e ora come cavolo la uso”? Non vi preoccupate, sono qui per questo.
Salvate il testo della classe in un nuovo file Actionscript, e chiamatelo “PixelPerfectCollisionDetection.as”. Ovviamente sarà contenuto nella stessa cartella delle altre classi. Successivamente tornate alla classe AvoiderGame.as.
Cambiate il codice
if ( avatar.hitTestObject( enemy ) )in
if ( PixelPerfectCollisionDetection.isColliding( avatar, enemy, this, true ) )Ma analizziamo un po’ questa istruzione:
- PixelPerfectCollisionDetection: il nome della classe;
- isColliding: è la funzione contenuta della classe che si occupa di verificare la collisione. Nello specifico, è una funzione statica. Ciò vuol dire che può essere richiamata dalla classe direttamente, senza creare un istanza della classe come avevamo fatto, per esempio, con i nemici e l’avatar;
- avatar, enemy: sono i due oggetti dei quali vogliamo verificare la collisione;
- this: il terzo parametro che passiamo è un oggetto “genitore” in comune che i due oggetti hanno. Dato che nel nostro caso abbiamo eseguito il metodo addChild per entrambi gli oggetti dalla classe corrente, allora abbiamo usato la parola chiave this;
- true: quest’ultimo parametro da specificare indica il controllo per pixel. Di conseguenza, passando come valore true il controllo verrà eseguito con la massima precisione.
Salvate tutto e provate il vostro gioco. Dopo tutti questi piccoli miglioramenti avrà di sicuro qualcosa in più rispetto a prima, e soprattutto avrete qualcosa in più voi
Allego come al solito il materiale della lezione, scaricabile da Qui.
Al prossimo appuntamento!



