Mappe da file

Fino ad ora abbiamo visto come realizzare semplici livelli di gioco basati sui tiles e con una grafica molto semplice.

Riprendendo l'argomento del tile mapping di cui si e' gia' parlato, implementeremo in questo articolo un pratico sistema di gestione delle suddette mappe da file.

Esistono svariati modi di gestire la cosa, tipo file binari, xml o semplici file di testo, io ho optato per la semplicita' del file di testo oltre che per la sua facilita' di gestione a livello di codice.

Una struttura semplice ed ispirata a quanto e' stato fatto in una delle prime implementazioni del gioco SuperTux , svariati parametri di gioco separati l'uno dall'altro ed un paio di layer in formato CSV per rappresentare la grafica di livello ed i suoi contenuti.

La struttura del livello e' nel file di testo e' identica a quanto segue:
Maze 1
128
192
255
10
10
0,0,0,2,3,3,3,3,2,1,
2,0,0,0,0,0,0,0,4,1,
2,3,3,2,0,2,2,0,4,1,
1,1,2,2,0,2,2,0,2,2,
1,1,4,0,0,0,0,0,0,4,
2,3,2,0,2,2,0,2,0,4,
4,11,2,0,4,4,0,4,2,2,
4,0,0,0,2,2,0,2,2,0,
2,3,3,2,0,0,0,0,6,0,
1,1,1,2,3,3,3,3,3,2

0,0,0,1,1,1,1,1,1,1,
1,0,0,0,0,0,0,0,1,1,
1,1,1,1,0,1,1,0,1,1,
1,1,1,1,0,1,1,0,1,1,
1,1,1,0,0,0,0,0,0,1,
1,1,1,0,1,1,0,1,0,1,
1,0,1,0,1,1,0,1,1,1,
1,0,0,0,1,1,0,1,1,0,
1,1,1,1,0,0,0,0,1,0,
1,1,1,1,1,1,1,1,1,1
Andando per ordine dall'alto verso il basso, i valori corrispondo rispettivamente a:
Nome del livello
Valore di colore R dello sfondo
Valore di colore G dello sfondo
Valore di colore B dello sfondo
Larghezza del livello
Altezza del livello
Mappa dei tiles
Mappa degli oggetti

Una struttura semplice e banale, di facile espansione nel caso vi sia necessita', oltretutto facile da gestire a livello di codice con semplici istruzioni messe nella giusta sequenza.

Tutte le informazioni presenti in questo file, non verranno lette ed usate dal programma in tempo reale, ma bensi' salvate in un'apposita struttura apposita.
La struttura atta a contenere il nostro livello e relativi parametri avra' queste sembianze:
struct _Level
{
char name[20];
int map[ROW][COL];
int wall[ROW][COL];
int bkgd_red;
int bkgd_green;
int bkgd_blue;
int width;
int height;
} Level;
Proprio come la struttura del nostro file, anche se i valori qui non sono scritti nello stesso ordine del file, troviamo una variabile atta a contenere ogni singola informazione di esso.
Verra' spiegato piu' nei dettagli successivamente, l'utilizzo del tutto, quindi non preoccupatevi ora di comprenderne a pieno il tutto.

Ora che abbiamo una struttura adatta a contenere i dati di livello, realizziamo una funzione che legga i valori necessari dal file e li allochi in questa struttura.
int level_load(struct _Level* plevel, int level)
{
int i,j;
FILE * fi;
char str[80];
char filename[1024];
char * line;

// File
snprintf(filename, 1024, "maze%dcsv.dat", level);

fi = fopen(filename, "r");
if (fi == NULL)
{
perror(filename);
return -1;
}

// Titolo livello
fgets(str, 20, fi);
strcpy(plevel->name, str);
plevel->name[strlen(plevel->name)-1] = '\0';

// Colori di sfondo
fgets(str, 10, fi);
plevel->bkgd_red = atoi(str);
fgets(str, 10, fi);
plevel->bkgd_green= atoi(str);
fgets(str, 10, fi);
plevel->bkgd_blue = atoi(str);

// Larghezza
fgets(str, 10, fi);
plevel->width = atoi(str);

// Altezza
fgets(str, 10, fi);
plevel->height = atoi(str);

//Layer 1: Tiles
for(i=0; iwidth; i++ && !feof (fi))
for(j=0; jheight; j++)
fscanf (fi, "%02d%*c", &Level.map[i][j]);

//Layer 2: Oggetti
for(i=0; iwidth; i++ && !feof (fi))
for(j=0; jheight; j++)
fscanf (fi, "%02d%*c", &Level.wall[i][j]);

fclose(fi);
return 0;
}
Come avrete sicuramente notato, la funzione accetta due argomenti, un puntatore alla struttura da utilizzare ed il numero di livello, questo per non limitare l'utilizzo di una sola struttura e rendere la funzione il piu' versatile possibile in caso di necessita' di gestione di piu' livelli contemporaneamente.
Necessita' quali tenere a memoria i cambiamenti di un livello appena giocato, in memoria senza dover sovrascrivere irrimediabilmente i valori dentro il file ad esempio.

