Implementazione di Tetris compensazione delle linee

Nel mio precedente tutorial su Tetris, ti ho mostrato come gestire il rilevamento delle collisioni in Tetris. Ora diamo un'occhiata all'altro aspetto importante del gioco: la linea libera.

Nota: Sebbene il codice in questo tutorial sia stato scritto usando AS3, dovresti essere in grado di utilizzare le stesse tecniche e concetti in quasi tutti gli ambienti di sviluppo di giochi.


Rilevazione di una linea completata

Rilevare che una linea è stata compilata è in realtà molto semplice; solo guardando l'array di array sarà abbastanza ovvio cosa fare:

La linea piena è quella che non ha zero in esso - quindi possiamo controllare una data riga come questa:

 riga = 4; // controlla la quarta riga isFilled = true; per (var col = 0; col < landed[row].length; col++)  if (landed[row][col] == 0)  isFilled = false;   //if isFilled is still true than row 4 is filled

Con questo, ovviamente, possiamo scorrere ogni riga e capire quali di questi sono riempiti:

 per (var row = 0; col < landed.length; row++)  isFilled = true; for (var col = 0; col < landed[row].length; col++)  if (landed[row][col] == 0)  isFilled = false;   //if isFilled is still true then current row is filled 

Ok, potremmo ottimizzare questo capendo quali sono le linee probabile da compilare, in base a quali file occupa l'ultimo blocco, ma perché preoccuparsi? Effettuare il looping di ogni singolo elemento nella griglia 10x16 non è un'attività che richiede un uso intensivo del processore.

La domanda ora è: come facciamo noi chiaro le linee?


Il metodo ingenuo

A prima vista, questo sembra semplice: basta unire le righe piene (s) dalla matrice e aggiungere nuove righe vuote nella parte superiore.

 per (var row = 0; row < landed.length; row++)  isFilled = true; for (var col = 0; col < landed[row].length; col++)  if (landed[row][col] == 0)  isFilled = false;   //remove the filled line sub-array from the array landed.splice(row, 1); //add a new empty line sub-array to the start of the array landed.unshift([0,0,0,0,0,0,0,0,0,0]); 

Se proviamo questo sull'array sopra (e quindi eseguiamo il rendering di tutto), otteniamo:

... che è quello che ci aspetteremmo, giusto? Ci sono ancora 16 file, ma quello pieno è stato rimosso; la nuova riga vuota ha spinto tutto verso il basso per compensare.

Ecco un esempio più semplice, con side-by-side delle immagini precedenti e successive:

Un altro risultato atteso. E - anche se non lo mostrerò qui - lo stesso codice si occupa anche di situazioni in cui più di una riga viene riempita contemporaneamente (anche se quelle linee non sono adiacenti).

Tuttavia, ci sono casi in cui questo non fa quello che ci si potrebbe aspettare. Guarda questo:

È strano vedere quel blocco blu fluttuare lì, attaccato al nulla. Non è sbagliato, esattamente - la maggior parte delle versioni di Tetris lo fanno, inclusa la classica edizione di Game Boy - quindi potresti lasciarlo.

Tuttavia, ci sono un paio di altri modi popolari per affrontare questo ...


Il metodo Big Clump

E se facessimo cadere quel blocco blu solitario dopo che la linea è stata cancellata?

La grande difficoltà con questo è in realtà capire esattamente cosa stiamo cercando di fare. È più difficile di quanto sembri!

Il mio primo istinto sarebbe quello di far cadere ogni singolo blocco fino a quando non fosse atterrato. Ciò porterebbe a situazioni come questa:

... ma sospetto che non sarebbe divertente, poiché tutte le lacune si riempirebbero rapidamente. (Non esitate a sperimentare con questo, però - potrebbe esserci qualcosa in esso!)

Voglio che quei blocchi arancioni rimangano collegati, ma quel blocco blu deve cadere. Forse potremmo far cadere dei blocchi se non hanno altri blocchi a sinistra oa destra di essi? Ah, ma guarda questa situazione:

Qui, voglio che i blocchi blu cadano nei loro rispettivi "buchi" dopo che la linea è stata cancellata - ma l'insieme medio di blocchi blu ha tutti gli altri blocchi accanto a loro: altri blocchi blu!

