Crea il tuo Tetris in 30 minuti

Lo scorso mese ho introdotto l'utilizzo delle matrici in un semplice gioco di campo minato, in questo articolo estenderemo quel concetto realizzando un completo clone del tetris.

Realizzare un clone del tetris non e' poi cosi' difficile, una volta capito il concetto lo si realizza molto in fretta, bastano solo 30 minuti per realizzarne un clone completamente funzionante!

Cominciamo come di consueto dichiarando gli header necessari al progetto e dichiarando alcune costanti.
#include <stdio.h>
#include <stdlib.h>
#include <SDL/SDL.h>
#include <time.h>

#define SCREEN_WIDTH 330
#define SCREEN_HEIGHT 320

#define MAP_W 12
#define MAP_H 20

#define TILESIZE 16
#define PIECE_SIZE 4

Dovrebbero essere abbastanza chiare e familiari, per quanto riguarda l'ultimo gruppetto, le costanti TILESIZE e PIECE_SIZE definiscono rispettivamente la grandezza in pixel di ogni blocco del tetramino (i vari pezzi del gioco) e la dimensione della matrice contenente il tetramino.

Dichiariamo anche i colori dei vari tetramini, che saranno gli stessi che si potranno vedere per i singoli pezzi anche nei giochi ufficiali di Tetris.
#define GREY 0xC0C0C0
#define GREEN 0x008000
#define BLUE 0x0000FF
#define RED 0xFF0000
#define ORANGE 0xFF9D2E
#define WHITE 0xFFFFFF
#define PURPLE 0xFF00FF
#define YELLOW 0xFFFF00
#define SKYBLUE 0x8080FF
Ora che abbiamo definito le nostre costanti, definiamo anche una manciata di variabili globali per il gioco come segue.
int t, tl = 0, frequency = 1000 / 100, temp, t2;
int repeat, i, quit=0;
SDL_Event event;
SDL_Surface *screen;
SDL_Rect rect;
SDLKey keyPressed;
Anche qui, dovrebbe essere tutto familiare dagli articoli precedenti.
Non ci rimane ora che definire una matrice per il campo di gioco in cui si muoveranno i tetramini e le classiche strutture di cui non possiamo fare a meno
int playground[MAP_H][MAP_W];

struct _Game
{
int score, level, lines, status;
} Game;

struct Piece {
int size[PIECE_SIZE][PIECE_SIZE]; // Tetramino Attuale
int next[PIECE_SIZE][PIECE_SIZE]; // Tetramino successivo
int x, y;
} sPiece;
Troviamo una struttura Piece che sara' la base di riferimento per ogni tetramino, al suo interno verranno salvate le informazioni riguardo la posizione non che la sua composizione sotto forma matriciale, sia per quello attualmente in uso al giocatore che per quello successivo.

Cominciamo ora a stendere il cuore vero e proprio del nostro gioco, partiamo con una funzione che crei i nostri tetramini sotto forma di matrice.
void createTetramini()
{
int tetramino, i,j;

// 0 1 2 3 4 5 6
// X Questi
// X XX X XX XX XX XX sono
// X XX XXX XX XX X X i tipi
// X X X di blocchi
sPiece.x=MAP_W/2-2;
sPiece.y=0;

tetramino=rand()%7;

//Puliamo la matrice del tetramino successivo
for(i=0; i<4; i++)
for(j=0; j<4; j++)
{
sPiece.next[i][j]=0;
}

switch (tetramino)
{
case 0: //Bar
{
sPiece.next[1][0]=1;
sPiece.next[1][1]=1;
sPiece.next[1][2]=1;
sPiece.next[1][3]=1;
break;
}
case 1: //Box
{
sPiece.next[1][1]=2;
sPiece.next[1][2]=2;
sPiece.next[2][1]=2;
sPiece.next[2][2]=2;
break;
}
case 2: //T
{
sPiece.next[1][1]=3;
sPiece.next[0][2]=3;
sPiece.next[1][2]=3;
sPiece.next[2][2]=3;
break;
}
case 3://Left Leaner
{
sPiece.next[0][1]=4;
sPiece.next[1][1]=4;
sPiece.next[1][2]=4;
sPiece.next[2][2]=4;
break;
}
case 4://Right Leaner
{
sPiece.next[2][1]=5;
sPiece.next[1][1]=5;
sPiece.next[1][2]=5;
sPiece.next[0][2]=5;
break;
}
case 5://L inverted
{
sPiece.next[1][1]=6;
sPiece.next[2][1]=6;
sPiece.next[2][2]=6;
sPiece.next[2][3]=6;
break;
}
case 6://L regular
{
sPiece.next[2][1]=7;
sPiece.next[1][1]=7;
sPiece.next[1][2]=7;
sPiece.next[1][3]=7;
break;
}
}
}
Come si puo' ben vedere, viene assegnata una posizione al tetramino, viene pulita la matrice next della struttura sPiece e viene scelto un numero casuale tra 0 e 6, in base al numero viene composto il tetramino nell'apposita matrice della struttura.

