Premesse
Questa guida richiede una conoscenza discreta del C, siccome non è suo compito
spiegarvelo, nel caso non possedesse questa conoscenza acquistate un buon libro sull'argomento
(consiglio "Programmare in C Guida completa" di Peter Aitken, Bradley L. Jones edizioni
Apogeo
[3]).
Inoltre questa guida e 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.
SDL
SDL ovvero Simple DirectMedia Layer, è una libreria multipiattaforma (utilizzabile
su diversi sistemi, come ad esempio linux, MAC OS, windows, ed altri ancora) che permette
un accesso di basso livello alle periferiche multimediali, ovvero all'audio, al video 2D,
al video 3D tramite OpenGl, alla tastiera, al mouse, al joystick ed al cdrom, inoltre
fornisce funzioni per la gestione del tempo e dei thread.
Ovviamente SDL è utilizzata per lo sviluppo di applicazioni multimediali, ed in
particolar modo di giochi, come esempi possiamo indicare "Cube"
[4], "The battle for
Wesnorth"
[5] e svariati porting della Loki Games
[6].
La libreria può essere scaricata dal sito ufficiale
[7] insieme alla documentazione
in formato HTML e pagine di man, cosa che consiglio vivamente, vista la comodità di
consultazione di queste ultime.
Il nostro obiettivo
In questa guida vedremo come inizializzare correttamente SDL, spiegheremo
dei concetti base di SDL e vedremo alcune delle sue prime funzioni.
In particolare verrà creato un programma in cui viene inizializzato il sistema di SDL
e si crea una finestra vuota dalla quale si può uscire premendo i tasti Esc oppure clickando
con il mouse sulla x di chiusura finestra, inoltre sarà aggiunta la possibilità di passare
in modalità schermo intero e tornare alla modalità finestra con la semplice pressione
dello spazio.
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 nel prossimo paragrafo.
#include <stdio.h>
#include <stdlib.h>
#include <SDL/SDL.h>
#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 a una struttura SDL_Surface
SDL_Surface *screen;
//indichiamo la superficie screen come direttamente collegata al video
screen = SDL_SetVideoMode(DIM_H, DIM_V, 0,SDL_HWSURFACE|SDL_DOUBLEBUF);
if ( screen == NULL )
{
printf("Problemi con il settaggio dello schermo: %s\n", SDL_GetError());
return 1;
}
//variabile per il ciclo principale
int fine=0;
//memoria dello stato dello schermo (intero o a finestra)
int full_s=0;
//flag per SDL_SetVideoMode
Uint32 flags=SDL_HWSURFACE|SDL_DOUBLEBUF|SDL_FULLSCREEN;
//dichiariamo una struttura SDL_Event
SDL_Event event;
//game loop, ovvero il loop che viene eseguito finche` non si esce
while(!fine)
{
//SDL_WaitEvent attende il prossimo evento
SDL_WaitEvent(&event);
//premendo la x della finestra col mouse si esce
if ( event.type == SDL_QUIT )
fine = 1;
if ( event.type == SDL_KEYDOWN )
{
//ma si esce anche premendo Esc
if ( event.key.keysym.sym == SDLK_ESCAPE )
fine = 1;
if ( event.key.keysym.sym == SDLK_SPACE )
{
//premendo SPAZIO si passa in modalita` schermo intero
if(!full_s)
{
screen = SDL_SetVideoMode(DIM_H, DIM_V, 0,flags);
full_s++;
flags=SDL_HWSURFACE|SDL_DOUBLEBUF;
}
//premendolo ancora si torna in modalita` finestra
else
{
screen = SDL_SetVideoMode(DIM_H, DIM_V, 0,flags);
full_s--;
flags=SDL_HWSURFACE|SDL_DOUBLEBUF|SDL_FULLSCREEN;
}
}
}
}
return 0;
}
Spiegazioni del sorgente
Procediamo con l'analisi dettagliata del sorgente e con le spiegazioni circa
la struttura generale di SDL e le funzioni utilizzate.
#include <stdio.h>
#include <stdlib.h>
#include <SDL/SDL.h>
#define DIM_H 800
#define DIM_V 600
Nelle prime righe includiamo gli header necessari, oltre i classici stdio e stdlib
appare l'header necessario per utilizzare le funzioni della libreria SDL.
Alle righe 5 e 6 definiamo due costanti che useremo in seguito per indicare la risoluzione
dello schermo, in questo caso 800x600.
// 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;
}
In questo primo blocco di codice andiamo ad inizializzare SDL, ed in particolare
il sottosistema video.
Per fare ciò utilizziamo la function SDL_Init che prende come parametri una delle seguenti
costanti:
-
SDL_INIT_TIMER (inizializzazione sottosistema temporizzazione)
-
SDL_INIT_AUDIO (inizializzazione sottosistema audio)
-
SDL_INIT_VIDEO (inizializzazione sottosistema video)
-
SDL_INIT_CDROM (inizializzazione sottosistema cdrom)
-
SDL_INIT_JOYSTICK (inizializzazione sottosistema joystick)
-
SDL_INIT_NOPARACHUTE (impedisce a SDL di lanciare segnali fatali)
-
SDL_INIT_EVENTTHREAD (inizializzazione thread? - non documentata)
-
SDL_INIT_EVERYTHING (inizializzazione di tutti i sottosistemi)
ovviamente è possibile inizializzare più sottosistemi alla volta, utilizzando
l'operatore or, ovvero passando come parametro una lista del genere:
SDL_INIT_VIDEO|SDL_INIT_AUDIO|...altre const...
quindi, volendo inizializzare i sottosistemi video ed audio trasformiamo la chiamata alla
function in:
SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO);
La function SDL_Init ritorna -1 in caso di errore, 0 altrimenti, pertanto gestiamo
il caso di errore stampando a video la notifica dell'errore ed una breve descrizione del
problema, descrizione ottenuta grazie alla function SDL_GetError().
Una volta inizializzato correttamente un sottosistema è possibile utilizzarlo,
ovviamente non è possibile utilizzare i sottosistemi non inizializzati.
Nel caso fosse necessario inizializzare un sottosistema nel corso del programma, quindi
dopo aver già eseguito la SDL_Init, è possibile utilizzare la function SDL_InitSubSystem,
che prende come unico parametro le stesse costanti descritte per SDL_Init.
// all'uscita del programma esegui SDL_Quit per risistemare le cose
atexit(SDL_Quit);
Utilizzando la function del C atexit andiamo ad eseguire la function SDL_Quit ad
ogni uscita corretta dal nostro programma.
SDL_Quit si occupa di disattivare i sottosistemi attivati e di liberare le risorse allocate
dalla libreria.
Utilizzando atexit evitiamo di dover richiamare SDL_Quit prima di ogni punto di
uscita dal programma.
//dichiariamo un puntatore a una struttura SDL_Surface
SDL_Surface *screen;
//indichiamo la superficie screen come direttamente collegata al video
screen = SDL_SetVideoMode(DIM_H, DIM_V, 0,SDL_HWSURFACE|SDL_DOUBLEBUF);
if ( screen == NULL )
{
printf("Prolemi con il settaggio dello schermo: %s\n", SDL_GetError());
return 1;
}
Alla riga 22 dichiariamo un puntatore ad una struttura SDL_Surface, ovvero a una
superficie di SDL.
Le superfici sono la componente fondamentale nell'utilizzo di SDL, una superficie è ogni
cosa sulla quale si può disegnare e sulla quale possono essere disegnate anche altre superfici.
Anche lo schermo è considerato una superficie, e per renderlo utilizzabile (disegnabile) è necessario
assegnarlo ad una superficie precedentemente dichiarata, utilizzando la function SDL_SetVideoMode
come mostrato alla riga 25.
La function SDL_SetVideoMode prende in ingresso 4 parametri che sono, nell'ordine:
-
la componente orizzontale della risoluzione (DIM_H nel nostro caso)
-
la componente verticale della risoluzione (DIM_V nel nostro caso)
-
la profondità del colore (0 nel nostro caso)
-
una o più flag (SDL_HWSURFACE|SDL_DOUBLEBUF nel nostro caso)
ma andiamo ad analizzare questi parametri con maggiore dettaglio:
-
i primi due parametri rappresentano la risoluzione della nostra superficie, e
nel caso in cui ci troviamo ad operare con una finestra, anche la dimensione
della finestra.
-
la profondità del colore è un valore che può variare tra 8,16,24,32, valori che indicano
i bit per pixel. Utilizzando il valore 0 si utilizza la risoluzione settata al momento di
lanciare il programma nell'ambiente dell'utente.
-
i flag che si possono utilizzare sono i seguenti:
-
SDL_HWSURFACE: crea la superficie video in modalità hardware (ovviamente più performante)
-
SDL_SWSURFACE: crea la superficie video in modalità software (utilizzata nel caso di problemi
con la superficie hw)
-
SDL_ASYNCBLIT: incrementa le prestazioni su SMP, da non usare su mono processori
-
SDL_ANYFORMAT: se non è disponibile la profondità desiderata usa quella più simile
-
SDL_HWPALETTE: dà ad SDL l'accesso esclusivo alle palette, onde evitare problemi con SDL_SetColors
-
SDL_DOUBLEBUF: attiva il buffering doppio, in pratica permette la scrittura di un buffer su una
superficie con SDL_Flip, richiede SDL_HWSURFACE
-
SDL_FULLSCREEN: va in modalità schermo intero, se ciò non è possibile usa la risoluzione più
grande possibile
-
SDL_OPENGL: utilizza il rendering OpenGl
-
SDL_OPENGLBLIT: utilizza OpenGl e permette alpha channel
-
SDL_RESIZABLE: permette il ridimensionamento della finestra da parte dell'utente
-
SDL_NOFRAME: crea una finestra senza la barra superiore
SDL_SetVideoMode ritorna NULL in caso di errore, pertanto alla riga 27 controlliamo
questa eventualità e gestiamo l'errore.
//variabile per il ciclo principale
int fine=0;
//memoria dello stato dello schermo (intero o a finestra)
int full_s=0;
//flag per SDL_SetVideoMode
Uint32 flags=SDL_HWSURFACE|SDL_DOUBLEBUF|SDL_FULLSCREEN;
In queste righe dichiariamo ed iniziliazziamo due variabili (fine e full_s) che ci
serviranno nel ciclo principale per determinare se è stata raggiunta la condizione di uscita
(fine) e se ci troviamo o meno in modalità schermo intero.
Oltre alle prime due variabili dichiariamo ed inizializziamo la variabile flags che
useremo per cambiare la modalità dello schermo.
Da notare che la variabile viene dichiarata con il tipo Uint32 che è un tipo definito
all'interno di SDL. Gli altri tipi definiti sono riportati di seguito:
-
Uint8: un unsigned char
-
Uint16: unsigned integer a 16 bit (2 byte)
-
Uint32: unsigned integer a 32 bit (4 byte)
-
Uint64: unsigned integer a 64 bit (8 byte)
-
Sint8: un signed char
-
Sint16: signed integer a 16 bit (2 byte)
-
Sint32: signed integer a 32 bit (4 byte)
-
Sint64: signed integer a 64 bit (8 byte)
//dichiariamo una struttura SDL_Event
SDL_Event event;
Alla riga 41 dichiariamo una struttura SDL_Event, che è una struttura generica che
contiene al suo interno l'insieme di tutte le sottostrutture associate a tutti i possibili
eventi in SDL.
Per SDL, infatti, un evento è una qualsiasi azione eseguita dall'utente, dalla pressione di
un tasto allo spostamento del mouse.
Tutti gli eventi che si verificano nel corso del programma, sono inseriti in una coda di eventi
dalla quale possono essere prelevati e gestiti da funzioni apposite (come quella che vedremo
adesso).
//game loop, ovvero il loop che viene eseguito finche` non si esce
while(!fine)
{
Alla riga 44 facciamo partire il loop principale, il cosiddetto "game loop", che è il
ciclo che viene eseguito finchè non è terminato l'utilizzo del programma da parte dell'utente.
//SDL_WaitEvent attende il prossimo evento
SDL_WaitEvent(&event);
Alla riga 47 chiamiamo la function SDL_WaitEvent che mette il programma in attesa del
prossimo evento gestibile, ciò permette di non sprecare utilizzo di CPU durante lo scarso utilizzo
del programma.
Una volta chiamata, SDL_WaitEvent richiede alla coda degli eventi SDL il primo evento disponibile
e memorizza le informazioni necessarie nella struttura SDL_Event passata.
Nel nostro caso gestiremo come eventi la pressione dei tasti Esc e spazio ed il click sulla x di
uscita dalla finestra.
//premendo la x della finestra col mouse si esce
if ( event.type == SDL_QUIT )
fine = 1;
Alla riga 50 riconosciamo il click del mouse sulla x della finestra aperta e gestiamo
l'evento ponendo ad 1 il valore della nostra variabile fine, ovvero uscendo dal loop principale.
if ( event.type == SDL_KEYDOWN )
{
Alla riga 53 riconosciamo la pressione di un tasto, siccome dobbiamo riconoscere la
pressione di più tasti, dobbiamo addentrarci in un altro livello di if.
//ma si esce anche premendo Esc
if ( event.key.keysym.sym == SDLK_ESCAPE )
fine = 1;
il primo tasto che andiamo a riconoscere è Esc, quando l'utente lo preme settiamo
a 1 la variabile fine, quindi usciamo.
Per capire bene come è strutturata la struttura SDL_event e le sue sottostrutture utilizzate
nel riconoscimento del tasto premuto consiglio di eseguire man SDL_Event, SDL_KeyboardEvent
e man SDL_Event, mentre per conoscere i codici associati ai tasti man SDLKey.
if ( event.key.keysym.sym == SDLK_SPACE )
{
//premendo SPAZIO si passa in modalita` schermo intero
if(!full_s)
{
screen = SDL_SetVideoMode(DIM_H, DIM_V, 0,flags);
full_s++;
flags=SDL_HWSURFACE|SDL_DOUBLEBUF;
}
//premendolo ancora si torna in modalita` finestra
else
{
screen = SDL_SetVideoMode(DIM_H, DIM_V, 0,flags);
full_s--;
flags=SDL_HWSURFACE|SDL_DOUBLEBUF|SDL_FULLSCREEN;
}
Alla riga 59 riconosciamo la pressione del tasto spazio ed andiamo a gestire, nelle righe
successive, il passaggio di modalità video da finestra a schermo intero e viceversa grazie alla
variabile full_s che tiene traccia dello stato attuale.
Per spostarci da una modalità all'altra utilizziamo la function SDL_SetVideoMode che oltre
ad associare una superficie allo schermo serve anche a modificare le impostazioni di quest'ultimo
nel corso del programma.
La modifica avviene settando la variabile flag, con o senza la costante SDL_FULLSCREEN.
Una volta chiusi correttamente i vari blocchi possiamo uscire correttamente ed andare
a compilare il nostro sorgente.
Compilazione
Per compilare il nostro sorgente è necessario utilizzare come segue il gcc:
g++ -Wall -O2 -s -o SDL_primo sorgente.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_primo
`sdl-config --cflags --libs` ci darà tutte le opzioni necessarie per includere
ed utilizzare la libreria SDL.
Adesso abbiamo finalmente il nostro programma che apre una bella finestra nera!