Articoli Manifesto Tools Links Canali Libri Contatti ?
Giochi / Programmazione / SDL

Seconda introduzione alla programmazione con SDL

Abstract
La guida continua l'introduzione alla libreria SDL (Simple DirectMedia Layer) utilizzando dei concetti già mostrati nella precedente guida [1] e spiegandone di nuovi.
In particolare verrà realizzato un programma che, utilizzando la stampa di pixel su video, permetta la visualizzazioni di tre gradienti.
Data di stesura: 08/04/2004
Data di pubblicazione: 22/03/2004
Ultima modifica: 04/04/2006
di Davide Coppola Discuti sul forum   Stampa

Premesse

Questa guida richiede una conoscenza discreta del C e di un pò di logica booleana, siccome non è suo compito spiegarvi queste due cose, nel caso non possedesse queste conoscenze, acquistate un buon libro sul C (consiglio "Programmare in C Guida completa" di Peter Aitken, Bradley L. Jones edizioni Apogeo [3]) e cercate di colmare le vostre lacune sulla logica booleana con qualche documento in rete (se ne trovano in quantità).
Oltre alla conoscenza del C, presumo che sia già stata letta la mia guida "Introduzione alla programmazione con SDL", pertanto i concetti già spiegati non verranno trattati con la stessa profondità della prima guida.

Si fa presente che questa guida ed il programma in essa spiegato sono stati realizzati su piattaforma linux (slackware 9.1 con gcc 3.3.3 e SDL 1.2.6), ciò non dovrebbe comportare problemi nell'esecuzione del programma sotto altri sistemi, ma di questo non posso dare certezze.

Si segnala che l'articolo è stato realizzato durante lo sviluppo di Mars, Land of No Mercy, un gioco strategico a turni opensource.

Il nostro obiettivo

In questa guida verranno mostrare altre function che stanno alla base dell'utilizzo di SDL e verrà mostrato come disegnare dei pixel sullo schermo.

In particolare verrà creato un programma che disegnerà a video un gradiente dal nero ad uno dei tre colori fondamentali (Rosso, Verde, Blu), dando la possibilità all'utente di spostarsi da un gradiente all'altro utilizzando le frecce SU e GIÙ.
Inoltre sarà aggiunta la possibilità di passare in modalità schermo intero e tornare alla modalità finestra con la semplice pressione dello spazio.
Per uscire dal programma sarà sufficiente premere Esc oppure fare click sulla x della finestra.

Sorgente