Successivamente, il tetramino successivo appena impostato viene copiato nella struttura di quello attuale, successivamente viene realizzato il tetramino successivo.
Un po' ridondante all'inizio come procedimento, ma doveroso per poter realizzare un semplice sistema di previsione del tetramino successivo nel gioco, come si puo' vedere in figura.
void assignTetramini()
{
int i, j;

for(i=0; i<4; i++)
for(j=0; j<4; j++)
sPiece.size[i][j]=0;

for(i=0; i<4; i++)
for(j=0; j<4; j++)
sPiece.size[i][j] = sPiece.next[i][j];

//Crea un tetramino
createTetramini();
}
Qui semplicemente puliamo la matrice del tetramino attuale, per evitare che vi siano residui di tetramini precedenti e come gia' detto impostiamo il successivo come attuale e ne prepariamo un altro per dopo.

Terminiamo la gestione dei tetramini, realizzando una funzione che ne permetta la rotazione, questa e' un'alternativa meno dispendiosa alla realizzazione di modelli gia' rotati dei vari tetramini.
void RotateBlock()
{
int i, j, x, y, temp[4][4];
// Copiamo il tetramino in una matrice temporanea ruotandolo
for(i=0; i<4; i++)
for(j=0; j<4; j++)
temp[3-j][i]=sPiece.size[i][j];

// Controlliamo eventuali collisioni del tetramino ruotato
int px, py; // pezzo
int mX, mY; // mappa

for(mX = sPiece.x, px = 0; mX < (sPiece.x + PIECE_SIZE); mX++, px++)
for(mY = sPiece.y, py = 0; mY < (sPiece.y + PIECE_SIZE); mY++, py++)
if(temp[py][px] != 0)
{
// Il tetramino esce dai bordi della mappa
if((mX < 0) || (mX >= MAP_W) || (mY >= MAP_H))
return; //terminiamo la funzione

// Il tetramino sovrasta uno degli altri tetramini nella mappa
if(playground[mY][mX] != 0)
return;
}

//Nessuna collisione! Copiamo il tetramino girato nell'apposita matrice
for(i=0; i<4; i++)
for(j=0; j<4; j++)
sPiece.size[ i ][j]=temp[ i ][j];

return;
}
Non lasciatevi intimidire da questa funzione, e' piu' semplice di quanto non sembri, si basti guardare l'immagine esemplificativa sottostante per comprenderne il funzionamento.

La rotazione del tetramino avviene leggendo la matrice di esso una riga per volta, come indica il verso della freccia, questa riga viene trasposta come una colonna nella matrice temporanea.
Si nota bene che nel ciclo piu' interno, oltre che ad avere indici invertiti per le due matrici, l'indice delle righe di temp va a scalare un valore statico di 3, questo solo per poter cominciare la lettura dall'ultima riga della matrice ed avere cosi' un effetto di rotazione del contenuto.

