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

Gestire gli sprite con SDL

Abstract
In questa guida viene spiegato il concetto di sprite e vengono realizzate le classi necessarie ad una corretta gestione dell'oggetto con SDL. In particolare vengono gestite 3 animazioni su di uno sfondo.
Data di stesura: 23/03/2004
Data di pubblicazione: 31/05/2004
Ultima modifica: 04/04/2006
di Davide Coppola Discuti sul forum   Stampa

Premesse

Per la piena comprensione di questa guida sono necessarie delle discrete conoscenze di C++, nel caso non fosse in possesso di tali conoscenze consiglio di studiare un buon libro sull'argomento, personalmente consiglio "C++ La guida completa" della McGraw Hill [2] e la documentazione della STL realizzata dalla SGI [3].
Altre conoscenze necessarie possono essere dei fondamenti di logica booleana ed in particolare si presume che il lettore abbia già studiato le mie precedenti guide su SDL, pertanto alcuni concetti relativi a funzioni e strutture dati già utilizzate, non verranno ripresi, se non brevemente.

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, SDL 1.2.6 ed SDL_Image 1.2), ciò non dovrebbe comportare problemi nell'esecuzione del programma sotto altri sistemi, ma di questo non posso dare certezze.
La libreria SDL [4] e la sotto-libreria SDL_Image [5] possono essere scaricate agli indirizzi indicati nei riferimenti.

Chiedo scusa per la grafica utilizzata, ma non sono un grafico e dopotutto questa guida è sulla programmazione di SDL e non sull'utilizzo di GIMP.

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 gestiremo l'animazione e lo spostamento di più figure su di uno sfondo con SDL, ovvero implementeremo una gestione degli sprite in SDL.

Vista la non estrema semplicità dell'argomento sono state adottate diverse semplificazioni nel modello e soprattutto nell'implementazione, riducendo le azioni gestibili dalle nostre figure al minimo indispensabile.
Eventuali miglioramenti e progetti per il futuro verranno discussi in fondo all'articolo.

Allo stato attuale della guida le classi implementate non possono essere considerate utilizzabili in un programma (gioco) completo, ma possono essere ritenute un buon punto di partenza per uno sviluppo concreto.
Quindi un obiettivo secondario è quello di rendere le classi utilizzate facilmente aggiornabili, permettendo quindi, una certa flessibilità nell'aggiunta di caratteristiche.

Gli sprite

Volendo dare una definizione iniziale si potrebbe dire che uno sprite è un oggetto grafico al quale sono associate una o più animazioni.

Da questa prima definizione si possono iniziare a dividere gli sprite in due classi principali:

  • Gli sprite ATTIVI, ovvero quegli elementi grafici che possono essere controllati e/o con i quali si può interagire in qualche maniera.
  • Gli sprite PASSIVI, ovvero quelli che sono inseriti per pura coreografia.
Gli sprite appartenenti alla seconda categoria (i passivi), sono i più facili da gestire, in quanto l'unica preoccupazione del programmatore deve essere l'implementazione dell'animazione.

Gli sprite attivi invece, richiedono una gestione molto più accurata in quanto, oltre l'animazione, possono essere gestite altre funzionalità come ad esempio il movimento e l'interazione con degli altri sprite (la cosiddetta gestione delle collisioni).

Volendo fare degli esempi pratici, prendendo come ambientazione una schermata di un gioco di strategia, uno sprite passivo può essere un fiume che scorre, in questo caso si deve gestire l'animazione dell'acqua, ma non lo spostamento (solitamente i fiumi sono statici), mentre uno sprite attivo può essere un soldato, questa volta possiamo effettuare uno spostamento dello sprite assistendo all'animazione del movimento e possiamo farlo attaccare un bersaglio, assistendo ad un eventuale spostamento ed all'attacco.

Il modello ad oggetti utilizzato

Prima di passare all'implementazione degli sprite è necessario comprendere la loro struttura ad un livello di astrazione logico/teorico.

Gli sprite possono essere considerati come degli oggetti formati da animazioni che, come abbiamo visto, possono avere la capacità di spostarsi e di interagire con degli altri sprite, pertanto bisogna definire innanzitutto il modo di realizzare un'animazione e poi pensare a come gestire le caratteristiche "avanzate".

Un'animazione è data da un insieme di immagini che scorrendo rapidamente in sequenza, danno l'illusione del movimento, queste immagini sono definite FRAME.
Pertanto uno sprite, avendo almeno un'animazione (nel nostro caso tratteremo una sola animazione, per semplicita`), deve possedere un insieme di frame da visualizzare.

Stabilito ciò useremo un modello ad oggetti basato su due classi, la classe sprite e la classe frame. Come avrete intuito la classe sprite conterrà più oggetti frame.
Utilizzando tale modello sarà possibile distinguere le azioni sui singoli frame (modifiche, trasparenze, rotazioni, ecc..) dalle azioni sullo sprite (movimento, interazioni, ecc...).

Si tenga presente che non è necessario realizzare una classe apposita per gli sprite passivi, visti che questi possono essere considerati come sprite attivi che non si muovono, quindi un loro sott'insieme.

Deciso il modello da usare possiamo passare all'implementazione del codice.

Sorgente - main.cpp