Di seguito è riportato il sorgente del programma che andremo a realizzare. Potete iniziare a darci un primo sguardo per avere una prima di idea di quello che andremo a spiegare nei prossimi paragrafi.
  1. #include <stdio.h> 
  2. #include <stdlib.h> 
  3. #include <SDL/SDL.h> 
  4.  
  5. void Put_Pixel(SDL_Surface *surface, int x, int y, Uint8 R, Uint8 G, Uint8 B); 
  6. void Draw_Screen(SDL_Surface *screen, int color); 
  7.  
  8. #define DIM_H 800 
  9. #define DIM_V 600 
  10.  
  11. int main() 
  12.   // si inizializza il sistema video 
  13.   if( SDL_Init(SDL_INIT_VIDEO) <0 ) 
  14.     // SDL_GetError() ritorna una descrizione dell'errore 
  15.     printf("Errore init SDL: %s\n", SDL_GetError()); 
  16.     return 1; 
  17.  
  18.   // all'uscita del programma esegui SDL_Quit per risistemare le cose 
  19.   atexit(SDL_Quit); 
  20.  
  21.  
  22.   //dichiariamo un puntatore ad una superficie ed un int a 32 bit per i flag necessari 
  23.   SDL_Surface *screen; 
  24.   Uint32 flags; 
  25.   //SDL_VideoInfo da informazioni sulla scheda video 
  26.   const SDL_VideoInfo *info_hw; 
  27.  
  28.   //otteniamo le informazioni sul video 
  29.   info_hw=SDL_GetVideoInfo(); 
  30.  
  31.   //verifichiamo se possiamo creare una superficie hardware, altrimenti software 
  32.   if ( info_hw->hw_available ) 
  33.     flags = SDL_HWSURFACE; 
  34.   else 
  35.     flags = SDL_SWSURFACE; 
  36.  
  37.   //aggiungiamo il buffer doppio 
  38.   flags|=SDL_DOUBLEBUF; 
  39.  
  40.   // per rendere disegnabile la Superficie bisogna utilizzare SDL_SetVideoMode 
  41.   if(!( screen = SDL_SetVideoMode(DIM_H, DIM_V, 0,flags) )) 
  42.     printf("Problemi con il settaggio dello schermo: %s\n", SDL_GetError()); 
  43.     return 1; 
  44.  
  45.   //variabile per il ciclo principale 
  46.   int done=0; 
  47.   //indice del colore 0 rosso, 1 verde , 2 blu 
  48.   int color=0; 
  49.  
  50.   //dichiariamo una struttura SDL_Event 
  51.   SDL_Event event; 
  52.  
  53.   //game loop, ovvero il loop che viene eseguito finche` non si esce 
  54.   while(!done) 
  55.     // SDL_WaitEvent attende il prossimo evento 
  56.     SDL_WaitEvent(&event); 
  57.  
  58.     //premendo la x della finestra col mouse si esce 
  59.     if ( event.type == SDL_QUIT ) 
  60.       done = 1; 
  61.  
  62.     if ( event.type == SDL_KEYDOWN ) 
  63.       //ma si esce anche premendo Esc 
  64.       if ( event.key.keysym.sym == SDLK_ESCAPE ) 
  65.         done = 1; 
  66.  
  67.       //premendo la freccia GIU` ci spostiamo al colore precendete 
  68.       if ( event.key.keysym.sym == SDLK_DOWN) 
  69.         //se abbiamo raggiunto l'inizio degli indici torniamo all'ultimo 
  70.         if(!color) 
  71.           color=2; 
  72.         else 
  73.           color--; 
  74.       //premendo la freccia SU ci spostiamo al prossimo colore 
  75.       if ( event.key.keysym.sym == SDLK_UP) 
  76.         //se abbiamo raggiunto la fine degli indici torniamo a 0 
  77.         if(color==2) 
  78.           color=0; 
  79.         else 
  80.           color++; 
  81.       //premendo spazio si passa dalla modalita` schermo intero alla modalita` 
  82.       //finestra e viceversa 
  83.       if ( event.key.keysym.sym == SDLK_SPACE ) 
  84.         flags^=SDL_FULLSCREEN; 
  85.         screen = SDL_SetVideoMode(DIM_H, DIM_V, 0,flags); 
  86.  
  87.     //coloriamo lo schermo 
  88.     Draw_Screen(screen,color); 
  89.  
  90.     //SDL_Flip disegna il buffer direttamente sullo schermo 
  91.     SDL_Flip(screen); 
  92.  
  93.  
  94.   return 0; 
  95.  
  96. //disegna un pixel sullo schermo puntato da screen alle coordinate (x,y) con colori R,G,B 
  97. void Put_Pixel(SDL_Surface *surface, int x, int y,Uint8 R, Uint8 G, Uint8 B) 
  98.   //restituisce la mappatura di un colore RGB in base ai bpp (alla definizione) 
  99.   Uint32 color = SDL_MapRGB(surface->format, R, G, B); 
  100.  
  101.   //byte per pixel 
  102.   int bpp = surface->format->BytesPerPixel; 
  103.  
  104.   //pixel della superficie da colorare 
  105.   Uint8 *p = (Uint8 *)surface->pixels + y * surface->pitch + x * bpp; 
  106.  
  107.   //verifichiamo il numero di byte usati per la nostra superficie 
  108.   switch (bpp) 
  109.     case 1: // 1 byte =>  8-bpp 
  110.       *p = color; 
  111.       break; 
  112.     case 2: // 2 byte => 16-bpp 
  113.       *(Uint16 *)p = color; 
  114.       break; 
  115.     case 3: // 3 byte => 24-bpp 
  116.       if(SDL_BYTEORDER == SDL_BIG_ENDIAN) 
  117.         p[0] = (color >> 16) & 0xff; 
  118.         p[1] = (color >> 8) & 0xff; 
  119.         p[2] = color & 0xff; 
  120.       else 
  121.         p[0] = color & 0xff; 
  122.         p[1] = (color >> 8) & 0xff; 
  123.         p[2] = (color >> 16) & 0xff; 
  124.       break; 
  125.     case 4: // 4 byte => 32-bpp 
  126.       *(Uint32 *)p = color; 
  127.       break; 
  128.  
  129. // esegue la colorazione dello schermo 
  130. void Draw_Screen(SDL_Surface *screen, int color) 
  131.   int l_c[3]={0,0,0}; 
  132.   int col; 
  133.  
  134.   if(color==0) 
  135.     l_c[0]=1; 
  136.   else if(color==1) 
  137.     l_c[1]=1; 
  138.   else if(color==2) 
  139.     l_c[2]=1; 
  140.  
  141.   //blocchiamo lo schermo se necessario 
  142.   if ( SDL_MUSTLOCK(screen) ) 
  143.     SDL_LockSurface(screen); 
  144.  
  145.   //disegnamo i pixel sul buffer screen precedentemente creato 
  146.   for(int y=0;y<DIM_V;y++) 
  147.     for(int x=0;x<DIM_H;x++) 
  148.       col=(x/4)+25; 
  149.  
  150.       Put_Pixel(screen, x,y,col*l_c[0],col*l_c[1],col*l_c[2]); 
  151.  
  152.   //sblocchiamo lo schermo se necessario 
  153.   if ( SDL_MUSTLOCK(screen) ) 
  154.     SDL_UnlockSurface(screen); 