Per rileggere correttamente la matrice trasposta, dovremmo avere l'accortezza di rimettere gli indici nel giusto ordine prima della lettura, nell'esempio di destra ho improvvisato un print della matrice in console.
Nel resto della funzione, controlliamo che nell'area del tetramino attuale, ogni pezzo non vuoto della matrice temporanea non esca dai bordi della mappa o sovrasti altri tetramini gia' posizionati, in caso la sovrapposizione non vi sia, copiamo definitivamente il contenuto temporaneo nella matrice principale.
Notate bene che le coordinate del tetramino non corrispondono a quelle grafiche, pertanto ci e' possibile sfruttare questo fatto per controlli molto semplici direttamente in matrice.
La resa grafica verra' data in un secondo tempo, moltiplicando la posizione di ogni tetramino per la dimensione di un tile.

Successivamente, passiamo a gestire un altro aspetto fondamentale del gioco, la distruzione delle linee complete nella mappa.
Il tutto viene effettuato in due tempi, da prima viene richiamata la funzione multiLineDelete() che controllera' l'effettiva completezza di almeno una linea.
Per ogni linea completa, si chiamera' la funzione oneLineDelete(int row) che cancellera' la riga completa alla posizione specificata in argomento.
void oneLineDelete(int row)
{
int x,y, counter=0;

for(x=row; x>0; x--)
for(y=0; y<MAP_W; y++)
playground[x][y]=playground[x-1][y];

//Aggiorniamo lo status
Game.lines++;
if(!(Game.lines % 10)) // Livello+1 ogni 10 linee
Game.level++;
}

void multiLineDelete()
{
int x, y, counter;
for(x = sPiece.y; x < MAP_H-1; x++)
{
y = 1;
while (y < MAP_W)
{
if(playground[x][y] == 0) break;
y++;
counter++;
}

if (y == MAP_W)
oneLineDelete(x);
}

Game.score=Game.score+(5*counter);
}
Nella seconda funzione, partiamo direttamente dalla riga in cui si trova il nostro tetramino fino a fondo mappa, in quanto sara' impossibile che sopra di esso ci siano righe complete non ancora distrutte, evitiamo quindi calcoli superflui e ci concentriamo solo sullo stretto necessario.

Impostiamo la variabile y a 1, in quanto essa leggera' le colonne tra la prima e l'ultima della mappa, dato che esse sono da considerarsi pareti del livello che in seguito verranno colorate dall'apposita funzione grafica.

Nel ciclo while interno controlliamo tutte le celle della matrice della mappa, da sinistra fino al muro di destra, fino a che i valori delle celle siano diversi da zero.
Uno zero nel campo di gioco vuol dire spazio vuoto, quindi che la riga presenta un'interruzione, non vi sono righe complete da distruggere allora terminiamo il ciclo.
Sommiamo anche una variabile counter per dare un punteggio al giocatore in base al numero di linee distrutte.

Un volta terminato il ciclo, controlliamo che la variabile y sia di valore pari alla larghezza del campo di gioco, cio' significhera' che il ciclo precedente ha rilevato una linea consecutiva nella mappa, di conseguenza chiameremo la funzione oneLineDelete(row) che cancellera' la riga interessata.
Una volta finito, il ciclo for principale ripetera' tutte queste azioni fino a fondo mappa per cancellare ogni riga presente.
Se non vi e' altro da fare, si aggiorna il punteggio del giocatore.

Nella funzione oneLineDelete(row), semplicemente sposteremo verso il basso tutte le righe sovrastanti di una riga in piu', proseguendo fino alla riga piu' alta della mappa di gioco.

