Retrogaming: Nintendo NES, mini Pac-Man

videogiochi

Il videogame più "iconico" della storia degli Arcade, Pac-Man, è stato distribuito negli anni '80 del secolo scorso su più di 300.000 macchine da sala giochi per poi essere sviluppato e reso disponibile su quasi tutte le piattaforme di home computer e console del periodo. Ancora oggi è molto giocato sui siti online da numerosi amanti del retrogaming.

Mini PAC-MAN

Di seguito, vedremo come sviluppare una versione "ridotta" di PAC-MAN. Il videogioco originale sviluppato dalla Namco nel lontano 1980 prevede di condurre all'interno di un labirinto un sorta di creatura perennemente affamata costituita da una grande bocca che si apre e si chiude ritmicamente per ingoiare delle piccole palline o biscottini :-D. Il percorso della creatura è ostacolato da quattro fantasmini che costantemente cercano a loro volta di fermarla. Ogni volta che la creatura viene a contatto con un fantasmino, quest'ultima perde una delle tre vite a disposizione. Nel 2010,  per il trentesimo anniversario dalla data del rilascio, Google ha reso disponibile un doodle giocabile (Figura 1) come tributo a questo emblematico videogioco del passato.

Doodle di Pac-Man su Google

Figura 1: Doodle di Pac-Man su Google

La versione che andremo a presentare, pur mantenendo le logiche di base, è comunque molto semplificata rispetto all'originale, in pratica può essere considerata come una base di partenza per futuri sviluppi. Lo scopo è quello di realizzare una versione giocabile ma comunque contenuta in termini di complessità e di scrittura del codice in maniera da evidenziare solo gli aspetti principali legati alla programmazione in linguaggio C. Nel nostro caso, il videogioco è ambientato nel labirinto di un vecchio castello con mura possenti (Figura 2) e popolato da fantasmi di antiche e malvagie creature ( 😆 ) che ne presidiano tutti i corridoi. Pac-Man, per terminare il livello, deve riuscire a mangiare tutte le palline/biscottini presenti lungo i corridoi. I fantasmi non sanno dov'è Pac-Man ma sono molto veloci a spostarsi, e se lo dovessero incontrare ... game over!

Mini Pac-Man

Figura 2: Mini Pac-Man

La struttura dati

Prima di affrontare il discorso della programmazione, descriviamo brevemente il modello dati utilizzato per implementare il videogame. Per prima cosa, dobbiamo pensare a come rappresentare il labirinto in cui si muovono Pac-Man e i fantasmi, e in seconda battuta come disegnare e animare a video gli attori principali. Partiamo dal presupposto che dobbiamo disegnare tutto su uno schermo delle dimensioni di 256x240 pixel e ricordando che i grafici contenuti nella nostra pattern table (Figura 3) sono delle tile di 8x8 pixel, abbiamo uno schermo 32x30 tile o meglio 16x15 se assembliamo le tile a gruppi di 4 al fine di avere oggetti di maggiori dimensioni e quindi più definiti.

Pattern Table

Figura 3: Pattern Table

In questo caso la struttura dati più semplice che ci permette di rappresentare labirinto e attori è una matrice (16x15):

unsigned char map[]= {
 1,1,1,1,1,9,9,9,9,9,9,1,1,1,1,1,
 1,2,1,2,1,9,9,9,9,9,9,1,2,1,2,1,
 1,2,2,2,1,1,1,1,1,1,1,1,2,2,2,1,
 1,2,2,2,1,2,2,2,2,2,2,1,2,2,2,1,
 1,2,1,2,2,2,1,2,2,1,2,2,2,1,2,1,
 1,2,1,2,2,2,1,2,2,1,2,2,2,1,2,1,
 1,2,2,2,1,2,2,2,2,2,2,1,2,2,2,1,
 1,2,2,2,2,2,4,5,6,7,2,2,2,2,2,1,
 1,2,2,2,2,2,1,1,1,1,2,2,2,2,2,1,
 1,2,1,2,2,2,1,2,2,1,2,2,2,1,2,1,
 1,2,1,2,2,2,2,2,2,2,2,2,2,1,2,1,
 1,2,2,2,2,2,1,2,2,1,2,2,2,2,2,1,
 1,2,2,2,1,2,2,2,2,2,2,1,2,2,2,1,
 1,3,2,2,1,2,1,2,2,1,2,1,2,2,2,1,
 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
};