Spiegazioni del sorgente

Procediamo con l'analisi dettagliata del sorgente e con le spiegazioni circa le nuove function e strutture utilizzate.
  1. #include <stdio.h> 
  2. #include <stdlib.h> 
  3. #include <SDL/SDL.h> 
  4.  
  5. void Put_Pixel(SDL_Surface *surface, int x, int y, Uint8 R, Uint8 G, Uint8 B); 
  6. void Draw_Screen(SDL_Surface *screen, int color); 
  7.  
  8. #define DIM_H 800 
  9. #define DIM_V 600 
Nelle prime righe includiamo gli header necessari (1-3), poi dichiariamo i prototipi delle function che useremo (5-6) e in seguito definiamo le dimensioni della nostra risoluzione (8-9).
  1. // si inizializza il sistema video 
  2. if( SDL_Init(SDL_INIT_VIDEO) <0 ) 
  3.   // SDL_GetError() ritorna una descrizione dell'errore 
  4.   printf("Errore init SDL: %s\n", SDL_GetError()); 
  5.   return 1; 
  6.  
  7. // all'uscita del programma esegui SDL_Quit per risistemare le cose 
  8. atexit(SDL_Quit); 
Nel primo blocco di codice inizializziamo il sistema video, controllando che non ci siano errori (14-19) e poi assegnamo alla function SDL_Quit il compito di gestire correttamente l'uscita (22).
  1. //dichiariamo un puntatore ad una superficie ed un int a 32 bit per i flag da assegnarle 
  2. SDL_Surface *screen; 
  3. Uint32 flags; 
In queste righe definiamo un puntatore ad una superficie, che useremo per disegnare a video, ed un intero a 32 bit, che useremo per impostare le opzioni del video.
  1. //SDL_VideoInfo da informazioni sulla scheda video 
  2. const SDL_VideoInfo *info_hw; 
  3.  
  4. //otteniamo le informazioni sul video 
  5. info_hw=SDL_GetVideoInfo(); 
  6.  
  7. //verifichiamo se possiamo creare una superficie hardware, altrimenti la creiamo software 
  8. if ( info_hw->hw_available ) 
  9.   flags = SDL_HWSURFACE; 
  10. else 
  11.   flags = SDL_SWSURFACE; 
Alla riga 29 definiamo un puntatore ad una struttura SDL_VideoInfo, la quale contiene i campi necessari a conservare le informazioni riguardanti l'hardware video.
Nel nostro caso useremo queste informazioni, dopo averle recuperate alla riga 32 grazie alla function SDL_GetVideoInfo, per verificare se è possibile creare una superficie hardware, in caso contrario, dobbiamo creare una superficie software (35-38).
Da notare che anteponiamo la parola chiave const alla struttura in quanto abbiamo a che fare con una struttura di sola lettura.

Per maggiori informazioni sulla struttura SDL_VideoInfo e sulle informazioni da essa ricavabili consiglio di eseguire un man SDL_VideoInfo.

  1. //aggiungiamo il buffer doppio 
  2. flags|=SDL_DOUBLEBUF; 
  3.  
  4. // per rendere disegnabile la Superficie bisogna utilizzare SDL_SetVideoMode 
  5. if(!( screen = SDL_SetVideoMode(DIM_H, DIM_V, 0,flags) )) 
  6.   printf("Problemi con il settaggio dello schermo: %s\n", SDL_GetError()); 
  7.   return 1; 