Ora che abbiamo affrontato buona parte del gameplay di tetris, dedichiamoci alla gestione del movimento dei tetramini, stendiamo quindi le funzioni checkMovement(int nx, int ny) e move(int x, int y), rispettivamente responsabili del controllo e del movimento del tetramino nel campo di gioco.
int checkmovement(int nx, int ny)
{
int px, py; // pezzo
int mX, mY; // mappa

int newx=nx;
int newy=ny;

for(mX = sPiece.x, px = 0; mX < (sPiece.x + PIECE_SIZE); mX++, px++)
for(mY = sPiece.y, py = 0; mY < (sPiece.y + PIECE_SIZE); mY++, py++)
if(sPiece.size[py][px] != 0)
{
// Il pezzo esce fuori dai bordi della mappa
if((mX < 0) || (mX >= MAP_W) || (mY >= MAP_H))
return 0;

// Il pezzo tocca un'altro tetramino nella mappa
if(playground[mY+newy][mX+newx] != 0)
return 0;
}
return 1;
}

void move()
{
int i, j;

if(checkmovement(0, 1))
{
sPiece.y++;
}
else
{
for(i=0; i<4; i++)
for(j=0; j<4; j++)
if(sPiece.size[i][j] != 0)
playground[sPiece.y+i][sPiece.x+j] = sPiece.size[i][j];

multiLineDelete();
if(sPiece.y<=0) // Se il pezzo tocca la parte alta della mappa
{
quit=1; //game over!
}
else
assignTetramini(); // Creiamo un nuovo tetramino
}
}
La prima funzione e' molto simile a quella gia' vista per la roteazione del tetramino, le differenze sono minime, non ci suffermo su piu' di tanto.

La seconda funzione invece, e' responsabile dell'inesorabile caduta verso il basso del tetramino.
Controllera' che alla posizione y+1 sia possibile il movimento, nel caso sia possibile, il tetramino verra' spostato di una riga in giu' come e' giusto che sia, altrimenti verra' semplicemente copiato nella mappa di gioco.
Verra' depositato in quanto non piu' in grado di muoversi, successivamente si controllera' se vi siano linee da distruggere e se abbia toccato il bordo superiore del campo di gioco, cio' consistera' in un game over.
Nel caso tutto sia nella norma, si crea il nuovo tetramino e si ricomincia la solita routine.

Buona parte del gioco, ora e' completa, non rimane altro che disegnare i vari tetramini e il campo di gioco sullo schermo e gestire le interazioni con il giocatore.
A fare cio', ci penseranno le funzioni drawPlayfield(), drawBlock() e drawNextBlock() che richiameremo tutte insieme dalla funzione draw() per comodita'.
void drawPlayfield()
{
int i, j;

for(i = 0; i < MAP_H; i++) // Disegna le colonne
{
for(j = 0; j < MAP_W; j++) // Disegna le righe
{
if(playground[i][j]==8)
DrawRect(j * TILESIZE, i * TILESIZE, TILESIZE, TILESIZE, WHITE); //Parete del campo di gioco
if(playground[i][j]==1)
DrawRect(j * TILESIZE, i * TILESIZE, TILESIZE, TILESIZE, SKYBLUE);
if(playground[i][j]==2)
DrawRect(j * TILESIZE, i * TILESIZE, TILESIZE, TILESIZE, YELLOW);
if(playground[i][j]==3)
DrawRect(j * TILESIZE, i * TILESIZE, TILESIZE, TILESIZE, PURPLE);
if(playground[i][j]==4)
DrawRect(j * TILESIZE, i * TILESIZE, TILESIZE, TILESIZE, GREEN);
if(playground[i][j]==5)
DrawRect(j * TILESIZE, i * TILESIZE, TILESIZE, TILESIZE, RED);
if(playground[i][j]==6)
DrawRect(j * TILESIZE, i * TILESIZE, TILESIZE, TILESIZE, BLUE);
if(playground[i][j]==7)
DrawRect(j * TILESIZE, i * TILESIZE, TILESIZE, TILESIZE, ORANGE);
}
}
}