Ogni elemento della matrice è contraddistinto da un codice numerico:

  • 1: muro
  • 2: pallina o biscottino
  • 3: Pac-Man
  • 4: fantasma A
  • 5: fantasma B
  • 6: fantasma C
  • 7: fantasma D
  • 9: area dedicata al conteggio dei punti

In pratica, la matrice map[] è la nostra mappa di gioco composta da tutti gli elementi con cui si andrà ad interagire.

L'utilizzo dei buffer-video

Per quanto antico e superato, il meccanismo di output grafico utilizzato dalla piattaforma Nintendo NES è molto interessante. In pratica, il programmatore ha a disposizione due buffer video per scrivere sulla nametable attiva ovvero sullo schermo. Il primo buffer, il "background layer", ovvero lo sfondo, è disegnabile tramite delle funzioni presenti nella libreria neslib.lib. Queste funzioni permettono di individuare la posizione dove scrivere vram_addr() e di scrivere intere stringhe di testo con vram_write() o di trasferire una singola tile presente nella pattern table tramite vram_put(), di seguito ad esempio le funzioni utilizzate per disegnare la mappa di gioco:

....
#define EMPTY		0
#define WALL		1
#define COOKIE		2
#define PAC_MAN		3
#define GHOST_A		4
#define GHOST_B		5
#define GHOST_C		6
#define GHOST_D		7
#define MSG_AREA	9
....
const unsigned char game_over[]="GAME OVER!";
const unsigned char victory[]="VICTORY!";
....
// meta tile = 4 tile
void draw_meta_tile_x0y0() {
 vram_adr(NTADR_A(x0,y0));
 vram_put(t01);
 vram_adr(NTADR_A(x0+1,y0));
 vram_put(t02);
 vram_adr(NTADR_A(x0,y0+1));
 vram_put(t03);
 vram_adr(NTADR_A(x0+1,y0+1));
 vram_put(t04);
}

void reset_score() {
 score001 = 0;
 score010 = 0;
 score100 = 0;
}

void draw_game_title() {
 vram_adr(NTADR_A(10,1));	 
 vram_write(title,12);
 if (status == GAME_OVER) {
  vram_adr(NTADR_A(11,20));
  vram_write(game_over,11); 
 } else
 if (status == GAME_VICTORY) {
  vram_adr(NTADR_A(12,20));
  vram_write(victory,8); 
 }
}

void draw_map(){
 draw_game_title();
 reset_score();
 for (y=0;y<15;++y) {
  y0=y<<1;	  
  for (x=0;x<16;++x) {
   x0 = x<<1;	
   i = (y<<4)+x;  	  
   if (map[i] == WALL) {   //wall
    t01 = 0xC0;t02 = 0xC1; 
    t03 = 0xD0;t04 = 0xD1; 
    draw_meta_tile_x0y0();
   } else
   if (map[i] == COOKIE) { // cookie
    t01 = 0x01;t02 = 0x02;
    t03 = 0x11;t04 = 0x12;
    draw_meta_tile_x0y0();
    ++cookie;
   } else { 
    if (map[i] == PAC_MAN) { // pac-man
     pac_xpos = x;
     pac_ypos = y;
    } else 
    if (map[i] == GHOST_A) { // ghost 
     gsa_xpos = x;
     gsa_ypos = y;
    } else
    if (map[i] == GHOST_B) { // ghost b
     gsb_xpos = x;
     gsb_ypos = y;
    } else
    if (map[i] == GHOST_C) { // ghost c
     gsc_xpos = x;
     gsc_ypos = y;
    } else
    if (map[i] == GHOST_D) { // ghost d
     gsd_xpos = x;
     gsd_ypos = y;
    }
    if (map[i] != MSG_AREA) { // meta sprite start tile //
     t01 = 0x05;t02 = 0x06;
     t03 = 0x15;t04 = 0x16;
     draw_meta_tile_x0y0();
    }
   }
  }
 }
}

...

