Initialisation d'une fenêtre SDL et affichage d'une image

Écrit le 02/01/2004 par Kurim
Dernière mise à jour : 02/02/2006

Introduction

Dans ce premier tutorial sur SDL je vais vous apprendre à créer une fenetre simple dans laquel il sera possible de gérer de la 2D.

Pour ceux qui ne le savent pas, SDL est une API libre orienté vers le jeu qui permet la gestion du son, de l'input (clavier/joystick), du CD-ROM, des évenements exterieurs et du temps (ne croyez pas comme ca, c'est plutot bien de pouvoir gerer directement le temps grace a cette API). Elle gère aussi la vidéo en 2D exclusivement (donc, dans le but de faire un moteur 2D), et permet un fenetrage assez simple pour OpenGL (non traité ici) qui serait apparament plus performant que GLUT.
SDL ne dépend à priori d'aucune plateforme, elle est portable (même pour OpenGL), donc très interessante. De plus, son utilisation est assez simple et relativement efficace. Mais mettre en place un framework fonctionnel necessite quelques manipulations que nous allons voir ici. Ce tutorial explique comment démarrer avec Visual Studio 6 (la version que j'utilise). Je pense que la manip est la meme pour une autre version de VC.

Vous devrez télécharger les sources de SDL sur le site officiel de SDL. L'ensemble des sources pour Visual Studio est disponible dans la partie Download SDL.1.2 ou directement ici. Décompressez les fichiers, placez les .lib dans le dossier /lib de VC, et les fichiers include en .h dans /includes/sdl/ (créez le répertoire SDL). Vous pouvez les mettre directement dans le dossier include, mais si vous avez beaucoup d'api installé sur votre poste, ce dossier devrait devenir assez bordelique, c'est pourquoi personnellement je met les includes de chaque API que j'utilise dans un sous dossier. Notez que si vous créez un sous dossier SDL, dans votre code il faudra inclure les fichiers avec le chemin "SDL/sdl.h".

Création du projet

Lancez donc Visual Studio, et créez un nouveau projet en mode Win32 Console Application.

http://www.game-lab.com/images/tuts/sdl_affichage_image/new.jpg

Il y a deux manipulations à faire pour pouvoir compiler. Allez dans Project -> Settings puis dans l'onglet Link. Ajoutez comme librairie sdl.lib qui permet d'utiliser SDL ainsi que sdlmain.lib qui permet de créer une fonction main compatible avec SDL et portable.

http://www.game-lab.com/images/tuts/sdl_affichage_image/lib.jpg

Ceci fait, passez à l'onglet C / C++. Choisissez la catégorie Code Generation puis changez en Multithreaded DLL comme sur l'image :

http://www.game-lab.com/images/tuts/sdl_affichage_image/set.jpg

Maintenant, on va créer une fonction main standard, et compiler :

#include "sdl/SDL.h"

int main( int argc, char* argv[] )
{
    return 0;
}

Théoriquement, F7 devrait aboutir à un joli 0 error(s), 0 warning(s). Si ce n'est pas le cas, vérifiez que vous avez bien rempli toutes les étapes. Si c'est bon, on peut passer a la suite.

Création de la fenêtre

On a donc notre fonction main qui fonctionne. Première étape, on va initialiser SDL. La fonction qui se charge de ca est SDL_Init(). Voici sa syntaxe  :

int SDL_Init( Uint32 flags )

La fonction prend en argument un entier non-signé de 32 bits (soit 4 octets) qui lui indique ce qu'elle doit initialiser. Elle retourne -1 en cas d'erreur et 0 en cas de succès. Les differents flags possibles sont :
SDL_INIT_TIMER : Initialise le système de gestion du temps
SDL_INIT_AUDIO : Initialise le système de gestion du son
SDL_INIT_VIDEO : Initialise le système de gestion de la vidéo
SDL_INIT_CDROM : Initialise le système de gestion du CD-ROM
SDL_INIT_JOYSTICK : Initialise le système de gestion du joystick
SDL_INIT_EVERYTHING : Initialise les 5 systèmes en même temps.

Par exemple, pour initialiser seulement l'audio et la vidéo, il faudra passer en argument ceci :

SDL_Init( SDL_INIT_VIDEO | SDL_INIT_AUDIO );

Jusque la, c'est très simple. On va ajouter seulement l'initialisation de la vidéo pour le moment à notre fonction main, avec un test en cas d'échec.

#include "sdl/SDL.h"

int main( int argc, char* argv[] )
{
    if ( SDL_Init( SDL_INIT_VIDEO ) == -1 )
        return 0;

    return 0;
}