Alla riga 41 aggiungiamo la costante SDL_DOUBLEBUF che attiverà il buffer doppio, che ci permetterà di disegnare tutti i pixel su di una superficie per poi disegnare quest'ultima sullo schermo.

Di seguito impostiamo la superficie screen come collegata al monitor, rendendola così, la superficie disegnabile, ovviamente gestiamo il caso di errori con un messaggio e l'uscita.

  1. //variabile per il ciclo principale 
  2. int done=0; 
  3. //indice del colore 0 rosso, 1 verde , 2 blu 
  4. int color=0; 
  5.  
  6. //dichiariamo una struttura SDL_Event 
  7. SDL_Event event; 
Una volta completata l'inizializzazione di SDL, andiamo a dichiarare ed inizializzare la variabile di terminazione del ciclo principale (done) e la variabile che useremo per indicare il colore su cui si dovrà basare il gradiente (color).
Infine dichiariamo la struttura SDL_Event per gestire gli eventi.
  1. //game loop, ovvero il loop che viene eseguito finche` non si esce 
  2. while(!done) 
  3.   // SDL_WaitEvent attende il prossimo evento 
  4.   SDL_WaitEvent(&event); 
Dichiarate le variabili necessarie, possiamo cominciare il ciclo principale e chiamare il gestore degli eventi SDL_WaitEvent.
  1. //premendo la x della finestra col mouse si esce 
  2. if ( event.type == SDL_QUIT ) 
  3.   done = 1; 
Il primo evento che andiamo a gestire è il click della x di chiusura della finestra, evento che ovviamente comporta la fine dell'applicazione.
  1. if ( event.type == SDL_KEYDOWN ) 
  2.   //ma si esce anche premendo Esc 
  3.   if ( event.key.keysym.sym == SDLK_ESCAPE ) 
  4.     done = 1; 
  5.  
Il secondo evento gestito è la pressione del tasto Esc, il quale comporta anch'esso la terminazione del programma.
  1. //premendo la freccia GIU` ci spostiamo al colore precendete 
  2. if ( event.key.keysym.sym == SDLK_DOWN) 
  3.   //se abbiamo raggiunto l'inizio degli indici torniamo all'ultimo 
  4.   if(!color) 
  5.     color=2; 
  6.   else 
  7.     color--; 
  8.  
  9. //premendo la freccia SU ci spostiamo al prossimo colore 
  10. if ( event.key.keysym.sym == SDLK_UP) 
  11.   //se abbiamo raggiunto la fine degli indici torniamo a 0 
  12.   if(color==2) 
  13.     color=0; 
  14.   else 
  15.     color++; 
Come già annunciato negli obiettivi, vogliamo permettere all'utente di modificare il colore del gradiente grazie alla pressione dei tasti freccia GIÙ e freccia SU.

Alle righe 75-82 gestiamo la pressione del tasto freccia GIÙ andando a decrementare l'indice del colore, stando attenti alla condizione di indice uguale a 0, in cui è necessario ricominciare dall'indice 2.

Alle righe 85-92 gestiamo la pressione del tasto freccia SU andando ad incrementare l'indice del colore, stando attenti alla condizione di indice uguale a 2, in cui è necessario ricominciare dall'indice 0.

In pratica la nostra variabile color può essere vista come una coda circolare, la quale può assumere i valori 0 (ad indicare il rosso), 1 (ad indicare il verde) e 2 (ad indicare il blu).

  1. //premendo spazio si passa dalla modalita` schermo intero alla modalita` 
  2. //finestra e viceversa 
  3. if ( event.key.keysym.sym == SDLK_SPACE ) 
  4.   flags^=SDL_FULLSCREEN; 
  5.   screen = SDL_SetVideoMode(DIM_H, DIM_V, 0,flags); 
In fine, gestiamo la pressione dello spazio, andando a cambiare la modalità dello schermo da finestra a schermo intero e viceversa.