Comprendere i vari sorgenti senza leggere le singole spiegazioni non è facile, ma per mantenere fede al modello delle guide precedenti, e soprattutto per mantenere un riferimento non frammentato, ho inserito i vari sorgenti, inoltre un lettore con una sufficiente conoscenza di SDL (almeno quella trattata nelle guide precedenti), dovrebbe riuscire a districarsi tra i sorgenti senza troppe difficoltà visti gli innumerevoli commenti in linea inseriti.
  1. #include <iostream> 
  2. #include <vector> 
  3. #include <string> 
  4. #include <SDL/SDL.h> 
  5. #include <SDL/SDL_image.h> 
  6. #include "frame.h" 
  7. #include "sprite.h" 
  8.  
  9. #define DIM_H   800 
  10. #define DIM_V   600 
  11.  
  12. using namespace std; 
  13.  
  14. //dichiariamo un puntatore ad una superficie globale  
  15. SDL_Surface *screen; 
  16.  
  17. //anche bg e` globale 
  18. SDL_Surface *bg; 
  19.  
  20. int main() 
  21.  
  22.   // si inizializza il sistema video   
  23.   if( SDL_Init(SDL_INIT_VIDEO) == -1 ) 
  24.     // SDL_GetError() ritorna una descrizione dell'errore 
  25.     cout << "Errore init SDL: " << SDL_GetError() << endl; 
  26.     return 1; 
  27.  
  28.   // all'uscita del programma esegui SDL_Quit per risistemare le cose 
  29.   atexit(SDL_Quit); 
  30.  
  31.   //dichiariamo  un int a 32 bit per i flag da assegnare a screen 
  32.   Uint32 flags; 
  33.  
  34.   flags = SDL_HWSURFACE|SDL_DOUBLEBUF; 
  35.  
  36.   // per rendere disegnabile la Superficie bisogna utilizzare SDL_SetVideoMode 
  37.   if(!( screen = SDL_SetVideoMode(DIM_H, DIM_V, 0,flags) )) 
  38.     cout << "Problemi con il settaggio dello schermo: " << SDL_GetError() << endl; 
  39.     return 1; 
  40.  
  41.   //carichiamo l'immagine di sfondo 
  42.   bg=IMG_Load("data/bg.png"); 
  43.  
  44.   //disegnamo lo sfondo su screen 
  45.   SDL_BlitSurface(bg, NULL, screen, NULL); 
  46.    
  47.   //il vector che conterra` le img dello sprite 
  48.   vector<string> files; 
  49.    
  50.   //carichiamo le stringhe nel vector 
  51.   files.push_back("data/tank_01.png"); 
  52.   files.push_back("data/tank_02.png"); 
  53.   files.push_back("data/tank_03.png"); 
  54.   files.push_back("data/tank_04.png"); 
  55.   files.push_back("data/tank_05.png"); 
  56.   files.push_back("data/tank_06.png"); 
  57.   files.push_back("data/tank_07.png"); 
  58.   files.push_back("data/tank_08.png"); 
  59.    
  60.   //istanziamo gli sprite 
  61.   sprite tank(files); 
  62.   sprite tank2(files,0,200,1); 
  63.   sprite tank3(files,0,400,3); 
  64.  
  65.   //flag di fine loop principale 
  66.   int end=0; 
  67.   //limite orizzontale da non superare  
  68.   int x_limit2=DIM_H-tank2.get_w(); 
  69.   int x_limit3=(DIM_H-tank3.get_w())/3; 
  70.     
  71.   //stato della tastiera 
  72.   Uint8* keys; 
  73.  
  74.   while(!end) 
  75.     //dichiariamo una struttura SDL_Event 
  76.     SDL_Event event; 
  77.  
  78.     // SDL_PollEvent attiva il gestore di eventi 
  79.     while ( SDL_PollEvent(&event) ) 
  80.       //premendo la x della finestra col mouse si esce 
  81.       if ( event.type == SDL_QUIT )   
  82.         end = 1;   
  83.  
  84.       if ( event.type == SDL_KEYDOWN ) 
  85.         //ma si esce anche premendo Esc 
  86.         if ( event.key.keysym.sym == SDLK_ESCAPE )  
  87.           end = 1;  
  88.         //premendo spazio si passa dalla modalita` schermo intero alla modalita`  
  89.         //finestra e viceversa 
  90.         if ( event.key.keysym.sym == SDLK_SPACE )  
  91.           flags^=SDL_FULLSCREEN; 
  92.           screen = SDL_SetVideoMode(DIM_H, DIM_V, 0,flags); 
  93.  
  94.           //disegnamo lo sfondo su screen 
  95.           SDL_BlitSurface(bg, NULL, screen, NULL); 
  96.      
  97.      
  98.     //per verificare se un tasto e` mantenuto premuto usiamo SDL_GetKeyState che  
  99.     //ritorna un'immagine dei tasti premuti 
  100.     keys = SDL_GetKeyState(NULL); 
  101.      
  102.     //FRECCIA SU: se il tank non e` posizionato a (x,0) andiamo su 
  103.     if ( keys[SDLK_UP] )  
  104.     {        
  105.       if(tank.get_y()) 
  106.         tank.mov_y(-1); 
  107.     //FRECCIA GIU: approssima il bordo inferiore in base al numero di tank e a DIM_V 
  108.     if ( keys[SDLK_DOWN] )  
  109.       if(tank.get_y()<(DIM_V-tank.get_h())) 
  110.         tank.mov_y(); 
  111.     //FRECCIA SX: se il tank non e` posizionato a (0,y) andiamo a sx 
  112.     if ( keys[SDLK_LEFT] )  
  113.       if(tank.get_x()) 
  114.         tank.mov_x(-1); 
  115.     //FRECCIA DX: se il tank non e` posizionato a (DIM_H-tank->w,y) andiamo a sx 
  116.     if ( keys[SDLK_RIGHT] )  
  117.       if(tank.get_x()<(DIM_H-tank.get_w())) 
  118.         tank.mov_x(); 
  119.              
  120.     //muoviamo il secondo tank, se ci troviamo entro i limiti 
  121.     if((x_limit2--)>0) 
  122.       tank2.mov_x(1); 
  123.  
  124.     //muoviamo il terzo tank, se ci troviamo entro i limiti 
  125.     if((x_limit3--)>0) 
  126.       tank3.mov_x(1); 
  127.      
  128.     //disegnamo le parti di sfondo interessate 
  129.     tank.draw_bg(); 
  130.     tank2.draw_bg(); 
  131.     tank3.draw_bg(); 
  132.  
  133.     //disegniamo i tank 
  134.     tank.draw_sprite(); 
  135.     tank2.draw_sprite(); 
  136.     tank3.draw_sprite(); 
  137.  
  138.     //aggiorniamo lo schermo 
  139.     SDL_Flip(screen); 
  140.  
  141.   return 0; 
  142.    

Sorgente - sprite.h

  1. #ifndef __CLASS_SPRITE__ 
  2. #define __CLASS_SPRITE__ 
  3.  
  4. #include <vector> 
  5. #include <string> 
  6. #include <SDL/SDL.h> 
  7. #include "frame.h" 
  8.  
  9. using namespace std; 
  10.  
  11. //accediamo alla screen globale 
  12. extern SDL_Surface *screen; 
  13. //accediamo alla bg globale 
  14. extern SDL_Surface *bg; 
  15.  
  16. class sprite 
  17.   //cordinate dell'img (x,y) 
  18.   int x, y;  
  19.   //velocita` di spostamento dell'animazione 
  20.   int s_speed; 
  21.   //indice del frame corrente 
  22.   int cur_frame; 
  23.   //flag che indica se ci stiamo muovendo 
  24.   bool moving; 
  25.   //ultimo tempo di aggiornamento del frame 
  26.   long update_time; 
  27.   //idealmente uno sprite e` un insieme di frame  
  28.   vector<frame *> frames; 
  29. public: 
  30.   //costruttore 
  31.   sprite(vector <string> files, int init_x=0, int init_y=0, int speed=2);  
  32.   //distruttore 
  33.   ~sprite(); 
  34.  
  35.   //function di spostamento frame 
  36.   void mov_x(int mov=1);  
  37.   void mov_y(int mov=1);  
  38.  
  39.  
  40.   //function che ritornano le cordinate posizionali del frame 
  41.   int get_x() { return x; }; 
  42.   int get_y() { return y; }; 
  43.    
  44.   //function che ritornano le dimensioni di un frame 
  45.   int get_w(int ind=0) { return frames[ind]->get_w();} 
  46.   int get_h(int ind=0) { return frames[ind]->get_h();} 
  47.  
  48.   //function principale, che gestisce il disegno e l'animazione dello sprite 
  49.   void draw_sprite(); 
  50.   //function che ridisegna il background modificato dallo sprite 
  51.   void draw_bg(); 
  52.  
  53. }; 
  54.  
  55. #endif 