Bon jusque la, rien de bien compliqué, peut importe ce qu'il se passe, le programme quitte immédiatement. On va maintenant ajouter de quoi créer une fenêtre de rendu. Pour cela, il faut déja savoir comment fonctionne SDL. SDL utilise une structure, SDL_Surface, d'une certaine taille et d'un certain format. Dans chaque Surface, on peut agir sur les pixels qui la composent, comme y inserer une image ou simplement lui attribuer une couleur. SDL a besoin d'utiliser une surface qui aura la taille d'une fenetre, et donc les modifications seront directement visible à l'écran. Vous pouvez aussi utiliser les surfaces pour créer des petites portions d'image, et les fusionner a la surface de l'écran. On travaille sur cette structure exclusivement avec des pointeurs. Il existe des fonctions pour créer des surfaces, qui retourne un pointeur sur la surface crée afin de la manipuler par la suite, mais aussi, dans le cas qui nous interesse, une fonction qui créer une surface à afficher à l'écran, et retourne également un pointeur pour la manipuler. Cette fonction est :

SDL_Surface *SDL_SetVideoMode( int width, int height, int bpp, Uint32 flags )

Très simplement, on lui spécifie la largeur, la hauteur, le nombre de bits par pixel, et certains flags, et elle retourne un pointeur vers la surface ainsi créée qui correspond donc a l'image visible à l'écran. Voici les principaux flags qui peuvent vous interesser :

SDL_SWSURFACE : Créer la surface en mode logiciel
SDL_HWSURFACE : Créer la suface avec accéleration materielle
SDL_DOUBLEBUF : Active le double buffering. Nécessite d'etre en mode materiel. La fonction SDL_Flip() est alors à utiliser à chaque rendu de l'image
SDL_FULLSCREEN : Active la fenetre en plein écran.
SDL_OPENGL : Créer une fenetre OpenGL, non traité dans ce tutorial
SDL_RESIZABLE Créer une genetre redimensionnable. Si la taille est changé, un evenement SDL_VIDEORESIZE est appelé (voir dans un prochain tutorial la gestion des evenements), et on peut alors réutiliser la fonction SDL_SetVideoMode() pour recréer la surface avec la bonne taille.
SDL_NOFRAME : Créer une fenetre sans barre de titre ni bordure (si possible). Automatiquement actif en mode plein écran.

Derniere chose à savoir avant de l'appliquer à notre code : au même titre qu'SDL a besoin d'etre initialisé, il faut aussi de préference le quitter proprement. Pour cela, on utilise la fonction SDL_Quit(). Sachant cela, on peut donc y aller : on va créer une fonction, Init, qui se charge de mettre en route SDL et qui retourne la suface d'écran. En cas de problème, on quitte, sinon, on créer une boucle infinie :

#include "sdl/SDL.h"

SDL_Surface    *Screen;

int Init( void )
{
    if ( SDL_Init( SDL_INIT_VIDEO ) == -1 )
    {
        printf( "Echec lors de l'initialisation de la vidéo : %s", SDL_GetError() );
        SDL_Quit();
    }
    else
    {
        if ( Screen = SDL_SetVideoMode( 640, 480, 16, SDL_HWSURFACE | SDL_DOUBLEBUF ) )
            return 1;
        else
        {
            printf( "Echec lors de la création de la fenetre : %s", SDL_GetError() );
            SDL_Quit();
        }
    }
    return 0;
}

void Frame( void )
{
    while (1)
    {
        // Code pour l'affichage de chaque frame
    }
}

int main( int argc, char* argv[] )
{
    if ( Init() )
    {
        Frame();
    }
    
    return 0;
}

Et voila. Le code est assez simple a comprendre, la fonction Init retourne 1 en cas de succes, 0 en cas d'erreur, et execute les 2 fonctions necessaire a la mise en place de la fenetre. Ici la résolution est de 640x480 en 16 bits. J'ai créer une fonction Frame pour la boucle infinie afin de pouvoir y inserer du code pour l'affichage d'une image, ce que l'on va essayer de faire dans la partie suivante.

Affichage d'une image

C'est le moment de lancer Paint. Créez une nouvelle image en 640x480, et laissez libre cours à votre créativité pour pouvoir sauvegarder un magnifique bmp que vous placerez dans le dossier de votre projet et que vous nommerez bg.bmp. On va créer une nouvelle fonction chargé de poser l'image dans notre fenetre. Pour cela, on va avoir besoin de 3 nouvelles fonctions SDL et d'une nouvelle structure. La structure est SDL_Rect, elle contient 2 entiers codé sur 16 bits ( Sint16 ) qui contiennent l'origine (x et y), et 2 entier non signé sur 16 bits aussi, Uint16, qui contiennent la largeur et la hauteur du rectangle (w et h). Cette structure sert à définir un cadre sur la surface pour faire des manipulations. Bien qu'on puisse, dans notre cas, s'en passer, on va quand meme l'utiliser pour voir comment on s'en sert. Mais commençons par le commencement, il faut déja charger l'image du fichier. Rien de plus simple, la fonction SDL_LoadBMP( char* path ) prend en argument le chemin de l'image et retourne un pointeur sur un SDL_Surface. On peut donc charger l'image comme ceci :