Come avrete notato la gestione dell'evento si è ridotta notevolmente rispetto alla prima guida [1], questo grazie all'utilizzo dell'operatore ^ ovvero OR esclusivo, un piccolo esempio di come la logica booleana, alle volte, possa aiutare decisamente nella programmazione.

  1.     //coloriamo lo schermo 
  2.     Draw_Screen(screen,color); 
  3.  
  4.     //SDL_Flip disegna il buffer direttamente sullo schermo 
  5.     SDL_Flip(screen); 
  6.  
  7.  
  8.   return 0; 
Dopo aver gestito tutti gli eventi possiamo disegnare sullo schermo, grazie alla nostra function Draw_Screen, la quale verrà spiegata in seguito.

Alla riga 105 utilizziamo SDL_Flip per svuotare il contenuto della superficie interamente sullo schermo, ciò è reso possibile grazie alla bufferizzazione doppia, infatti utilizzando SDL_Flip, una superficie viene ricopiata a video e viene aggiornata l'area interessata (in questo caso tutto lo schermo, quindi non c'è effettivo guadagno, ma quando si lavora con parti dello schermo il guadagno è notevole).
Nel caso in cui il nostro sistema non supporti la bufferizzazione doppia, verrà aggiornato l'intero schermo, in quanto SDL_Flip chiamerà la function SDL_UpdateRect in modalità tutto schermo (ovvero SDL_UpdateRect(screen, 0, 0, 0, 0)).

Una volta terminato il ciclo principale non ci resta che uscire.

Spiegazioni del sorgente - Put_Pixel

  1. //disegna un pixel sullo schermo puntato da screen alle coordinate (x,y) con colori R,G,B 
  2. void Put_Pixel(SDL_Surface *surface, int x, int y,Uint8 R, Uint8 G, Uint8 B) 
  3.   //restituisce la mappatura di un colore RGB in base ai bpp (alla definizione) 
  4.   Uint32 color = SDL_MapRGB(surface->format, R, G, B); 
  5.  
  6.   //byte per pixel 
  7.   int bpp = surface->format->BytesPerPixel; 
  8.  
  9.   //pixel della superficie da colorare 
  10.   Uint8 *p = (Uint8 *)surface->pixels + y * surface->pitch + x * bpp; 
La function Put_Pixel disegna su di una superficie un pixel alle coordinate (x,y) avente come componenti del colore i valori R,G e B.

Alla riga 116, grazie alla function SDL_MapRGB otteniamo una mappatura del colore in base alle informazioni dell'attuale formato grafico della superficie (per maggiori informazioni eseguite man SDL_PixelFormat) e alle tre componenti R,G e B del colore passate alla function.

Alla riga 119, acquisiamo l'informazione riguardante la risoluzione attuale, espressa in numero di byte utilizzati (1 byte => 8 bpp, 2 byte => 16 bpp, 3 byte => 24 bpp e 4 byte => 32 bpp).

Alla riga 122 andiamo a definire l'esatto pixel che dobbiamo colorare.

  1. //verifichiamo il numero di byte usati per la nostra superficie 
  2.   switch (bpp) 
  3.     case 1: // 1 byte =>  8-bpp 
  4.       *p = color; 
  5.       break; 
  6.     case 2: // 2 byte => 16-bpp 
  7.       *(Uint16 *)p = color; 
  8.       break; 
  9.     case 3: // 3 byte => 24-bpp 
  10.       if(SDL_BYTEORDER == SDL_BIG_ENDIAN) 
  11.         p[0] = (color >> 16) & 0xff; 
  12.         p[1] = (color >> 8) & 0xff; 
  13.         p[2] = color & 0xff; 
  14.       else 
  15.         p[0] = color & 0xff; 
  16.         p[1] = (color >> 8) & 0xff; 
  17.         p[2] = (color >> 16) & 0xff; 
  18.       break; 
  19.     case 4: // 4 byte => 32-bpp 
  20.       *(Uint32 *)p = color; 
  21.       break; 
Di seguito, andiamo ad assegnare al pixel la colorazione desiderata, curando la gestione delle quattro risoluzioni possibili con gli opportuni operatori di cast e, dando particolare attenzione alla gestione della profondità 24 bpp, che richiede una differenziazione nel caso di architettura big endian e small endian.