void drawBlock()
{
int xmy, ymx;

for(xmy=0; xmy<4; xmy++)
{
for(ymx=0; ymx<4; ymx++)
{
if(sPiece.size[xmy][ymx]==1)
DrawRect((sPiece.x+ymx)*TILESIZE, (sPiece.y+xmy)*TILESIZE, TILESIZE, TILESIZE, SKYBLUE);
if(sPiece.size[xmy][ymx]==2)
DrawRect((sPiece.x+ymx)*TILESIZE, (sPiece.y+xmy)*TILESIZE, TILESIZE, TILESIZE, YELLOW);
if(sPiece.size[xmy][ymx]==3)
DrawRect((sPiece.x+ymx)*TILESIZE, (sPiece.y+xmy)*TILESIZE, TILESIZE, TILESIZE, PURPLE);
if(sPiece.size[xmy][ymx]==4)
DrawRect((sPiece.x+ymx)*TILESIZE, (sPiece.y+xmy)*TILESIZE, TILESIZE, TILESIZE, GREEN);
if(sPiece.size[xmy][ymx]==5)
DrawRect((sPiece.x+ymx)*TILESIZE, (sPiece.y+xmy)*TILESIZE, TILESIZE, TILESIZE, RED);
if(sPiece.size[xmy][ymx]==6)
DrawRect((sPiece.x+ymx)*TILESIZE, (sPiece.y+xmy)*TILESIZE, TILESIZE, TILESIZE, BLUE);
if(sPiece.size[xmy][ymx]==7)
DrawRect((sPiece.x+ymx)*TILESIZE, (sPiece.y+xmy)*TILESIZE, TILESIZE, TILESIZE, ORANGE);
}
}
}

void drawNextBlock()
{
int xmy, ymx;
for(xmy=0; xmy<4; xmy++)
{
for(ymx=0; ymx<4; ymx++)
{
if(sPiece.next[xmy][ymx]==1)
DrawRect(SCREEN_WIDTH-120+(ymx*TILESIZE), 45+(xmy*TILESIZE), TILESIZE, TILESIZE, SKYBLUE);
if(sPiece.next[xmy][ymx]==2)
DrawRect(SCREEN_WIDTH-120+(ymx*TILESIZE), 45+(xmy*TILESIZE), TILESIZE, TILESIZE, YELLOW);
if(sPiece.next[xmy][ymx]==3)
DrawRect(SCREEN_WIDTH-120+(ymx*TILESIZE), 45+(xmy*TILESIZE), TILESIZE, TILESIZE, PURPLE);
if(sPiece.next[xmy][ymx]==4)
DrawRect(SCREEN_WIDTH-120+(ymx*TILESIZE), 45+(xmy*TILESIZE), TILESIZE, TILESIZE, GREEN);
if(sPiece.next[xmy][ymx]==5)
DrawRect(SCREEN_WIDTH-120+(ymx*TILESIZE), 45+(xmy*TILESIZE), TILESIZE, TILESIZE, RED);
if(sPiece.next[xmy][ymx]==6)
DrawRect(SCREEN_WIDTH-120+(ymx*TILESIZE), 45+(xmy*TILESIZE), TILESIZE, TILESIZE, BLUE);
if(sPiece.next[xmy][ymx]==7)
DrawRect(SCREEN_WIDTH-120+(ymx*TILESIZE), 45+(xmy*TILESIZE), TILESIZE, TILESIZE, ORANGE);
}
}
}
Niente di complicato come si puo' ben vedere, disegnamo semplicemente le matrici del campo di gioco e dei tetramini, alle loro rispettive posizioni.
Per il campo di gioco le cose sono molto semplici, si deve solo controllare il valore numerico nella matrice e disegnare un blocco di 16x16 pixel sullo schermo per ognuno di essi con un colore specifico.

Per quanto riguardo i tetramini in movimento, partiamo a disegnarli dalle loro coordinate X e Y sommando ogni blocco di essi alla posizione attuale e moltiplicando per la grandezza di un tile, otterremo la posizione esatta in pixel in cui disegnare.
Stessa cosa per il tetramino d'anteprima, anche se la sua posizione rimarra' fissa nella finestra.