("Quindi controlla solo se i blocchi si trovano vicino ai blocchi rossi", potresti pensare, ma ricorda che li ho solo colorati in blu e in rosso per rendere più facile fare riferimento a blocchi diversi, potrebbero essere tutti i colori, e avrebbe potuto essere posato in qualsiasi momento.)

Possiamo identificare una cosa che i blocchi blu nell'immagine a destra - e il blocco blu galleggiante isolato da prima - tutti in comune: sono sopra la linea che è stata cancellata. Quindi, se invece di cercare di far cadere i singoli blocchi, raggruppiamo tutti questi blocchi blu e li facciamo cadere all'unisono?

Potremmo anche riutilizzare lo stesso codice che fa cadere un individuo tetromino. Ecco un promemoria, dal tutorial precedente:

 // imposta tetromino.potentialTopLeft su una riga sotto tetromino.topLeft, quindi: for (var row = 0; row < tetromino.shape.length; row++)  for (var col = 0; col < tetromino.shape[row].length; col++)  if (tetromino.shape[row][col] != 0)  if (row + tetromino.potentialTopLeft.row >= landed.length) // questo blocco si trova sotto il campo di gioco else if (atterrato [row + tetromino.potentialTopLeft.row]! = 0 && landed [col + tetromino.potentialTopLeft.col]! = 0) / / lo spazio è occupato

Ma piuttosto che usare un tetromino oggetto, creeremo un nuovo oggetto di cui forma contiene solo i blocchi blu: chiamiamo questo oggetto gruppo.

Il trasferimento dei blocchi è solo una questione di loop attraverso il atterrato array, trovando ogni elemento diverso da zero, compilando il stesso elemento nel clump.shape array e impostando l'elemento di atterrato array a zero.

Come al solito, questo è più facile da capire con un'immagine:

A sinistra è il clump.shape array, e sulla destra è il atterrato array. Qui, non mi sto prendendo la briga di compilare righe vuote clump.shape per mantenere le cose in ordine, ma puoi farlo senza problemi.

Quindi, il nostro gruppo l'oggetto è simile a questo:

 clump.shape = [[1,0,0,0,0,0,0,0,0,0,0], [1,0,0,1,1,0,0,0,0,0,0], [ 1,0,0,1,1,0,0,0,0,1]]; clump.topLeft = row: 10, col: 0;

... e ora eseguiamo ripetutamente lo stesso codice che usiamo per far cadere un tetromino, fino a quando il clump atterra:

 // imposta clump.potentialTopLeft su una riga sotto clump.topLeft, quindi: for (var row = 0; row < clump.shape.length; row++)  for (var col = 0; col < clump.shape[row].length; col++)  if (clump.shape[row][col] != 0)  if (row + clump.potentialTopLeft.row >= landed.length) // questo blocco sarebbe sotto il campo di gioco else if (atterrato [row + clump.potentialTopLeft.row]! = 0 && landed [col + clump.potentialTopLeft.col]! = 0) / / lo spazio è occupato

Una volta che il gruppo è atterrato, copiamo i singoli elementi nuovamente atterrato array - di nuovo, proprio come quando un tetromino atterra. Tuttavia, piuttosto che eseguire questo ogni mezzo secondo e ri-rendering di ogni cosa tra ogni caduta, suggerisco di eseguirlo più e più volte fino a quando il clump atterra, il più rapidamente possibile, e poi rendering di tutto, in modo che sembri che cali all'istante.

Segui questo se vuoi; ecco il risultato:

È possibile che venga formata un'altra linea qui, senza che il giocatore debba rilasciare un altro blocco - aprendo le possibili strategie del giocatore non disponibili con il metodo Naive - quindi è necessario ricontrollare immediatamente le linee riempite. In questo caso, non ci sono linee piene, quindi il gioco può continuare e puoi generare un altro blocco.

Tutto sembra buono per il metodo Clump, ma sfortunatamente c'è un problema, come mostrato in questo esempio precedente e successivo:


Dopo che la linea piena scompare, entrambi i blocchi blu cadono di due quadrati e poi si fermano.

Qui, il blocco blu nel mezzo è atterrato - e dal momento che è raggruppato insieme al blocco blu sulla destra, anche quello è considerato "atterrato". Il blocco successivo verrà generato, e di nuovo abbiamo un blocco blu che galleggia a mezz'aria.

