
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.

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!

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.

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.

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.