Le variabili globali t01,t02... , contengono di volta in volta l'indirizzo (in esadecimale) della tile presente nella pattern table che bisogna stampare a video, mentre la macro NTADR_A() converte le coordinate della mappa nell'indirizzo di dove andare a scrivere sulla nametable attiva (A). Attenzione, però, la scrittura della nametable può avvenire solo quando la PPU è spenta, quindi il processo completo di scrittura del "background layer" è del tipo:

 
// clear vram and blank the screen
// call the function when rendering 
// is turned off (..ppu_off() ..)
void clear_vram() {
 clear_vram_buffer();
 vram_adr(NAMETABLE_A);
 vram_fill(0,1024); // blank the screen	
}
 ....
 ppu_off();    // ppu off 
 clear_vram();
 draw_map();
 ppu_on_all(); // ppu on
 .... 

Per quanto riguarda gli oggetti in movimento, occorre utilizzare un altro buffer, quello che gestisce gli sprite. Ritornando alla funzione draw_map() vista sopra, avrete sicuramente notato che quando all'interno della mappa vengono intercettati i personaggi che saranno oggetto di spostamenti (manuali o automatici) PAC_MAN, GHOST_A, GHOST_B, ecc., vengono lette solo le coordinate di partenza ma nulla viene stampato a video. Per gestire questi oggetti si usano delle funzioni apposite contenute sempre nella libreria neslib.lib, che nella fattispecie sono:

  • oam_meta_spr(); che stampa a video un meta sprite
  • oam_clear(); che svuota il buffer degli sprite

Per completezza d'informazione, diciamo che la funzione per disegnare uno sprite composto da una sola tile è oam_spr(), nel nostro caso visto che abbiamo deciso di raggruppare le tile in gruppi da 4 utilizzeremo la funzione oam_meta_spr() che permette di comporre uno sprite utilizzando appunto 4 tile. Di seguito, è riportata la funzione che disegna sul buffer degli sprite, Pac-Man:

.....
void draw_pacman() {
 switch(pac_man_dir) {
  case UP:
   if (timer01)
    oam_meta_spr(x0<<4,y0<<4,pac_up);  
   else
    oam_meta_spr(x0<<4,y0<<4,pac_base);  
  break;
  case DOWN:
   if (timer01)
    oam_meta_spr(x0<<4,y0<<4,pac_down);  
   else
    oam_meta_spr(x0<<4,y0<<4,pac_base);  
  break;
  case LEFT:
   if (timer01)
    oam_meta_spr(x0<<4,y0<<4,pac_left);  
   else
    oam_meta_spr(x0<<4,y0<<4,pac_base);  
  break;
  case RIGHT:
   if (timer01)
    oam_meta_spr(x0<<4,y0<<4,pac_right);  
   else
    oam_meta_spr(x0<<4,y0<<4,pac_base);  
  break;
 }
}
....

Da notare che l'effetto apertura/chiusura della bocca è creato disegnando alternativamente due set di tile (Figura 4) e che timer01 è una variabile globale temporizzata che viene posta a 0 o a 1 ciclicamente.

Proiezione alternata di due meta sprite per ottenere l'effetto apertura e chiusura della bocca di Pac-Man

Figura 4: Proiezione alternata di due meta sprite per ottenere l'effetto apertura e chiusura della bocca di Pac-Man

I dati relativi al meta sprite sono contenuti in una variabile particolare e ben definita, del tipo visibile di seguito:

  const unsigned char pac_base[]={
   0,      0,      PAC_TILE_BASE +0,  PAC_PALETTE, 
   8,      0,      PAC_TILE_BASE +1,  PAC_PALETTE, 
   0,      8,      PAC_TILE_BASE +16, PAC_PALETTE, 
   8,      8,      PAC_TILE_BASE +17, PAC_PALETTE, 
 128};

In pratica, si tratta di un array contenente le coordinate x,y dell'angolo in alto a sinistra delle 4 tile, gli indirizzi esadecimali della pattern table e la palette di colori da utilizzare per ogni tile. L'array è poi terminato con il codice 128, in Figura 5, la rappresentazione grafica di un meta sprite.

[...]

ATTENZIONE: quello che hai appena letto è solo un estratto, l'Articolo Tecnico completo è composto da ben 2667 parole ed è riservato agli ABBONATI. Con l'Abbonamento avrai anche accesso a tutti gli altri Articoli Tecnici che potrai leggere in formato PDF per un anno. ABBONATI ORA, è semplice e sicuro.

Scarica subito una copia gratis

Scrivi un commento

Seguici anche sul tuo Social Network preferito!

Send this to a friend