Il metodo Big Clump non è in realtà un metodo efficace, a causa di questo problema non intuitivo, ma è vero è a metà strada con un buon metodo ...


Il metodo appiccicoso

Guarda ancora questi due esempi:

In entrambi i casi, c'è un modo ovvio per separare i blocchi blu in gruppi separati: due blocchi (ciascuno di un blocco) nel primo e tre blocchi (di tre, quattro e uno) nel secondo.

Se raggruppiamo i blocchi in quel modo, e poi facciamo in modo che ogni gruppo cada in modo indipendente, allora dovremmo ottenere il risultato desiderato! Inoltre, "clump" non sembrerà più una parola.

Ecco cosa intendo:

Iniziamo da questa situazione. Ovviamente, la seconda fila verrà cancellata.

Abbiamo diviso i blocchi sopra la linea deselezionata in tre gruppi distinti. (Ho usato colori diversi per identificare quali blocchi si raggruppano.)

I ciuffi cadono in modo indipendente - notate come il gruppo verde cade di due file, mentre i ciuffi blu e viola cadono dopo essere caduti solo uno. La linea di fondo è ora riempita, quindi anche questa viene cancellata e i tre gruppi cadono.

Come possiamo capire la forma dei ciuffi? Bene, come puoi vedere dall'immagine, in realtà è piuttosto semplice: raggruppiamo tutti i blocchi in forme contigue - cioè, per ogni blocco, lo raggruppiamo con tutti i suoi vicini, e i vicini dei vicini, e così on, fino a quando ogni blocco è in un gruppo.

Piuttosto che spiegare esattamente come fare questo raggruppamento, ti indicherò la pagina di Wikipedia per il riempimento dell'inondazione, che spiega diversi modi per raggiungere questo obiettivo, insieme ai pro e ai contro di ciascuno.

Una volta che hai le forme dei clumps, puoi incollarle in un array:

 ciuffi = []; clumps [0] .shape = [[3], [3]]; clumps [0] .topLeft = row: 11, col: 0; ciuffi [1] .shape = [[0,1,0], [0,1,1], [0,1,1], [1,1,1]]; clumps [1] .topLeft = row: 9, col: 3; ciuffi [2] .shape = [[1,1,1], [1,1,1], [0,1,1]]; clumps [2] .topLeft = row: 10, col: 7;

Quindi, basta fare un iterazione di ciascun gruppo nella matrice che cade, ricordando di controllare le nuove linee riempite dopo che sono atterrate.

Questo è chiamato il metodo Sticky, ed è usato in alcuni giochi, come Tetris Blast. Mi piace; è una svolta decente su Tetris, consentendo nuove strategie. C'è un altro metodo popolare che è un po 'diverso ...


Sfida: il metodo Cascade

Se hai seguito i concetti fino ad ora, penso che valga la pena provare a implementare il metodo Cascade come esercizio.

Fondamentalmente, ciascun blocco ricorda di quale tetromino faceva parte, anche quando un segmento di quel tetromino viene distrutto da una linea libera. I tetromino - o le parti strane e spezzettate di tetromino - cadono come grumi.

Come sempre, le immagini aiutano:

Un T-tetromino cade, completando una linea. Nota come ogni blocco rimane collegato al suo tetromino originale? (Assumeremo qui che nessuna riga è stata cancellata finora).

La linea completata viene cancellata, che divide il Z-tetromino verde in due pezzi separati e taglia pezzi di altri tetromino.

Il T-tetromino (o cosa ne è rimasto) continua a cadere, perché non è bloccato da nessun altro blocco.

Il T-tetromino atterra, completando un'altra linea. Questa linea è cancellata, tagliando pezzi ancora più tetrominoes.

Come potete vedere, il metodo Cascade gioca in modo un po 'diverso rispetto agli altri due metodi principali. Se non sei ancora chiaro su come funziona, vedi se riesci a trovare una copia di Quadra o Tetris 2 (o guarda i video su YouTube), poiché entrambi usano questo metodo.

In bocca al lupo!


Conclusione

Grazie per aver letto questo tutorial! Spero che tu abbia imparato qualcosa (e non solo riguardo a Tetris), e che tu abbia un approccio alla sfida. Se realizzi giochi utilizzando queste tecniche, mi piacerebbe vederli! Si prega di postarli nei commenti qui sotto, o tweet me a @ MichaelJW.

.