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.
#include <stdio.h>
#include <stdlib.h>
#include <SDL/SDL.h>
void Put_Pixel(SDL_Surface *surface, int x, int y, Uint8 R, Uint8 G, Uint8 B);
void Draw_Screen(SDL_Surface *screen, int color);
#define DIM_H 800
#define DIM_V 600
int main()
{
// si inizializza il sistema video
if( SDL_Init(SDL_INIT_VIDEO) <0 )
{
// SDL_GetError() ritorna una descrizione dell'errore
printf("Errore init SDL: %s\n", SDL_GetError());
return 1;
}
// all'uscita del programma esegui SDL_Quit per risistemare le cose
atexit(SDL_Quit);
//dichiariamo un puntatore ad una superficie ed un int a 32 bit per i flag necessari
SDL_Surface *screen;
Uint32 flags;
//SDL_VideoInfo da informazioni sulla scheda video
const SDL_VideoInfo *info_hw;
//otteniamo le informazioni sul video
info_hw=SDL_GetVideoInfo();
//verifichiamo se possiamo creare una superficie hardware, altrimenti software
if ( info_hw->hw_available )
flags = SDL_HWSURFACE;
else
flags = SDL_SWSURFACE;
//aggiungiamo il buffer doppio
flags|=SDL_DOUBLEBUF;
// per rendere disegnabile la Superficie bisogna utilizzare SDL_SetVideoMode
if(!( screen = SDL_SetVideoMode(DIM_H, DIM_V, 0,flags) ))
{
printf("Problemi con il settaggio dello schermo: %s\n", SDL_GetError());
return 1;
}
//variabile per il ciclo principale
int done=0;
//indice del colore 0 rosso, 1 verde , 2 blu
int color=0;
//dichiariamo una struttura SDL_Event
SDL_Event event;
//game loop, ovvero il loop che viene eseguito finche` non si esce
while(!done)
{
// SDL_WaitEvent attende il prossimo evento
SDL_WaitEvent(&event);
//premendo la x della finestra col mouse si esce
if ( event.type == SDL_QUIT )
done = 1;
if ( event.type == SDL_KEYDOWN )
{
//ma si esce anche premendo Esc
if ( event.key.keysym.sym == SDLK_ESCAPE )
done = 1;
//premendo la freccia GIU` ci spostiamo al colore precendete
if ( event.key.keysym.sym == SDLK_DOWN)
{
//se abbiamo raggiunto l'inizio degli indici torniamo all'ultimo
if(!color)
color=2;
else
color--;
}
//premendo la freccia SU ci spostiamo al prossimo colore
if ( event.key.keysym.sym == SDLK_UP)
{
//se abbiamo raggiunto la fine degli indici torniamo a 0
if(color==2)
color=0;
else
color++;
}
//premendo spazio si passa dalla modalita` schermo intero alla modalita`
//finestra e viceversa
if ( event.key.keysym.sym == SDLK_SPACE )
{
flags^=SDL_FULLSCREEN;
screen = SDL_SetVideoMode(DIM_H, DIM_V, 0,flags);
}
}
//coloriamo lo schermo
Draw_Screen(screen,color);
//SDL_Flip disegna il buffer direttamente sullo schermo
SDL_Flip(screen);
}
return 0;
}
//disegna un pixel sullo schermo puntato da screen alle coordinate (x,y) con colori R,G,B
void Put_Pixel(SDL_Surface *surface, int x, int y,Uint8 R, Uint8 G, Uint8 B)
{
//restituisce la mappatura di un colore RGB in base ai bpp (alla definizione)
Uint32 color = SDL_MapRGB(surface->format, R, G, B);
//byte per pixel
int bpp = surface->format->BytesPerPixel;
//pixel della superficie da colorare
Uint8 *p = (Uint8 *)surface->pixels + y * surface->pitch + x * bpp;
//verifichiamo il numero di byte usati per la nostra superficie
switch (bpp)
{
case 1: // 1 byte => 8-bpp
*p = color;
break;
case 2: // 2 byte => 16-bpp
*(Uint16 *)p = color;
break;
case 3: // 3 byte => 24-bpp
if(SDL_BYTEORDER == SDL_BIG_ENDIAN)
{
p[0] = (color >> 16) & 0xff;
p[1] = (color >> 8) & 0xff;
p[2] = color & 0xff;
}
else
{
p[0] = color & 0xff;
p[1] = (color >> 8) & 0xff;
p[2] = (color >> 16) & 0xff;
}
break;
case 4: // 4 byte => 32-bpp
*(Uint32 *)p = color;
break;
}
}
// esegue la colorazione dello schermo
void Draw_Screen(SDL_Surface *screen, int color)
{
int l_c[3]={0,0,0};
int col;
if(color==0)
l_c[0]=1;
else if(color==1)
l_c[1]=1;
else if(color==2)
l_c[2]=1;
//blocchiamo lo schermo se necessario
if ( SDL_MUSTLOCK(screen) )
SDL_LockSurface(screen);
//disegnamo i pixel sul buffer screen precedentemente creato
for(int y=0;y<DIM_V;y++)
{
for(int x=0;x<DIM_H;x++)
{
col=(x/4)+25;
Put_Pixel(screen, x,y,col*l_c[0],col*l_c[1],col*l_c[2]);
}
}
//sblocchiamo lo schermo se necessario
if ( SDL_MUSTLOCK(screen) )
SDL_UnlockSurface(screen);
}
Spiegazioni del sorgente
Procediamo con l'analisi dettagliata del sorgente e con le spiegazioni circa
le nuove function e strutture utilizzate.
#include <stdio.h>
#include <stdlib.h>
#include <SDL/SDL.h>
void Put_Pixel(SDL_Surface *surface, int x, int y, Uint8 R, Uint8 G, Uint8 B);
void Draw_Screen(SDL_Surface *screen, int color);
#define DIM_H 800
#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).
// si inizializza il sistema video
if( SDL_Init(SDL_INIT_VIDEO) <0 )
{
// SDL_GetError() ritorna una descrizione dell'errore
printf("Errore init SDL: %s\n", SDL_GetError());
return 1;
}
// all'uscita del programma esegui SDL_Quit per risistemare le cose
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).
//dichiariamo un puntatore ad una superficie ed un int a 32 bit per i flag da assegnarle
SDL_Surface *screen;
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.
//SDL_VideoInfo da informazioni sulla scheda video
const SDL_VideoInfo *info_hw;
//otteniamo le informazioni sul video
info_hw=SDL_GetVideoInfo();
//verifichiamo se possiamo creare una superficie hardware, altrimenti la creiamo software
if ( info_hw->hw_available )
flags = SDL_HWSURFACE;
else
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.
//aggiungiamo il buffer doppio
flags|=SDL_DOUBLEBUF;
// per rendere disegnabile la Superficie bisogna utilizzare SDL_SetVideoMode
if(!( screen = SDL_SetVideoMode(DIM_H, DIM_V, 0,flags) ))
{
printf("Problemi con il settaggio dello schermo: %s\n", SDL_GetError());
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.
//variabile per il ciclo principale
int done=0;
//indice del colore 0 rosso, 1 verde , 2 blu
int color=0;
//dichiariamo una struttura SDL_Event
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.
//game loop, ovvero il loop che viene eseguito finche` non si esce
while(!done)
{
// SDL_WaitEvent attende il prossimo evento
SDL_WaitEvent(&event);
Dichiarate le variabili necessarie, possiamo cominciare il ciclo principale e chiamare
il gestore degli eventi SDL_WaitEvent.
//premendo la x della finestra col mouse si esce
if ( event.type == SDL_QUIT )
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.
if ( event.type == SDL_KEYDOWN )
{
//ma si esce anche premendo Esc
if ( event.key.keysym.sym == SDLK_ESCAPE )
done = 1;
Il secondo evento gestito è la pressione del tasto Esc, il quale comporta anch'esso
la terminazione del programma.
//premendo la freccia GIU` ci spostiamo al colore precendete
if ( event.key.keysym.sym == SDLK_DOWN)
{
//se abbiamo raggiunto l'inizio degli indici torniamo all'ultimo
if(!color)
color=2;
else
color--;
}
//premendo la freccia SU ci spostiamo al prossimo colore
if ( event.key.keysym.sym == SDLK_UP)
{
//se abbiamo raggiunto la fine degli indici torniamo a 0
if(color==2)
color=0;
else
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).
//premendo spazio si passa dalla modalita` schermo intero alla modalita`
//finestra e viceversa
if ( event.key.keysym.sym == SDLK_SPACE )
{
flags^=SDL_FULLSCREEN;
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.
//coloriamo lo schermo
Draw_Screen(screen,color);
//SDL_Flip disegna il buffer direttamente sullo schermo
SDL_Flip(screen);
}
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
//disegna un pixel sullo schermo puntato da screen alle coordinate (x,y) con colori R,G,B
void Put_Pixel(SDL_Surface *surface, int x, int y,Uint8 R, Uint8 G, Uint8 B)
{
//restituisce la mappatura di un colore RGB in base ai bpp (alla definizione)
Uint32 color = SDL_MapRGB(surface->format, R, G, B);
//byte per pixel
int bpp = surface->format->BytesPerPixel;
//pixel della superficie da colorare
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.
//verifichiamo il numero di byte usati per la nostra superficie
switch (bpp)
{
case 1: // 1 byte => 8-bpp
*p = color;
break;
case 2: // 2 byte => 16-bpp
*(Uint16 *)p = color;
break;
case 3: // 3 byte => 24-bpp
if(SDL_BYTEORDER == SDL_BIG_ENDIAN)
{
p[0] = (color >> 16) & 0xff;
p[1] = (color >> 8) & 0xff;
p[2] = color & 0xff;
}
else
{
p[0] = color & 0xff;
p[1] = (color >> 8) & 0xff;
p[2] = (color >> 16) & 0xff;
}
break;
case 4: // 4 byte => 32-bpp
*(Uint32 *)p = color;
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
// esegue la colorazione dello schermo
void Draw_Screen(SDL_Surface *screen, int color)
{
int l_c[3]={0,0,0};
int col;
if(color==0)
l_c[0]=1;
else if(color==1)
l_c[1]=1;
else if(color==2)
l_c[2]=1;
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.
//blocchiamo lo schermo se necessario
if ( SDL_MUSTLOCK(screen) )
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.
//disegnamo i pixel sul buffer screen precedentemente creato
for(int y=0;y<DIM_V;y++)
{
for(int x=0;x<DIM_H;x++)
{
col=(x/4)+25;
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.
//sblocchiamo lo schermo se necessario
if ( SDL_MUSTLOCK(screen) )
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.