Rallegratevi, avete appena coperto il 90% dell'intero gioco, non ci rimane altro ora che aggiungere le classiche funzioni di logica gia' viste in precedenti articoli ed impostare valori iniziali di gioco, come e' bene che sia.
void newGame()
{
int i, j;

for (i = 0; i < MAP_H; i++)
{
for (j = 0; j < MAP_W; j++)
{
if(j==0 || i==MAP_H-1 || j==MAP_W-1)
{
playground[i][j]=8;
}
else
playground[i][j]=0;
}
}

createTetramini();
assignTetramini();
Game.score=0;
Game.lines=0;
}
Non credo ci sia molto da dire al riguardo, viene pulita la mappa di gioco e vengono assegnati dei valori alle celle da considerare pareti, vengono creati i tretramini ed assegnati oltre che azzerare il punteggio e il numero di linee cancellate.

Poca differenza nella funzione di logica che gia' ben conosciamo.
void logicLoop()
{
if (SDL_PollEvent(&event))
{
if (event.type == SDL_QUIT)
quit = 1;

if (event.type == SDL_KEYDOWN)
{
keyPressed = event.key.keysym.sym;
switch (keyPressed)
{
case SDLK_ESCAPE:
quit = 1;
break;

case SDLK_LEFT:
if(checkmovement(-1, 0))
sPiece.x--;
break;

case SDLK_RIGHT:
if(checkmovement(1, 0))
sPiece.x++;
break;

case SDLK_UP:
case SDLK_z:
case SDLK_x:
RotateBlock();
break;

case SDLK_DOWN:
if(checkmovement(0, 1))
sPiece.y++;
break;

case SDLK_c:
case SDLK_SPACE:
while(checkmovement(0, 1))
sPiece.y++;
break;
}
}
}

if(t2 +(600-(Game.level*45)) < SDL_GetTicks())
{
move();
t2 = SDL_GetTicks();
}
}
Alla pressione di un tasto, si controlla che la posizione desiderata sia disponibile ed eventualmente si sposta il tetramino, si controlla anche che sia possibile ruotarli.
Interessante da notare anche la pressione del tasto spazio, che manda il tetramino direttamente al fondo dello schermo, o per meglio dire al punto libero piu' basso disponibile in un solo colpo.

Alla fine della funzione troviamo un controllo sul tempo, in base al livello raggiunto, il tetramino cadra' sempre piu' velocemente aumentando cosi' la difficolta' di gioco come ogni tetris che si rispetti.

Infine modifichiamo il nostro mainloop, realizziamo numeri sempre casuali e ad avviamo la nostra funzione di logica, oltre che di disegno come al solito.
int mainLoop()
{
while(!quit)
{
repeat = fps_sync ();

for (i = 0; i < repeat; i ++)
{
srand(time(NULL));
logicLoop();
}

SDL_FillRect(screen, NULL, SDL_MapRGB(screen->format, 0, 0, 0));
Draw();
SDL_Flip(screen);
}
}
Il nostro gioco e' praticamente terminato, cerchiamo di avere solo l'accortezza di aggiungere le seguenti definizioni e chiamate nella nostra funzione main ed un messaggio compreso di statistiche per la fine del gioco!
 t2 = t;  //last tick since we call this function
newGame();
mainLoop();
printf("Thanks for playing!\nYou made %d lines and %d points!\n", Game.lines, Game.score);
Anche questo gioco come i precedenti breakout e pong puo' essere arricchito, non solo con suoni e frivolezze grafiche come dico sempre, ma anche migliorando il controllo dei tetramini magari in maniera piu' fluida o inserendo caratteristiche dei giochi di Tetris di nuova generazione.
Questa e' una versione semplificata a scopo didattico di RetroTetris, scaricabile dal sito Pix3lWorkshop sotto la sezione Retroplay Series.

Per dovere di cronaca, segnalo che tutto quello che e' stato spiegato in questo articolo non e' del tutto farina del mio sacco, molte nozioni di base sono state apprese, manipolate e migliorate da tutorial scritti da Javier LĆ³pez e Evil_Greven (quest'ultimo diceva un'ora, io dico la meta'!)

Trovate il sorgente dell'articolo, qui.

Nessun commento :

Posta un commento

Related Posts Plugin for WordPress, Blogger...