Sorgente - sprite.cpp

  1. #include "sprite.h" 
  2.  
  3. //costruttore che si occupa di inizializzare tutti i dati necessari 
  4. sprite::sprite(vector <string> files,  int init_x, int init_y, int speed) 
  5.   //contatori 
  6.   int i,num_files=files.size(); 
  7.  
  8.   //puntatore temporaneo 
  9.   frame * temp_frame; 
  10.    
  11.   //alloco tutti i frame necessari 
  12.   for(i=0;i<num_files;i++) 
  13.     temp_frame= new frame (files[i],110);            
  14.     frames.push_back(temp_frame); 
  15.    
  16.   //inizializziamo le cordinate dello sprite       
  17.   x=init_x; 
  18.   y=init_y; 
  19.  
  20.   //settiamo la velocita` dello sprite 
  21.   s_speed=speed; 
  22.  
  23.   //il primo frame ha indice 0 
  24.   cur_frame=0; 
  25.  
  26.   //il tempo iniziale e` 0  
  27.   update_time=0; 
  28.  
  29.   //inizialmente non ci muoviamo 
  30.   moving=false; 
  31.  
  32. //function che si occupa di gestire il disegno e l'animazione dello sprite 
  33. void sprite::draw_sprite() 
  34.   //se e` passato il tempo necessario passiamo al prossimo frame 
  35.   if(update_time+(frames[cur_frame]->get_pause())<SDL_GetTicks()) 
  36.     cur_frame++; 
  37.  
  38.     //se siamo all'ultimo frame torniamo a 0 
  39.     if(cur_frame==(frames.size()))  
  40.             cur_frame=0; 
  41.  
  42.     //aggiorniamo il tempo dell'ultima modifica  
  43.     update_time = SDL_GetTicks(); 
  44.  
  45.   //indichiamo le cordinate del tank 
  46.   SDL_Rect dest_t; 
  47.   dest_t.x = x;  
  48.   dest_t.y = y; 
  49.   dest_t.w = frames[cur_frame]->get_w();  
  50.   dest_t.h = frames[cur_frame]->get_h();  
  51.    
  52.   //disegnamo il tank su screen 
  53.   if(moving) 
  54.     SDL_BlitSurface(frames[cur_frame]->get_surface(), NULL, screen,&dest_t); 
  55.   else 
  56.     SDL_BlitSurface(frames[0]->get_surface(), NULL, screen,&dest_t); 
  57.     cur_frame=0; 
  58.  
  59.   //consideriamo l'animazione finita       
  60.   moving=false; 
  61.  
  62.  
  63. //function che ridisegna il background modificato dallo sprite 
  64. void sprite::draw_bg() 
  65.   //aggiorniamo il rettangolo che circonda il tank 
  66.   SDL_Rect rect_bg; 
  67.   rect_bg.x = x-s_speed;  
  68.   rect_bg.y = y-s_speed; 
  69.   rect_bg.w = frames[cur_frame]->get_w()+(s_speed*2); 
  70.   rect_bg.h = frames[cur_frame]->get_h()+(s_speed*2); 
  71.  
  72.   //copiamo la parte di bg necessaria su screen 
  73.   SDL_BlitSurface(bg, &rect_bg, screen, &rect_bg); 
  74.  
  75. //function spostamento orizzontale 
  76. void sprite::mov_x(int mov)  
  77. {  
  78.   //ci spostiamo in base ad s_speed 
  79.   x+=mov*s_speed;  
  80.   //segnaliamo che ci stiamo muovendo 
  81.   moving=true; 
  82.  
  83. //function spostamento verticale 
  84. void sprite::mov_y(int mov)  
  85. {  
  86.   //ci spostiamo in base ad s_speed 
  87.   y+=mov*s_speed;  
  88.   //segnaliamo che ci stiamo muovendo 
  89.   moving=true; 
  90.  
  91. //distruttore 
  92. sprite::~sprite() 
  93.   //contatori 
  94.   int i,num_files=frames.size(); 
  95.    
  96.   //chiamiamo il distruttore dei vari frame allocati precendentemente 
  97.   for(i=0;i<num_files;i++) 
  98.     frames[i]->~frame(); 

Sorgente - frame.h

  1. #ifndef __CLASS_FRAME__ 
  2. #define __CLASS_FRAME__ 
  3.  
  4. #include <string> 
  5. #include <SDL/SDL.h> 
  6. #include <SDL/SDL_image.h> 
  7.  
  8. using namespace std; 
  9.  
  10. class frame 
  11.   //la superficie del frame 
  12.   SDL_Surface *The_Frame; 
  13.   //pausa dal prossimo frame 
  14.   int The_Pause; 
  15. public: 
  16.   //costruttore 
  17.   frame(string img, int pause); 
  18.   //distruttore 
  19.   ~frame(); 
  20.  
  21.   //function che ritorna una superficie dell'animazione 
  22.   SDL_Surface * get_surface() { return The_Frame; }; 
  23.    
  24.   //function che ritorna la larghezza del frame 
  25.   int get_w() { return The_Frame->w; }; 
  26.   //function che ritorna l'altezza del frame 
  27.   int get_h() { return The_Frame->h; }; 
  28.  
  29.   //function che ritorna la pausa impostata 
  30.   int get_pause() { return The_Pause; };   
  31. }; 
  32.  
  33. #endif 

Sorgente - frame.cpp

  1. #include "frame.h" 
  2.  
  3. //costruttore, inizializzamo i dati necessari 
  4. frame::frame(string img,int pause) 
  5.   //carichiamo l'immagine necessaria 
  6.   The_Frame=IMG_Load(img.c_str()); 
  7.    
  8.   //colore da rendere trasparente 
  9.   Uint32 colorkey; 
  10.   //prendiamo la mappatura del colore #0000FF in base al nostro specifico formato di pixel 
  11.   colorkey = SDL_MapRGB(The_Frame->format, 0, 0, 0xFF); 
  12.   //impostiamo il colore come trasparente 
  13.   SDL_SetColorKey(The_Frame, SDL_SRCCOLORKEY|SDL_RLEACCEL, colorkey); 
  14.    
  15.   //pausa dal prossimo frame 
  16.   The_Pause=pause; 
  17.  
  18. //distruttore, liberiamo la superficie necessaria 
  19. frame::~frame() 
  20. {  
  21.   SDL_FreeSurface(The_Frame); 

Spiegazione del sorgente - main.cpp

  1. #include <iostream> 
  2. #include <vector> 
  3. #include <string> 
  4. #include <SDL/SDL.h> 
  5. #include <SDL/SDL_image.h> 
  6. #include "frame.h" 
  7. #include "sprite.h" 
  8.  
  9. #define DIM_H   800 
  10. #define DIM_V   600 
  11.  
  12. using namespace std; 
Nelle prime righe del main non facciamo altro che includere gli header necessari, definire la nostra risoluzione e dichiarare l'utilizzo del namespace std.
  1. //dichiariamo un puntatore ad una superficie globale  
  2. SDL_Surface *screen; 
  3.  
  4. //anche bg e` globale 
  5. SDL_Surface *bg; 
Di seguito dichiariamo le superifici screen e bg come globali, questo per poterle utilizzare con maggiore comodità nelle altre classi (in particolare in sprite).
  1. int main() 
  2.  
  3.   // si inizializza il sistema video   
  4.   if( SDL_Init(SDL_INIT_VIDEO) == -1 ) 
  5.     // SDL_GetError() ritorna una descrizione dell'errore 
  6.     cout << "Errore init SDL: " << SDL_GetError() << endl; 
  7.     return 1; 
  8.  
  9.   // all'uscita del programma esegui SDL_Quit per risistemare le cose 
  10.   atexit(SDL_Quit); 
Inizializziamo il sotto-sistema video, gestendo l'errore, poi associamo la function SDL_Quit ad ogni uscita corretta del nostro programma.
  1. //dichiariamo  un int a 32 bit per i flag da assegnare a screen 
  2. Uint32 flags; 
  3.  
  4. flags = SDL_HWSURFACE|SDL_DOUBLEBUF; 
  5.  
  6. // per rendere disegnabile la Superficie bisogna utilizzare SDL_SetVideoMode 
  7. if(!( screen = SDL_SetVideoMode(DIM_H, DIM_V, 0,flags) )) 
  8.   cout << "Problemi con il settaggio dello schermo: " << SDL_GetError() << endl; 
  9.   return 1; 
Associamo il video alla superficie screen impostado i flag necessari per una superficie hardware con doppio buffer.
  1. //carichiamo l'immagine di sfondo 
  2. bg=IMG_Load("data/bg.png"); 
  3.  
  4. //disegnamo lo sfondo su screen 
  5. SDL_BlitSurface(bg, NULL, screen, NULL); 
Carichiamo l'immagine di sfondo e la copiamo sulla superficie screen, visto che la dimensione dell'immagine è la stessa della superficie, possiamo evitare di specificare i rettangoli sorgente e destinazione nella function SDL_BlitSurface.
  1. //il vector che conterra` le img dello sprite 
  2. vector<string> files; 
  3.  
  4. //carichiamo le stringhe nel vector 
  5. files.push_back("data/tank_01.png"); 
  6. files.push_back("data/tank_02.png"); 
  7. files.push_back("data/tank_03.png"); 
  8. files.push_back("data/tank_04.png"); 
  9. files.push_back("data/tank_05.png"); 
  10. files.push_back("data/tank_06.png"); 
  11. files.push_back("data/tank_07.png"); 
  12. files.push_back("data/tank_08.png"); 
In queste righe dichiariamo un vector di string che utilizziamo per contenere tutti i file che costituiscono i nostri frame. Tale vettore sarà passato al costruttore della classe sprite per l'inizializzazione dei vari frame.
  1. //istanziamo gli sprite 
  2. sprite tank(files); 
  3. sprite tank2(files,0,200,1); 
  4. sprite tank3(files,0,400,3); 
Una volta definite le immagini che compongono i frame possiamo creare i tre tank che utilizzeremo nel nostro programma.
Come potete notare la prima dichiarazione presenta un solo parametro, ovvero il vector di string contenente i percorsi dei file delle immagini da utilizzare.
Gli altri tre parametri (posizione orizzontale, posizione verticale e velocita`) sono lasciati impostati sui valori di default. Il secondo ed il terzo tank invece, specificano questi valori.
  1. //flag di fine loop principale 
  2. int end=0; 
  3. //limite orizzontale da non superare  
  4. int x_limit2=DIM_H-tank2.get_w(); 
  5. int x_limit3=(DIM_H-tank3.get_w())/3; 
  6.  
  7. //stato della tastiera 
  8. Uint8* keys; 
In queste linee vengono dichiarate ed inizializzate la variabile che controlla l'uscita dal loop principale, poi le variabili che indicano il limite orizzontale dell'avanzamento del secondo e del terzo tank ed infine viene dichiarato l'intero senza segno ad otto bit che conterrà lo stato della tastiera.
  1. while(!end) 
  2.   //dichiariamo una struttura SDL_Event 
  3.   SDL_Event event; 
  4.  
  5.   // SDL_PollEvent attiva il gestore di eventi 
  6.   while ( SDL_PollEvent(&event) ) 
Iniziato il ciclo principale andiamo ad attivare il gestore di eventi passandogli la struttura necessaria.
  1. //premendo la x della finestra col mouse si esce 
  2. if ( event.type == SDL_QUIT )   
  3.   end = 1; 
Il primo evento gestito è la pressione della x sulla finestra aperta, l'evento comporta la fine del nostro ciclo principale, quindi del programma.
  1. if ( event.type == SDL_KEYDOWN ) 
  2.   //ma si esce anche premendo Esc 
  3.   if ( event.key.keysym.sym == SDLK_ESCAPE )  
  4.     end = 1; 
Di seguito verifichiamo se è stato premuto e rilasciato il tasto ESC, anche in tal caso, terminiamo il ciclo principale ed usciamo.
  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); 
  6.  
  7.   //disegnamo lo sfondo su screen 
  8.   SDL_BlitSurface(bg, NULL, screen, NULL); 
La pressione del tasto spazio comporta il passaggio da modalità finestra a modalità schermo intero e viceversa.
Tale passaggio necessità il ridisegnamento dello sfondo sulla superficie screen (riga 104).
  1. //per verificare se un tasto e` mantenuto premuto usiamo SDL_GetKeyState che  
  2. //ritorna un'immagine dei tasti premuti 
  3. keys = SDL_GetKeyState(NULL); 
Alla riga 112 recuperiamo l'immagine della tastiera, quindi i tasti che sono premuti al momento.
  1. //FRECCIA SU: se il tank non e` posizionato a (x,0) andiamo su 
  2. if ( keys[SDLK_UP] )  
  3. {        
  4.   if(tank.get_y()) 
  5.     tank.mov_y(-1); 
  6. //FRECCIA GIU: approssima il bordo inferiore in base al numero di tank e a DIM_V 
  7. if ( keys[SDLK_DOWN] )  
  8.   if(tank.get_y()<(DIM_V-tank.get_h())) 
  9.     tank.mov_y(); 
  10. //FRECCIA SX: se il tank non e` posizionato a (0,y) andiamo a sx 
  11. if ( keys[SDLK_LEFT] )  
  12.   if(tank.get_x()) 
  13.     tank.mov_x(-1); 
  14. //FRECCIA DX: se il tank non e` posizionato a (DIM_H-tank->w,y) andiamo a sx 
  15. if ( keys[SDLK_RIGHT] )  
  16.   if(tank.get_x()<(DIM_H-tank.get_w())) 
  17.     tank.mov_x(); 
In queste righe gestiamo lo spostamento del primo tank verificando se è premuta una freccia direzionale.
I vari if interni si preoccupano di mantenere il tank all'interno della superficie screen. I due metodi per la gestione del movimento verranno trattati all'interno della classe sprite.
  1. //muoviamo il secondo tank, se ci troviamo entro i limiti 
  2. if((x_limit2--)>0) 
  3.   tank2.mov_x(1); 
  4.  
  5. //muoviamo il terzo tank, se ci troviamo entro i limiti 
  6. if((x_limit3--)>0) 
  7.   tank3.mov_x(1); 
Gli spostamenti del secondo e del terzo tank avvengono lungo l'asse orizzontale finché la superficie lo permette, ovvero li facciamo avanzare finché non si raggiunge il bordo destro.
  1. //disegnamo le parti di sfondo interessate 
  2. tank.draw_bg(); 
  3. tank2.draw_bg(); 
  4. tank3.draw_bg(); 
  5.  
  6. //disegniamo i tank 
  7. tank.draw_sprite(); 
  8. tank2.draw_sprite(); 
  9. tank3.draw_sprite(); 
Per disegnare i tank sullo schermo correttamente abbiamo bisogno di ridisegnare le parti di sfondo che vengono alterate dallo spostamento dei tank (148-150). Una volta aggiornate le parti di sfondo possiamo disegnare i tre sprite (153-155).

Per la spiegazione sul come funzionano questi metodi vi rimando sempre alla classe sprite.

  1.   //aggiorniamo lo schermo 
  2.   SDL_Flip(screen); 
Una volta terminati i disegni sulla superficie screen la riversiamo sullo schermo con la function SDL_Flip.
  1.   return 0; 
  2.          
Terminato il ciclo principale non ci resta che uscire.

Spiegazione del sorgente - sprite.h

sprite.h è l'header file della classe sprite, in esso è dichiarata la struttura della classe. L'implementazione dei metodi qui presentati, verrà illustrata nel prossimo paragrafo.
  1. #ifndef __CLASS_SPRITE__ 
  2. #define __CLASS_SPRITE__ 
  3.  
  4. #include <vector> 
  5. #include <string> 
  6. #include <SDL/SDL.h> 
  7. #include "frame.h" 
  8.  
  9. using namespace std; 
  10.  
  11. //accediamo alla screen globale 
  12. extern SDL_Surface *screen; 
  13. //accediamo alla bg globale 
  14. extern SDL_Surface *bg; 
Nelle prime righe dell'header della classe sprite includiamo gli header necessari e dichiriamo l'utilizzo del namespace std, poi accediamo alle variabili globali screen e bg dichiarate nel main (quindi esterne).
  1. class sprite 
  2.   //cordinate dell'img (x,y) 
  3.   int x, y;  
  4.   //velocita` di spostamento dell'animazione 
  5.   int s_speed; 
  6.   //indice del frame corrente 
  7.   int cur_frame; 
  8.   //flag che indica se ci stiamo muovendo 
  9.   bool moving; 
  10.   //ultimo tempo di aggiornamento del frame 
  11.   long update_time; 
  12.   //idealmente uno sprite e` un insieme di frame  
  13.   vector<frame *> frames; 
Nella parte iniziale della classe sono dichiarati gli attributi privati.
Andando per ordine discendente possiamo incontrare x e y che indicano la posizione dello sprite sullo schermo, poi la velocità s_speed con cui facciamo muovere il nostro sprire, ovvero il numero di pixel di cui lo spostiamo ad ogni movimento, di seguito troviamo il contatore cur_frame che indica il numero del frame visualizzato al momento, poi la flag booleana moving, la quale indica lo stato dello sprite, ovvero se si è in movimento o se si è fermi, poi la variabile update_time che tiene traccia dell'ultimo aggiornamento dell'animazione dello sprite ed infine frames, il vector di puntatori agli oggetti frame che costituiscono la nostra animazione.
  1. public: 
  2.   //costruttore 
  3.   sprite(vector <string> files, int init_x=0, int init_y=0, int speed=2);  
  4.   //distruttore 
  5.   ~sprite(); 
Alla riga 30 iniziano i metodi pubblici, nelle prime righe troviamo il costruttore ed il distruttore (32-34).
Notate i valori di default del costruttore che ci hanno permesso di dichiarare il primo tank con un solo parametro (riga 66 del main.c).
  1. //function di spostamento frame 
  2. void mov_x(int mov=1);  
  3. void mov_y(int mov=1); 
Subito dopo troviamo i metodi che si occupano del movimento dello sprite, rispettivamente di quello orizzontale (mov_x) e di quello verticale (mov_y). Anche qui possiamo notare i valori di default associati al parametro.
  1. //function che ritornano le cordinate posizionali del frame 
  2. int get_x() { return x; }; 
  3. int get_y() { return y; }; 
  4.  
  5. //function che ritornano le dimensioni di un frame 
  6. int get_w(int ind=0) { return frames[ind]->get_w();} 
  7. int get_h(int ind=0) { return frames[ind]->get_h();} 
Alle righe 42 e 43 sono presenti due metodi implementati in linea che ritornano le cordinate x ed y dello sprite. Alle righe 46 e 47 invece, troviamo altri due metodi in linea che ritornano le dimensioni orizzontali e verticali del frame di indice ind. Nel caso non venga indicato alcun indice, viene utilizzato il parametro di default, quindi viene ritornata la dimensione del primo frame.
Ciò avviene perché solitamente i frame hanno tutti la stessa dimensione, quindi è comodo evitare di passare esplicitamente un parametro.
  1.   //function principale, che gestisce il disegno e l'animazione dello sprite 
  2.   void draw_sprite(); 
  3.   //function che ridisegna il background modificato dallo sprite 
  4.   void draw_bg(); 
  5.  
  6. }; 
  7.  
  8. #endif 
Gli ultimi due metodi implementati sono quelli per il disegno dello sprite (riga 50) e della parte di sfondo modificata dallo spostamento di quest'ultimo (riga 52).

Spiegazione del sorgente - sprite.cpp

  1. #include "sprite.h" 
Ovviamente includiamo l'header sprite.h per operare correttamente sulla classe.
  1. //costruttore che si occupa di inizializzare tutti i dati necessari 
  2. sprite::sprite(vector <string> files,  int init_x, int init_y, int speed) 
  3.   //contatori 
  4.   int i,num_files=files.size(); 
  5.  
  6.   //puntatore temporaneo 
  7.   frame * temp_frame; 
  8.    
  9.   //alloco tutti i frame necessari 
  10.   for(i=0;i<num_files;i++) 
  11.     temp_frame= new frame (files[i],110);            
  12.     frames.push_back(temp_frame); 
Il primo metodo che troviamo è il costruttore, il cui compito è quello di inizializzare gli attributi in maniera appropriata.

La prima azione che effettuiamo è l'istanziazione dei frame necessari alla nostra animazione, per fare ciò utilizziamo l'allocazione dinamica allocando tanti frame quanti sono i file immagine a disposizione (13-17).
I puntatori ai frame allocati vengono inseriti nel vector frames, attributo della classe sprite.

  1. //inizializziamo le cordinate dello sprite       
  2. x=init_x; 
  3. y=init_y; 
  4.  
  5. //settiamo la velocita` dello sprite 
  6. s_speed=speed; 
  7.  
  8. //il primo frame ha indice 0 
  9. cur_frame=0; 
  10.  
  11. //il tempo iniziale e` 0  
  12. update_time=0; 
  13.  
  14. //inizialmente non ci muoviamo 
  15. moving=false; 
Le altre inizializzazioni sono decisamente più semplici in quanto andiamo ad assegnare a x,y ed s_spees i valori passatti al costruttore ed andiamo ad inizializzare cur_frame e update_time al valore 0, con ovvio significato. Infine consideriamo l'oggetto, inizialmente, fermo, quindi impostiamo il valore di moving su false.
  1. //function che si occupa di gestire il disegno e l'animazione dello sprite 
  2. void sprite::draw_sprite() 
  3.   //se e` passato il tempo necessario passiamo al prossimo frame 
  4.   if(update_time+(frames[cur_frame]->get_pause())<SDL_GetTicks()) 
  5.     cur_frame++; 
  6.  
  7.     //se siamo all'ultimo frame torniamo a 0 
  8.     if(cur_frame==(frames.size()))  
  9.             cur_frame=0; 
  10.  
  11.     //aggiorniamo il tempo dell'ultima modifica  
  12.     update_time = SDL_GetTicks(); 
Il metodo draw_sprite si occupa di determinare quale frame disegnare di volta in volta che viene invocato, per fare ciò si utilizza un semplice if che verifica se il tempo dell'ultimo aggiornamento (inizialmente 0) sommato alla pausa del frame corrente è inferiore al valore ritornato dalla function SDL_GetTicks. La function SDL_GetTicks ritorna il numero di millisecondi trascorsi dall'inizializzazione di SDL (da notare che se il programma viene utilizzato per più di 49 giorni ci possono essere dei comportamenti anomali).
Nel caso la verifica è positiva andiamo ad incrementare il valore di cur_frame prestando attenzione al caso in cui abbiamo raggiunto l'ultima immagine, quando si verifica ciò riazzeriamo il contatore (in pratica utilizziamo una sorte di coda circolare per la gestione dei frame).
Infine aggiorniamo update_time, per conservare il tempo dell'ultimo aggiornamento in modo da ripetere il procedimento alla prossima invocazione.
  1. //indichiamo le cordinate del tank 
  2. SDL_Rect dest_t; 
  3. dest_t.x = x;  
  4. dest_t.y = y; 
  5. dest_t.w = frames[cur_frame]->get_w();  
  6. dest_t.h = frames[cur_frame]->get_h(); 
Per disegnare il frame corrente abbiamo bisogno di indicare un rettangolo su screen dove inserirlo, per fare cià utilizziamo una struttura SDL_Rect inizializzata con le cordinate attuali dello sprite e con le dimensioni del frame corrente.
  1. //disegnamo il tank su screen 
  2. if(moving) 
  3.   SDL_BlitSurface(frames[cur_frame]->get_surface(), NULL, screen,&dest_t); 
  4. else 
  5.   SDL_BlitSurface(frames[0]->get_surface(), NULL, screen,&dest_t); 
  6.   cur_frame=0; 
Prima di disegnare il frame verifichiamo se ci troviamo in movimento (moving è true) oppure se siamo fermi. Nel primo caso non dobbiamo fare altro che copiare la superficie del frame corrente nell'apposito rettangolo su screen (riga 61), mentre, se stiamo fermi, copiamo la superficie del primo frame e impostiamo cur_frame a 0, ovvero all'indice del primo frame, in modo da far ripartire le future animazioni dall'inizio.
  1. //consideriamo l'animazione finita       
  2. moving = false; 
Prima di terminare impostiamo moving su false, presumendo che il movimento sia finito, di seguito vedremo che il flag verrà aggiornato ad ogni movimento.
  1. //function che ridisegna il background modificato dallo sprite 
  2. void sprite::draw_bg() 
  3.   //aggiorniamo il rettangolo che circonda il tank 
  4.   SDL_Rect rect_bg; 
  5.   rect_bg.x = x-s_speed;  
  6.   rect_bg.y = y-s_speed; 
  7.   rect_bg.w = frames[cur_frame]->get_w()+(s_speed*2); 
  8.   rect_bg.h = frames[cur_frame]->get_h()+(s_speed*2); 
Il metodo draw_bg si occupa di risistemare la parte di sfondo alterata dal moviemnto del nostro sprite. Per fare ciò andiamo a ridisegnare un rettangolo che ha come cordinate le cordinate dello sprite meno il valore della velocità, e che ha come dimensioni le dimensioni del frame aumentate del doppio della velocità.
Ciò ci permette di avere un rettangolo più grande dello srpite di s_speed pixel per lato, in pratica si usa la stessa tecnica usata nella guida mia precedente guida sulla gestione delle immagini in SDL [6].
  1. //copiamo la parte di bg necessaria su screen 
  2. SDL_BlitSurface(bg, &rect_bg, screen, &rect_bg); 
La parte di screen alterata viene sovrascritta con la corrispettiva parte di bg, la quale non subendo modifiche si è mantenuta inalterata.
  1. //function spostamento orizzontale 
  2. void sprite::mov_x(int mov)  
  3. {  
  4.   //ci spostiamo in base ad s_speed 
  5.   x+=mov*s_speed;  
  6.   //segnaliamo che ci stiamo muovendo 
  7.   moving=true; 
Il metodo mov_x è semplice, in pratica modifica la posizione orizzontale dello sprite in base al valore dell'attributo s_speed, che determina il numero di pixel dello spostamento e del parametro mov che costituisce la direzione di quest'ultimo (quindi viene passato 1 o -1 a seconda che ci si voglia spostare a destra o a sinistra).
Ogni volta che si esegue questo metodo viene impostato il flag moving su true, in modo da effettuare le azioni appropriate in draw_sprite.
  1. //function spostamento verticale 
  2. void sprite::mov_y(int mov)  
  3. {  
  4.   //ci spostiamo in base ad s_speed 
  5.   y+=mov*s_speed;  
  6.   //segnaliamo che ci stiamo muovendo 
  7.   moving=true; 
mov_y è il corrispettivo di mov_x per gli spostamenti verticali, pertanto è inutile ripetere quanto già detto.
  1. //distruttore 
  2. sprite::~sprite() 
  3.   //contatori 
  4.   int i,num_files=frames.size(); 
  5.    
  6.   //chiamiamo il distruttore dei vari frame allocati precendentemente 
  7.   for(i=0;i<num_files;i++) 
  8.     frames[i]->~frame(); 
L'ultimo metodo della classe è il distruttore, nel nostro caso si occupa di invocare i distruttori di tutti i frame allocati.

Spiegazione del sorgente - frame.h

frame.h è l'header file della classe frame, in esso è dichiarata la struttura della classe. L'implementazione dei metodi qui presentati, verrà illustrata nel prossimo paragrafo.
  1. #ifndef __CLASS_FRAME__ 
  2. #define __CLASS_FRAME__ 
  3.  
  4. #include <string> 
  5. #include <SDL/SDL.h> 
  6. #include <SDL/SDL_image.h> 
  7.  
  8. using namespace std; 
Come sempre la prima parte è dedicata all'inclusione degli header necessari e alla dichiarazione di utilizzo del namespace.
  1. class frame 
  2.   //la superficie del frame 
  3.   SDL_Surface *The_Frame; 
  4.   //pausa dal prossimo frame 
  5.   int The_Pause; 
Gli attributi privati della classe sono soltanto due, un puntatore a SDL_Surface (The_Frame), che servità per l'associazione del frame ad un'immagine e un intero (The_Pause), che servirà per indicare la pausa (in millisecondi) che si desidera dal prossimo frame.
  1. public: 
  2.   //costruttore 
  3.   frame(string img, int pause); 
  4.   //distruttore 
  5.   ~frame(); 
Anche qui i primi metodi dichiarati sono il costruttore ed il distruttore.
  1. //function che ritorna una superficie dell'animazione 
  2. SDL_Surface * get_surface() { return The_Frame; }; 
Alla riga 23 è presente un metodo in linea che ritorna il puntatore alla superficie del frame, ovvero alla superficie contenente l'immagine, come abbiamo visto nella classe sprite, tale informazione è necessaria per poter disegnare i vari frame.
  1.   //function che ritorna la larghezza del frame 
  2.   int get_w() { return The_Frame->w; }; 
  3.   //function che ritorna l'altezza del frame 
  4.   int get_h() { return The_Frame->h; }; 
  5.  
  6.   //function che ritorna la pausa impostata 
  7.   int get_pause() { return The_Pause; };   
  8. }; 
  9.  
  10. #endif 
Gli ultimi metodi presenti sono implementati tutti in linea e forniscono funzioni "informative" per la classe sprite, infatti abbiamo due metodi che ritornano le dimensioni del frame (righe 26 e 28) e un metodo che ritorna la pausa impostata (riga 31).

Spiegazione del sorgente - frame.cpp

  1. #include "frame.h" 
Ovviamente includiamo l'header frame.h per operare correttamente sulla classe.
  1. //costruttore, inizializzamo i dati necessari 
  2. frame::frame(string img,int pause) 
  3.   //carichiamo l'immagine necessaria 
  4.   The_Frame=IMG_Load(img.c_str()); 
  5.    
  6.   //colore da rendere trasparente 
  7.   Uint32 colorkey; 
  8.   //prendiamo la mappatura del colore #0000FF in base al nostro specifico formato di pixel 
  9.   colorkey = SDL_MapRGB(The_Frame->format, 0, 0, 0xFF); 
  10.   //impostiamo il colore come trasparente 
  11.   SDL_SetColorKey(The_Frame, SDL_SRCCOLORKEY|SDL_RLEACCEL, colorkey); 
Il primo metodo è il costruttore, il quale si occupa di caricare l'immagine passata e di rendere trasparente lo sfondo di quest'ultima, per fare ciò utilizziamo il presupposto che l'immagine abbia uno sfondo di colore #0000FF.
  1.   //pausa dal prossimo frame 
  2.   The_Pause=pause; 
La seconda azione del costruttore è l'impostazione della pausa secondo il parametro passato.
  1. //distruttore, liberiamo la superficie necessaria 
  2. frame::~frame() 
  3. {  
  4.   SDL_FreeSurface(The_Frame); 
Il distruttore è banale, in quanto si occupa solo di disallocare lo spazio riservato per l'immagine.

Compilazione

Per compilare il nostro sorgente è necessario utilizzare come segue il gcc:
g++ -s -o sprite main.cpp sprite.cpp frame.cpp `sdl-config --cflags --libs` -lSDL_image
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 tank.
L'uso di `sdl-config --cflags --libs` ci darà tutte le opzioni necessarie per includere ed utilizzare la libreria SDL, mentre -lSDL_image caricherà la libreria SDL_image.

Oppure è possibile sfruttare il Makefile digitando

make

Miglioramenti futuri

Il livello di questo programma è stato mantenuto davvero molto basso per permettere la scrittura di questa guida, ciò significa che c'è ancora molto lavoro da fare pere rendere le classi implementate utilizzabili all'interno di un'applicazione.
Di seguito riporto una lista di caratteristiche e migliorie che ho pensato di realizzare nelle implementazioni future:
  • Gestione di animazioni multiple a seconda dei tempi e degli eventi.
  • Gestione dello spostamento da un punto ad un altro con relativa animazione.
  • Gestione di operazioni grafiche sulle singole immagini dei frame.
  • Gestire le collisioni con relative animazioni.
  • Gestire ulteriori azioni grafiche sui frame.
  • Migliorare la gestione della trasparenza passando il colore da rendere trasparente.
  • Migliorare il passaggio dei nomi dei file delle immagini, magari passando un solo nome e lasciando alla classe sprite il compito di aggiungere la numerazione.
La lista è un punto di partenza per la realizzazione di classi utilizzabili, la pubblico insieme alla guida per dare uno spunto a chi voglia iniziare a lavorarci per i propri scopi.

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. Sorgenti dell'esempio.
    http://www.siforge.org/articles/2004/05/sdl_sprite/src.zip (59Kb)
  2. "C++ La guida completa", McGraw Hill
    http://www.catalogo.mcgraw-hill.it/catLibro.asp?item_id=1526
  3. Documentazione della STL: Standard Template Library Programmer's Guide
    http://www.sgi.com/tech/stl/
  4. Sito ufficiale del progetto SDL.
    http://www.libsdl.org
  5. Sito ufficiale del progetto SDL_Image.
    http://www.libsdl.org/projects/SDL_image/
  6. Gestire di immagini con SDL: SDL_Image.
    http://www.siforge.org/articles/2004/04/19-sdl_image.html
Discuti sul forum   Stampa

Cosa ne pensi di questo articolo?

Discussioni

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