Spiegazioni del sorgente - Draw_Screen

  1. // esegue la colorazione dello schermo 
  2. void Draw_Screen(SDL_Surface *screen, int color) 
  3.   int l_c[3]={0,0,0}; 
  4.   int col; 
  5.  
  6.   if(color==0) 
  7.     l_c[0]=1; 
  8.   else if(color==1) 
  9.     l_c[1]=1; 
  10.   else if(color==2) 
  11.     l_c[2]=1; 
  12.  
  13.  
La function Draw_Screen si occupa di effettuare la colorazione dell'intero schermo utilizzando la function Put_Pixel.

Prima di poter iniziare la colorazione abbiamo bisogno di dichiarare un array che indicherà quale colore deve essere utilizzato per la colorazione del gradiente (l_c[3]) e di dichiarare un intero che di volta in volta conterrà la componente di un determinato colore.

  1. //blocchiamo lo schermo se necessario 
  2. if ( SDL_MUSTLOCK(screen) ) 
  3.   SDL_LockSurface(screen); 
Prima di poter disegnare direttamente dei pixel su di una superficie è necessario bloccarla onde evitare problemi. Per fare ciò usiamo la function SDL_LockSurface che prende come unico parametro la superficie da bloccare e che ritorna, in caso di errore, il valore -1. Prima di bloccare la superficie, però, verifichiamo se è necessario bloccarla con l'id alla riga 167, questo poichè alcune superfici non richiedono di essere bloccate.
  1. //disegnamo i pixel sul buffer screen precedentemente creato 
  2. for(int y=0;y<DIM_V;y++) 
  3.   for(int x=0;x<DIM_H;x++) 
  4.     col=(x/4)+25; 
  5.  
  6.     Put_Pixel(screen, x,y,col*l_c[0],col*l_c[1],col*l_c[2]); 
Una volta bloccata la superficie, possiamo andare a disegnare i pixel una riga alla volta.

Da segnalare che essendo la risoluzione orizzontale 800, la nostra componente del colore varierà tra 25 e 225 (in quanto 800/4=200) e non tra 0 e 255, questa è una semplificazione che mi sono consentito per non complicare troppo il codice del calcolo del colore, dopotutto ad occhio umano la differenza è appena percettibile e soprattutto, non stiamo realizzando un sostituto per Gimp o per Corel Draw.

  1.   //sblocchiamo lo schermo se necessario 
  2.   if ( SDL_MUSTLOCK(screen) ) 
  3.     SDL_UnlockSurface(screen); 
Terminata la colorazione sblocchiamo la superficie per continuare con il programma. Ovviamente verifichiamo sempre se è necessario bloccarla, quindi se è stata bloccata in precedenza.

Compilazione

Per compilare il nostro sorgente è necessario utilizzare come segue il gcc:
g++ -Wall -O2 -s -o SDL_secondo sorgente2.cpp `sdl-config --cflags --libs`
L'opzione -Wall ci segnalerà tutti i warning.

L'opzione -O2 userà l'ottimizzazione di secondo livello del gcc (riduzione delle dimensioni e miglioramento di prestazioni dell'eseguibile).

L'opzione -s effettuerà lo strip sull'eseguibile, rimuovendo le parti non utilizzate, rendendo quindi, le dimensioni decisamente ridotte.

L'opzione -o imposterà il nome dell'eseguibile in SDL_secondo

`sdl-config --cflags --libs` ci darà tutte le opzioni necessarie per includere ed utilizzare la libreria SDL.

Informazioni sull'autore

Davide Coppola, studente di informatica alla Federico II di Napoli, appassionato di programmazione, sicurezza e linux. Fondatore e sviluppatore di dev-labs e di Mars Land of No Mercy. Per maggiori informazioni e contatti potete visitare la sua home page.

È possibile consultare l'elenco degli articoli scritti da Davide Coppola.

Altri articoli sul tema Giochi / Programmazione / SDL.

Risorse

  1. Prima parte: "Introduzione alla programmazione con SDL".
    http://www.siforge.org/articles/2004/03/01-sdl-intro.html
  2. Sorgente dell'esempio.
    http://www.siforge.org/articles/2004/03/sdl-intro-2/sorgente2.cpp (5Kb)
  3. "Programmare in C Guida completa" di Peter Aitken, Bradley L. Jones edizioni Apogeo
    http://www.apogeonline.com/libri/88-7303-850-6/scheda
Discuti sul forum   Stampa

Cosa ne pensi di questo articolo?

Discussioni

Questo articolo o l'argomento ti ha interessato? Parliamone.