int DrawBackground( void )
{
    SDL_Surface *Background = SDL_LoadBMP( "bg.bmp" );

    // Si l'image n'a pas été trouvé, on retourne 0
    if (Background == NULL)
        return 0;
}

Très simple donc, on va donc maintenant s'occuper de fusionner cette image avec la surface de l'écran. Comme je l'ai dit, on va créer un rectangle, qui sera de taille 640x480, qui aura pour origine 0,0. Comme notre image fait exactement la taille de la fenetre, ce rectangle represente la totalité du BMP, mais aussi la totalité de la fenetre. C'est pour cela que dans notre cas c'est facultatif, je vais expliquer tout de suite mais voyons déja la fonction :

int SDL_BlitSurface(SDL_Surface *src, SDL_Rect *srcrect, SDL_Surface *dst, SDL_Rect *dstrect);

Cette fonction opère un Blit, c'est a dire la fusion d'une portion d'une surface, sur une portion d'une autre surface. Le premier argument, SDL_Surface *src, represente la surface à utiliser pour prelever une portion d'image a fusionner. Le deuxieme parametre, SDL_Rect *srcrect, définit un rectangle qui sera la portion d'image à prelever. Si on passe NULL pour ce parametre, la surface source est copié en entier. (On peut donc s'en passer pour notre cas, car on veut bien copier le BMP en entier). Pour les 3ème et 4ème parametre, c'est pareil. SDL_Surface *dst est la surface cible dans laquel on va coller la portion d'image prelevé. Enfin, SDL_Rect *dstrect est le cadre cible. Comme la largeur et la hauteur de la portion d'image sont déja défini dans le rectangle source, les variables w et h de ce cadre sont ignorés. Il ne sert donc qu'a définir l'origine (x, y) de la portion d'image. Si NULL est envoyé pour ce parametre, l'origine utilisé sera (0,0). C'est également l'origine que l'on souhaite utiliser, et c'est donc pour cela qu'on peut se passer d'utiliser un SDL_Rect. Mais on va quand meme le faire, car c'est un tutorial et non pas un programme optimisé qu'on est en train de faire. Remarquez que les SDL_Rect en argument sont des pointeurs, il faudra donc utiliser & pour les passer. Dernière chose qui peut etre utile a savoir, en cas de succes la fonction retourne 0, et en cas d'échec -1. Mais complétons déja notre fonction :

int DrawBackground( void )
{
    SDL_Surface *Background;
    SDL_Rect rect;

    Background = SDL_LoadBMP( "bg.bmp" );
    if (!Background)
        return 0;

    // On rempli notre rectangle, origine 0, taille 640x480
    rect.x = rect.y = 0;
    rect.w = 640;
    rect.h = 480;

    SDL_BlitSurface( Background, &rect, Screen, &rect);
    SDL_Flip( Screen );

    SDL_FreeSurface( Background );

    return 1;
}

Remarquez la derniere fonction, dont je n'ai pas encore parler mais qui est tout de même très importante : SDL_Flip(). Elle prend en parametre la surface utilisé pour le rendu à l'écran. Comme on travaille en double buffering, elle execute simplement une inversion des buffers et affiche a l'écran ce qui a été traité. Dans le cas où l'on serait en simple buffering, cette fonction execute simplement la fonction qui valide une modification de l'image de l'écran. Je vais quand meme en parler, il s'agit de SDL_UpdateRect() :

void SDL_UpdateRect(SDL_Surface *screen, Sint32 x, Sint32 y, Sint32 w, Sint32 h);

Cette fonction prend en premier argument la surface de l'écran, puis 4 valeurs qui définissent un cadre à mettre a jour. Dans notre cas, on mettrait a jour la surface entiere. Notez que si x, y, w, h sont passé à 0, la fonction met a jour la surface entière de l'écran. C'est ce que fait automatiquement SDL_Flip(). Le code ci-dessus pourrait donc etre remplacé par :

SDL_BlitSurface( Background, &rect, Screen, &rect);
SDL_UpdateRect( Screen, 0, 0, 0, 0 );

Attention ! SDL_Flip fonctionne en double et en simple buffering, mais SDL_UpdateRect ne fonctionne qu'en simple buffering ! Théoriquement, si vous voulez coder un jeu vous essayerez forcement d'utiliser le double buffering, je vous conseille donc d'utiliser toujours SDL_Flip().

SDL_FreeSurface efface simplement de la memoire la surface crée temporairement pour charger l'image. Voyons maintenant notre programme en entier :

#include "sdl/SDL.h"

SDL_Surface    *Screen;

int Init( void )
{
    if ( SDL_Init( SDL_INIT_VIDEO ) == -1 )
    {
        printf( "Echec lors du chargement de la vidéo : %s", SDL_GetError() );
        SDL_Quit();
    }
    else
    {
        if ( Screen = SDL_SetVideoMode( 640, 480, 16, SDL_HWSURFACE | SDL_DOUBLEBUF ) )
            return 1;
        else
            SDL_Quit();
    }
    return 0;
}

int DrawBackground( void )
{
    SDL_Surface *Background;
    SDL_Rect rect;

    Background = SDL_LoadBMP( "bg.bmp" );
    if (!Background)
        return 0;

    rect.x = rect.y = 0;
    rect.w = 640;
    rect.h = 480;

    SDL_BlitSurface( Background, &rect, Screen, &rect );
    SDL_Flip( Screen );

    SDL_FreeSurface( Background );

    return 1;
}
void Frame( void )
{

    if (DrawBackground() == 0)
        return;

    while (1)
    {

    }
}

int main( int argc, char* argv[] )
{
    if ( Init() )
    {
        Frame();
    }
    
    return 0;
}

Vous remarquez que l'on ne dessine pas l'image dans la boucle, mais avant celle ci. La raison est que l'image ne changera pas durant la boucle ; il n'est donc pas nécessaire de remettre à jour a chaque fois. Voici le résultat que vous devriez obtenir :

http://www.game-lab.com/images/tuts/sdl_affichage_image/sdl1.jpg

Cependant, il y a 2 soucis dans ce programme : Si on déplace la fenetre, l'image aura quand meme besoin d'etre redessiner. De plus, rien ne permet de quitter proprement votre programme. On va donc brievement ajouter la gestion de ces 2 évenements. Je ne vais pas trop détailler la dessus, c'est juste histoire d'avoir un programme propre. Voici donc comment modifier la fonction Frame() :

void Frame( void )
{
    SDL_Event event;

    if (DrawBackground() == 0)
        return;

    while (1)
    {

      SDL_WaitEvent(&event);

        switch (event.type)
        {
            case SDL_VIDEOEXPOSE:
                DrawBackground();
            break;
            case SDL_QUIT:
                SDL_Quit();
                return;
       }
    }
}

Donc brievement : le début se passe comme tout a l'heure. Sauf que désormais, dans la boucle, on demande a SDL d'attendre un evenement (avec SDL_WaitEvent). Dès qu'un evenement se passe, on execute le switch : le premier cas, SDL_VIDEOEXPOSE, indique que la fenetre a besoin d'etre redessiné. On réexecute alors la fonction DrawBackground(). L'autre évenement, SDL_QUIT, est appelé lorsque l'utilisateur veut fermer le programme. On ferme alors SDL et on interrompt la boucle. Si un autre évenement arrive, la boucle en while(1) relance l'attente d'un évenement.

Le code final

Je remet ici le code final que l'on a donc obtenu :

#include "sdl/SDL.h"

SDL_Surface    *Screen;

int Init( void )
{
    if ( SDL_Init( SDL_INIT_VIDEO ) == -1 )
    {
        printf( "Echec lors du chargement de la vidéo : %s", SDL_GetError() );
        SDL_Quit();
    }
    else
    {
        if ( Screen = SDL_SetVideoMode( 640, 480, 16, SDL_HWSURFACE | SDL_DOUBLEBUF ) )
            return 1;
        else
            SDL_Quit();
    }
    return 0;
}

int DrawBackground( void )
{
    SDL_Surface *Background;
    SDL_Rect rect;

    Background = SDL_LoadBMP( "bg.bmp" );
    if (!Background)
        return 0;

    rect.x = rect.y = 0;
    rect.w = 640;
    rect.h = 480;

    SDL_BlitSurface( Background, &rect, Screen, &rect );
    SDL_Flip( Screen );

    return 1;
}
void Frame( void )
{
    SDL_Event event;

    if (DrawBackground() == 0)
        return;

    while (1)
    {

      SDL_WaitEvent(&event);

        switch (event.type)
        {
            case SDL_VIDEOEXPOSE:
                DrawBackground();
            break;
            case SDL_QUIT:
            SDL_Quit();
            return;
       }
    }
}

int main( int argc, char* argv[] )
{
    if ( Init() )
    {
        Frame();
    }
    
    return 0;
}

Voila, j'espere que ce tutorial vous aura été utile pour démarrer sur SDL.