Creare un Gioco in Flash – Parte 10: Livelli Multipli
- 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
Fino ad ora abbiamo aggiunto un sacco di elementi degni di un gioco nel vero senso della parola: dagli elementi di gameplay basilari fino ad arrivare ai suoni e ad un sofisticato preloader. Tuttavia, manca un elemento a mio parere (e non solo) fondamentale per un buon gioco: la progressione.
La riuscita del nostro gioco infatti dipende essenzialmente da una dose di fortuna, prontezza dei riflessi e subito dopo qualche secondo abbiamo mostrato al mondo tutto quello che c’era da mostrare. E non solo dal punto di vista del gameplay, ma anche graficamente e.c.c.
Per questo motivo, in questa decima lezione della nostra guida impareremo ad aggiungere dei nuovi livelli di gioco per far divertire il nostro utente ancora di più. Se non avete seguito gli altri tutorial provvedete immediatamente a prendere i file necessari alla fine della lezione precedente. Altrimenti preleviamo i backup e via!
Ehi ma… di che diamine stai parlando? Livelli?
Prima di decidere l’aggiunta dei livelli al nostro gioco, diamo una bella occhiata sulle varie modalità tramite le quali altri sviluppatori di giochi hanno risolto questo problema. Partiamo da Dodge, nel quale inizialmente avremo a che fare con pochi nemici.
Una volta distrutti questi, il livello cambierà e avremo la possibilità di scambiare i punti di gioco per punti salute prima di iniziare il successivo. Ad ogni livello, inoltre, viene incrementato il numero di nemici presenti. Come se non bastasse, vengono introdotti anche nuovi tipi di nemici (immagine di seguito)!
Amorphous+ invece funziona in modo differente. Si può scegliere quale nemico affrontare e quindi il livello viene deciso dall’utente prima ancora di iniziare il gioco.
Click sulle immagini per ingrandirle
Volendo fare altri esempi, invece, possiamo citare Four Second Frenzy, che presenta uno stile molto simile a Wario Ware: tanti mini giochi uno dopo l’altro con obiettivi completamente differenti.
Un esempio di mini gioco di “Four Second Frenzy”
Una volta creati tutti questi piccoli micro games, vengono incorporati in una struttura generale che li controlla e che gestisce le vite del giocatore.
Ricapitolando, come potete vedere ci sono svariati modi per identificare un “livello”. Un cambio di livello può significare esclusivamente un cambio di grafica, oppure solo di gameplay o addirittura entrambi, per non parlare del caso in cui cambi davvero tutto.
Bisogna decidere cosa fare.
E allora, cosa faremo?
Basandoci su quello che abbiamo detto nelle parti precedenti del tutorial, la soluzione ideale per la nostra estensione è creare una classe per ogni livello. In questo modo, potremmo avere più classi (che estendono AvoiderGame ovviamente) dal nome AvoiderGameLevelOne ed AvoiderGameLevelTwo.
Successivamente quindi potremmo creare un evento del tipo NavigationEvent.NEXT_LEVEL ed eseguire quindi il cambiamento di livello. Ovviamente la classe predisposta al cambio e al controllo di tutto sarà la classe documento: il livello corretto verrà “sparato” tramite un istruzione come playScreen = AvoiderGameLevelTwo() (o qualsiasi livello serva), passando le informazioni come il punteggio ed il tempo.
Dovrebbe funzionare. Potrebbe.
Ma abbiamo un limite. Abbiamo costruito 10 livelli, arriviamo al decimo e poi? Il gioco finisce? Se avesse una storia, magari come un gioco d’avventura, le cose sarebbero fattibili. Ma il nostro gioco è più simile ad un arcade game, e noi vorremmo che continuasse.
Consideriamo quindi il design di un pilastro del mondo videoludico quale Tetris. Alla fine di ogni livello, i blocchi vanno più veloci e, in alcune versioni, l’immagine di sfondo (o il colore) cambiano. Creando un nuovo livello con lo stratagemma di “una classe per livello”, perderemmo le informazioni precedenti.
Per questo motivo l’approccio che utilizzeremo sarà il seguente:
- finiremo il livello attuale
- passeremo i dati importanti ad un altra struttura appositamente creata
- riavvieremo il gioco con le informazioni prese dalla struttura
ed in questo modo non dovremmo perdere niente.
Potremmo basare la nostra struttura proprio su questo concept, però troviamo davvero limitante l’approccio “classe per livello”. Come diamine possiamo fare? Quando pensiamo alla classe AvoiderGame possiamo pensare ad essa come un insieme di regole ben definite, per ogni componente in essa presente.
Potremmo creare dei nuovi livelli semplicemente cambiando questi valori e queste regole in base al livello attuale:
if ( currentLevel == 1 )
{
gameScore.addToValue( 10 );
}
else if ( currentLevel == 2 )
{
gameScore.addToValue( 15 );
}
else if ( currentLevel == 3 )
{
gameScore.addToValue( 22 );
}Ma finiremmo per scrivere del codice “specifico” per il livello in giro per la classe, creando una gran confusione in più punti e rendendo un eventuale manutenzione praticamente impossibile. La migliore soluzione è quindi memorizzare tutte le informazioni specifiche in un “posto” separato dalle regole generiche, facendo in modo di richiedere questi valori da parte della classe AvoiderGame per il livello necessario ogni volta.
Creiamo nuove classi.
La Classe LevelData
Iniziamo la nostra lunga modifica effettuando un cambiamento innanzitutto visuale: l’immagine di sfondo del livello.
Apriamo il simbolo PlayScreen nella nostra libreria (ricordatevi che è collegata con la classe AvoiderGame). Attualmente, il background è disegnato direttamente sul PlayScreen. Volendo essere capaci di modificarlo dal codice, dovremmo creare un simbolo apposito. Selezionate tutto il background (facendo attenzione a non selezionare l’orologio ed il punteggio):
Per rendere la selezione un unico simbolo, quindi, cliccate sul menù “Modifica > Converti in Simbolo”, chiamatelo BackgroundContainer ed esportatelo per Actionscript con lo stesso nome. Ricordate una cosa importantissima: stiamo usando un preloader per il nostro gioco, quindi non esportate niente nel primo fotogramma. Quest’opzione deve rimanere sempre disattivata.
Entrate in modalità di modifica del PlayScreen e date all’istanza del BackgroundContainer il nome di “backgroundContainer”. Molto probabilmente si sarà automaticamente posizionato davanti a tutti gli altri contenuti del simbolo: cliccate su di esso con il pulsante destro del mouse, quindi selezionate “Disponi > Sposta Dietro”.
Quindi, riassumendo, il nostro PlayScreen contiene un clip di classe BackgroundContainer, che a sua volta contiene un altro movie clip che è il nostro “BlueBackground”. Ora, duplichiamo il nostro BlueBackground nella libreria ed entriamo in modalità di modifica nel nuovo simbolo duplicato.
Modificatelo come volete, io l’ho chiamato RedBackground e questo è il suo aspetto:
Salve tutto e testate, verificando che con il background blu funzioni tutto correttamente e senza intoppi. Ora dobbiamo passare al codice: creeremo la classe che contiene le informazioni specifiche dei livelli. Creiamo una nuova classe, che chiameremo LevelData.as. Serve davvero che vi dica dove dovete salvarla?
package
{
public class LevelData
{
public function LevelData()
{
}
}
}Notate una cosa importante: non stiamo estendendo nulla, e non ce n’è bisogno. In questa classe infatti caricheremo solo dei dati relativi al livello che non richiedono import specifici. Creiamo una variabile pubblica chiamata “backgroundImage”:
package
{
public class LevelData
{
public var backgroundImage:String;
public function LevelData()
{
}
}
}Come facciamo a “collegare” a livello logico il numero del livello con il background? Ecco il codice che ci serve:
package
{
public class LevelData
{
public var backgroundImage:String;
public function LevelData( levelNumber:Number )
{
if ( levelNumber == 1 )
{
backgroundImage = "blue";
}
else if ( levelNumber == 2 )
{
backgroundImage = "red";
}
}
}
}Ogni volta che creeremo una nuova istanza della classe LevelData, quindi, passeremo come parametro del costruttore un numero indicante il livello in cui stiamo giocando, impostando la variabile “backgroundImage” con il nome del colore rispettivo (parlando dello sfondo del livello
public var currentLevelData:LevelData;ed ecco l’istanza dei dati del livello.
Nella funzione del costruttore istanziamo l’oggetto in questo modo:
public function AvoiderGame()
{
currentLevelData = new LevelData( 1 );Volendo verificare anche con un semplice trace, attualmente la variabile currentLevelData.backgroundImage avrà il valore “blue”. Quello che ora faremo sarà aggiungere, in base a questa stringa, il giusto simbolo di background.
public function AvoiderGame()
{
currentLevelData = new LevelData( 1 );
if ( currentLevelData.backgroundImage == "blue" )
{
backgroundContainer.addChild( new BlueBackground() );
}
else if ( currentLevelData.backgroundImage == "red" )
{
backgroundContainer.addChild( new RedBackground() );
}Seguite bene i passi effettuati e non vi perdete. Ovviamente con la pratica sarà tutto più chiaro, per cui non vi spaventate se un concetto un po’ più avanzato come questo vi da problemi al primo colpo.
Quello che abbiamo fatto nella sezione di codice poco sopra è semplice: in base alla stringa pervenuta (“blue” o “red”) abbiamo addChild-ato di conseguenza il giusto background: con “blue” avremo un nuovo BlueBackground, e con “red” un nuovo RedBackground.
In questo passaggio l’uso del BackgroundContainter è spiegato: usando un oggetto contenitore, infatti, non dobbiamo portarlo dietro tutte le volte che creiamo (re-inizializziamo è più corretto
) un nuovo background. Non usando BackgroundContainer avremmo visto il background davanti a tutto e quindi riportarlo indietro ogni volta. Non è molto pratico.
Ad ogni modo, se salvate tutto e provate, il vostro gioco avrà lo sfondo di colore blu. Infatti il codice attuale è
currentLevelData = new LevelData( 1 );Cambiamolo ora, in:
currentLevelData = new LevelData( 2 );Salvate tutto e provate!
Wooooooooooooooooooooow.
Level Up!
Abbiamo appena visto come mostrare uno specifico livello di gioco. Ma come collegare il tutto logicamente e quindi andare al livello successivo durante la partita? Per prima cosa, dobbiamo decidere cosa il nostro giocatore dovrà fare per arrivare al livello successivo.
La scelta più ovvia è stata quella più semplice: una volta raggiunti un tot di punti, provvedere a mandare il giocatore al livello successivo. Cambiate il valore nell’istruzione “new LevelData”, passando 1 invece di 2. In questo modo faremo iniziare il giocatore dal primo livello.
Ora, ritrovate la funzione onTick() e aggiungeteci questo codice alla fine:
if ( avatarHasBeenHit )
{
bgmSoundChannel.stop();
dispatchEvent( new AvatarEvent( AvatarEvent.DEAD ) );
}
if ( gameScore.currentValue >= 150 )
{
currentLevelData = new LevelData( 2 );
if ( currentLevelData.backgroundImage == "blue" )
{
backgroundContainer.addChild( new BlueBackground() );
}
else if ( currentLevelData.backgroundImage == "red" )
{
backgroundContainer.addChild( new RedBackground() );
}
}
}Anzi, facciamo le cose ancora meglio: creiamo una nuova funzione con il codice come segue:
if ( avatarHasBeenHit )
{
bgmSoundChannel.stop();
dispatchEvent( new AvatarEvent( AvatarEvent.DEAD ) );
}
if ( gameScore.currentValue >= 150 )
{
currentLevelData = new LevelData( 2 );
setBackgroundImage();
}
}
public function setBackgroundImage():void
{
if ( currentLevelData.backgroundImage == "blue" )
{
backgroundContainer.addChild( new BlueBackground() );
}
else if ( currentLevelData.backgroundImage == "red" )
{
backgroundContainer.addChild( new RedBackground() );
}
}In questo modo non ripeteremo lo stesso codice del costruttore. Fate, ovviamente, la stessa cosa in quest’ultimo: cambiate il codice nel costruttore sostituendo le linee con l’unica necessaria a richiamare la funzione.
Salvate tutto e testate. Dopo aver raggiunto il vostro obiettivo, tuttavia, il gioco rallenterà in una maniera mostruosa. La spiegazione è semplice: una volta superati i 150 punti il controllo continuerà a ridare true sempre. Per questo motivo dall’istante in cui si raggiungeranno i 150 punti verrà ricreato il background e riposizionato (calcolando 25ms del timer) circa 40 volte al secondo.
Come risolvere questo problema? La mia soluzione consiste nel creare una variabile chiamata pointsToReachNextLevel, nella classe LevelData, ed impostarla in questo modo:
package
{
public class LevelData
{
public var backgroundImage:String;
public var pointsToReachNextLevel:Number;
public function LevelData( levelNumber:Number )
{
if ( levelNumber == 1 )
{
backgroundImage = "blue";
pointsToReachNextLevel = 150;
}
else if ( levelNumber == 2 )
{
backgroundImage = "red";
pointsToReachNextLevel = 9999999;
}
}
}
}Useremo un numero decisamente alto ed esagerato temporaneamente, dato che non abbiamo un livello 3
if ( gameScore.currentValue >= currentLevelData.pointsToReachNextLevel )
{
currentLevelData = new LevelData( 2 );
setBackgroundImage();
}Testate tutto e non dovreste avere problemi. Il nostro sistema funziona! Dopo la modifica alla grafica è giunto il momento, quindi, di effettuare qualche cambiamento a livello di gameplay. Cosa ne dite di modificare la frequenza dei nemici quando arriviamo al secondo livello?
Tutto quello che dobbiamo fare è aggiungere una nuova variabile alla classe LevelData ed impostarla:
package
{
public class LevelData
{
public var backgroundImage:String;
public var pointsToReachNextLevel:Number;
public var enemySpawnRate:Number;
public function LevelData( levelNumber:Number )
{
if ( levelNumber == 1 )
{
backgroundImage = "blue";
pointsToReachNextLevel = 150;
enemySpawnRate = 0.05;
}
else if ( levelNumber == 2 )
{
backgroundImage = "red";
pointsToReachNextLevel = 9999999;
enemySpawnRate = 0.1;
}
}
}
}… quindi usare questa variabile nella classe AvoiderGame:
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();
}Et voilà!
Livelli Infiniti
Qualche centinaio di parole fa avevo sottolineato il fatto che usare una classe per livello può rivelarsi uno svantaggio: la mia affermazione non era stata buttata così e per ora possiamo contare due livelli. Proviamo adesso ad aggiungerne un altro paio.
Nella classe LevelData modifichiamo il codice in questo modo:
public function LevelData( levelNumber:Number )
{
if ( levelNumber == 1 )
{
backgroundImage = "blue";
pointsToReachNextLevel = 150;
enemySpawnRate = 0.05;
}
else if ( levelNumber == 2 )
{
backgroundImage = "red";
pointsToReachNextLevel = 350;
enemySpawnRate = 0.1;
}
else if ( levelNumber == 3 )
{
backgroundImage = "blue";
pointsToReachNextLevel = 600;
enemySpawnRate = 0.13;
}
else if ( levelNumber == 4 )
{
backgroundImage = "red";
pointsToReachNextLevel = 9999999;
enemySpawnRate = 0.15;
}
}Dato che disponiamo di sue soli sfondi, ci limiteremo ad alternarli. Ovviamente potete farne più di due ed usarli come volete, fin quando modificate in maniera appropriata la funzione setBackgroundImage().
Tornando a noi, abbiamo un problema. Date un occhiata a questo codice:
if ( gameScore.currentValue >= currentLevelData.pointsToReachNextLevel )
{
currentLevelData = new LevelData( 2 );
setBackgroundImage();
}Man mano che andiamo avanti, nonostante si debba passare dal livello 2 al 3 oppure dal 3 al 4, il gioco continua a caricare il secondo livello ( l’istruzione new LevelData( 2 ); ) !!! Dobbiamo modificare un po’ la nostra classe, ed aggiungere una variabile che tenga traccia del numero del livello attuale.
In questo modo, potremmo richiamare un nuovo livello con un istruzione del tipo
new LevelData( currentLevelNumber + 1 );
Ovviamente metteremo anche questa variabile nella classe LevelData. Modificate il codice in questo modo:
public var backgroundImage:String;
public var pointsToReachNextLevel:Number;
public var enemySpawnRate:Number;
public var levelNum:Number;
public function LevelData( levelNumber:Number )
{
levelNum = levelNumber;Ho chiamato la variabile “levelNumber”, di tipo numerico. Nel file AvoiderGame.as, invece, usiamo il riferimento per questa variabile come segue:
if ( gameScore.currentValue >= currentLevelData.pointsToReachNextLevel )
{
currentLevelData = new LevelData( currentLevelData.levelNum + 1 );
setBackgroundImage();
}Testate il vostro gioco e tutto funzionerà
…
Pensavate davvero di finirla qua? Sapete quanto siamo pignoli da queste parti
Per cui, perché non implementare a tutti gli effetti un numero infinito di livelli? Le cose si fanno poco più complicate: dobbiamo definire una regola generica per impostare questi valori. Se avete fatto le successioni algebriche (in realtà pure capire semplicemente la moltiplicazione aiuta molto) capirete di cosa sto parlando, altrimenti continuate a leggere e la pratica vi chiarirà le idee.
Date un occhiata al codice qui di seguito:
else if ( levelNumber == 4 )
{
backgroundImage = "red";
pointsToReachNextLevel = 770;
enemySpawnRate = 0.15;
}
else
{
backgroundImage = "blue";
pointsToReachNextLevel = levelNumber * 200;
enemySpawnRate = 0.6 - ( 2 / levelNumber );
}Mi raccomando, massima attenzione al cambiare il valore del punteggio necessario perché lasciarlo a nove milioni e passa non è una buona idea
La regola generale sarà, quindi:
n * 200 = (punti da raggiungere per passare al livello n+1)
dove n = livello attuale
esempio pratico: per passare dal livello 7 al livello 8 avremo bisogno di 7*200 punti = 1400
Spero sia tutto chiaro
Ora rimane solo una cosa da risolvere: giocando al nostro capolavoro infatti noteremo presto che, dopo il livello 4, il background torna blu e tale rimane all’infinito. Non è esattamente quello che vogliamo, dato che dovremmo alternare il rosso e il blu!
Per risolvere quest’ultimo dilemma introduco la funzione modulo. Questo metodo restituisce il resto della divisione che gli viene sottoposta. Per esempio, cercando il modulo della divisione 2 / 2 otterremo come risultato 0. Per 1 / 2 otterremo 1 e via discorrendo.
Ora mi chiederete: tutto questo è molto bello ma cosa serve, in realtà?
La funzione di modulo, in flash, è rappresentata dall’operatore %. Quello che faremo noi sarà semplice: partendo dal numero del livello attuale, calcoleremo il modulo della divisione tra il livello attuale e 2. Due perché i nostri background sono due.
Il risultato sarà quindi 0 oppure 1, questi sono i possibili resti. Se otterremo 0 useremo il background rosso, nel caso di 1 invece quello blu. Ecco alcuni esempi:
-
Livello 6 = 6 % 2 = 0 –> Background Rosso
-
Livello 7 = 7 % 2 = 1 –> Background Blu
-
Livello 8 = 8 % 2 = 0 –> Background Rosso
Ora capite perché la regola è idealmente applicabile all’infinito?
Traduciamo questo ragionamento in codice:
else
{
if ( levelNumber % 2 == 1 )
{
backgroundImage = "blue";
}
else
{
backgroundImage = "red";
}
pointsToReachNextLevel = levelNumber * 200;
enemySpawnRate = 0.5 - ( 2 / levelNumber );
}e l’ultimo tocco è dato.
Sfide
Sulla scia della versione originale di questi articoli riporto qualche piccola sfida proposta per aumentare la familiarità con il codice. Per esempio:
- che ne dite di mostrare su schermo il livello attuale? ne dovreste essere capaci, lavorando con le variabili che avete (ne basterebbe una
) e una bella textbox dinamica! - magari potreste implementare delle schermate tra un livello e l’altro: con gameTimer.Stop() potreste fermare temporaneamente il gioco e quindi usare un simbolo da cliccare (un pulsante per esempio) per passare al livello successivo e far ripartire il caro gameTimer.
Qualcosa di più difficile?
- Create dei livelli bonus: magari al posto dei nemici appaiono delle monete e prendendole aumenta il punteggio!
Come potete ben capire, le idee possibili (anche per un gioco semplice come questo) sono migliaia. E tutte realizzabili. Scatenate la vostra fantasia e proponete! Inoltre, notate una cosa: stavolta non abbiamo usato neanche un import, un extend e addirittura neanche un Event Listener!
Prima di concludere vi lascio i file della lezione, che trovate Qui, annunciandovi che nella prossima lezione faremo qualcosa di molto avanzato: impareremo a gestire i salvataggi, a crearli e ricaricarli
Fino ad allora, buona serata a tutti!