La funzione qui sopra lavora in maniera al quanto banale, avvalendosi della funzione fgets(), legge una riga alla volta e salva il contenuto della riga in una variabile apposita, di grandezza che si speri sufficiente per la maggior parte dei casi.

L'ordine in cui vengono richiamata la funzione fgets e relative istruzioni collegate, rispecchia l'ordine esatto dei dati nel file, per cui se mai si dovesse aggiungere o togliere qualche parametro dal file, si abbia cura di aggiornare anche la funzione per non avere disguidi.

I valori testuali vengono copiati in maniera sicura nelle apposite variabili tramite la funzione strcpy(), mentre quelli numerici vengono convertiti tramite la funzione atoi() che li converte da stringa nel loro equivalente numerico.

Per quanto riguarda le matrici, identiche nel formato a quanto gia' visto in un precedente articolo, la loro lettura e' affidata alla funzione fscanf(), in questo caso istruita a dovere per leggere solo valori numerici ed ignorare tutte le virgole presenti.
La loro lettura avviene in un ciclo ben definito dai valori di larghezza e altezza gia' letti in precedenza nella funzione, si abbia anche qui la cura necessaria di specificare i valori esatti onde evitare spiacevoli risultati.

Come avrete sicuramente notato, vi sono due mappe distinte separate da una riga vuota, che verra' comodamente ignorata da fscanf() e ci permettera' di avere un file decisamente piu' pulito e comprensibile ad occhio nudo.
Eventuali altri layer di mappa, potranno essere aggiunti a piacere nel file, separandoli se si voglia con un "a capo).

Per questo articolo, a differenza di quanto abbia fatto nei precenti, ho optato per la gestione separata della grafica e degli oggetti nel campo di gioco, basato la mappa su una concezione a strati, ovvero layers.

E' pratica comune in molti giochi 2D, suddividere la mappa su piu' strati, quasi sempre per motivi di gestione della grafica in maniera piu' semplice.
Giochi come Super Mario Bros sfruttano mappe single layer, giochi 2D come quelli per SNES invece sfruttano piu' layers, per gli sfondi, gli oggetti e via dicendo.

Io personalmente ho optato per la gestione su due layer per abitudine, ai tempi che usavo Game Maker, era pratica comune disegnare i vari tiles del livello nel campo di gioco in uno strato neutro fine alla grafica in se, su un altro strato invece gestire gli oggetti di gioco e gli eventuali ostacoli sotto forma di oggetti invisibili.

L'utilizzo di mappe, i cui valori sono solo numerici, ci permette di avere una possibilita' di scelta dei tiles ed oggetti quasi infinita.
Ad ogni numero si potra' far corrispondere un tile grafico oppure un oggetto specifico, oltre tutto facilitandoci il disegno in maniera molto veloce e pratica come quanto segue:

void drawTile(SDL_Surface *tiles, int tile, SDL_Rect *dest)
{
SDL_Rect pick;

pick.x=(tile % 5)*TILE_SIZE;
pick.y=(tile / 5)*TILE_SIZE;
pick.w=TILE_SIZE;
pick.h=TILE_SIZE;

SDL_BlitSurface(tiles,&pick,screen, dest);
}
Con la funzione drawTile(), ci bastera' semplicemente specificare la superficie su cui i vari tiles sono salvati ed il numero specifico di quello che vogliamo usare, e la posizione in cui verra' disegnato sullo schermo.
Il tutto in maniera quasi anagolo a come si faceva per i testi e a come gia' alludevo in alcuni articoli.

La vera comodita' in ogni caso, l'avremmo automatizzando il tutto inviando questo valore specifico di tile tramite quello impostato nella matrice map[][], passandolo attraverso una funzione apposita per il disegno del campo di gioco.

void drawbackground()
{
int i, j;

// Destinazione del tile
SDL_Rect dest;

for (i = 0; i < COL; i++)
{
for (j = 0; j < ROW; j++)
{
dest.x = i * TILE_SIZE;
dest.y = j * TILE_SIZE;
dest.w = dest.h = TILE_SIZE;

drawTile(tiles, Level.map[j][i], &dest);
}
}
}
Qui leggiamo la matrice valore dopo valore, e ne passiamo ogni singolo numero alla funzione precedentemente discussa, per disegnare con semplicita' e in maniera del tutto automatica il nostro campo di gioco.

L'unica cosa che rimane da fare e' aggiornare la nostra funzione tileCollision() con l'apposita nuova matrice relativa agli oggetti, per gestire collisioni con eventuali muri, oggetti collezionabili e nemici di sorta.

Provate voi stessi ad aggiornare ed implementare qualche aggiunta in questo semplice esempio, siete ad un passo dalla realizzazione di un vostro gioco completo!

Trovate l'esempio completo dell'articolo al repository del blog.

Nessun commento :

Posta un commento

Related Posts Plugin for WordPress, Blogger...