-=(the3fold)=-

New Generation

Programmation de jeux avec SDL

SDL (Simple DirectMedia Layer) est une API (Application Program Interface) multimédia multi-plateforme libre. Certains vous diront que c'est un produit concurrent de DirectX, ce qui n'est pas tout à fait vrai. SDL est une API de bas niveau, et ne permet pas tout ce que DirectX permet. Mais si l'on prend en compte un certain nombre d'extension de SDL, il est vrai que les possibilités se rapprochent de celles offertes par DirectX.

Avant de commencer, quelques rappels sur les commandes qui pourront être utiles :

1 - Initialisation de SDL

Pour utiliser SDL, il faut tout d'abord inclure le fichier SDL.h. L'initialisation se fait à l'aide de la fonction SDL_Init qui prend en argument un drapeau indiquant les modules à initialiser. Ce drapeau peut-être une combinaison (à l'aide de l'opérateur | ) de : SDL_INIT_VIDEO, SDL_INIT_AUDIO, SDL_INIT_TIMER, SDL_INIT_CDROM, SDL_INIT_JOYSTICK qui initialisent chacun la partie de SDL correspondant. Il existe aussi le drapeau SDL_INIT_EVERYTHING qui initialise tout, SDL_INIT_NOPARACHUTE qui demande à SDL de ne pas gérer les erreurs et SDL_INIT_EVENTTHREAD qui crée un thread supplémentaire pour gérer les évenements.

Il existe aussi une fonction SDL_InitSubSystem qui utilise les même drapeaux et qui permet, une fois SDL initialisé, de rajouter un des modules non initialisé.

Une fois SDL initialisé, la fonction SDL_GetError permet de récupérer la dernière erreur produite par SDL sous forme de chaine de caractère.

Enfin, les fonction SDL_QuitSubSystem (qui prend pour paramètre les mêmes drapeaux que SDL_Init et SDL_InitSubSystem) et SDL_Quit permettent de fermer un ou tous les modules.

Je vous propose cet exemple qui illustre l'utilisation de ces fonctions :

#include <SDL/SDL.h>
#include <stdio.h>

int main()
{
   printf("Initialisation de SDL.\n");

   if(SDL_Init(SDL_INIT_VIDEO) == -1)
   {
     printf("Impossible d'initaliser SDL : %s.\n", SDL_GetError());
     return -1;
   }

   printf("SDL est initialisé.\n");

   printf("Quitte SDL.\n");
   SDL_Quit();

   return 0;
}

Dans la mesure ou SDL_Quit() doit être appelée de manière systématique avant la terminaison du programme, il peut être judicieux d'insérer la ligne atexit(SDL_Quit); juste après l'initialisation de SDL. Il est aussi souvent interessant de placer toutes les initialisations dans une fonction séparée, ce qui n'est pas pour autant une raison d'abuser des variables globales.

2 - Dessiner à l'écran

Avant de pouvoir afficher quoi que ce soit à l'écran, il faut créer votre fenêtre. Cela se fait à l'aide de la fonction SDL_SetVideoMode qui renvoi un pointeur sur une surface représentant le contenu de la fenêtre. Les paramètres de cette fonction sont dans l'ordre : la largeur et la hauteur en pixel, le nombre de bits par pixel et les drapeaux. Parmi les drapeaux, on trouvera SDL_DOUBLEBUF qui permet d'avoir un backbuffer, SDL_ANYFORMAT qui permet d'utiliser une autre résolution que celle demandé si cela est nécessaire et SDL_FULLSCREEN qui permet d'avoir une fenêtre en plein écran. Il existe de nombreuses autres options. Je vous renvoi à la documentation de SDL pour plus de détails.

Une fois que nous avons récupéré notre surface principale, nous allons pouvoir dessiner dessus. Il est possible d'avoir un accès direct à la surface pour écrire pixel par pixel, mais ce n'est pas la technique la plus utilisée. Nous allons utiliser d'autres surfaces que nous positionnerons sur cette surface. Une des fonctions permettant de créer une surface est SDL_CreateRGBSurface. Elle renvoi un pointeur vers une surface vide (ou plutôt remplie de noir). Nous allons la remplir à l'aide de la fonction SDL_FillRect() qui permet de tracer un rectangle plein dans une surface.

Nous copions ensuite cette surface dans la surface principale à l'aide de la fonction SDL_BlitSurface qui copie une partie d'une surface dans une autre. Nous affichons ensuite le tout à l'écran à l'aide de la fonction SDL_Flip() qui permet d'afficher le résultat à l'écran. Cette fonction permute le backbuffer et la surface primaire si l'on à utilisé SDL_DOUBLEBUF, ou appelle la fonction SDL_UpdateRect sur la totalité de l'écran dans le cas contraire.

Voici les parties importantes de cet exemple :

SDL_Rect rect;
SDL_Surface *Screen, *rectangle;

void affichage()
{
   SDL_FillRect(rectangle, NULL, SDL_MapRGB(rectangle->format, 0, 255, 0));
   SDL_FillRect(Screen, NULL, 0);

   rect.x = (Screen->w / 2) - (rect.w / 2);
   rect.y = (Screen->h / 2) - (rect.h / 2);
   rect.w = Screen->w / 2;
   rect.h = Screen->h / 2;

   SDL_BlitSurface(rectangle, NULL, Screen, &rect);
   SDL_Flip(Screen);
}

void InitSDL()
{

...

   Screen = SDL_SetVideoMode (640, 480, 16, SDL_SWSURFACE | SDL_DOUBLEBUF);
   if (Screen == NULL)
   {
     fprintf (stderr, "Erreur d'initialisation du mode video: %s\n", SDL_GetError ());
     exit(-1);
   }
   rectangle = SDL_CreateRGBSurface(SDL_SWSURFACE, 320, 240, 32, 0, 0, 0, 0);
}

3 - Chargement d'images

Maintenant que nous savons comment afficher des surfaces à l'écran, il s'agit de savoir comment charger des choses interessantes dans des surfaces. Nous allons commencer par de simples images au format BMP. SDL fournit une fonction très pratique et simple d'utilisation : SDL_LoadBMP qui prend comme paramètre un nom de fichier, et renvoi une surface contenant l'image.

Voici un exemple d'utilisation de cette fonction :

SDL_Surface *image;

image = SDL_LoadBMP(fileName);
if(image == NULL)
{
   fprintf(stderr, "Impossible de charger %s : %s\n", fileName, SDL_GetError());
   return NULL;
}

Cette fonction est cependant très limité puisqu'elle ne permet pas de charger d'autres format que le format BMP. Heureusement, il existe une extension de SDL qui permet de charger n'importe quel type d'image : SDL_image. On peut alors utiliser la fonction IMG_Load exactement de la même façon que SDL_LoadBMP comme le montre cet exemple :

SDL_Surface *image;

image = IMG_Load(fileName);
if(image == NULL)
{
   fprintf(stderr, "Impossible de charger %s : %s\n", fileName, SDL_GetError());
   return NULL;
}

Une fois l'image chargée, il est possible de définir une couleur qui sera transparente, c'est à dire qui ne sera pas affichée lors de la copie de la surface sur une autre. Cela se fait à l'aide de la fonction SetColorKey() qui prend comme arguments la surface, un drapeau qui sera placé à SDL_SRCCOLORKEY et la couleur. Il existe aussi la fonction SetAlpha() qui permet de définir un taux d'opacité pour la surface. Ici encore, les arguments sont : la surface, un drapeau placé à SDL_SRCALPHA et la valeur de la transparence entre 0 (totalement transparent) et 255 (totalement opaque)

4 - Ecriture de texte

SDL ne permet pas d'écrire de texte directement sur une surface, contrairement à ce qu'on pourrait faire avec DirectX. Par contre, il existe une extension de SDL appelé SDL_ttf qui permet d'obtenir une surface contenant du texte. Cette extension doit être installée en supplément de SDL.

Il faut d'abord bien penser à inclure le fichier SDL_ttf.h. Il faut ensuite rajouter quelques lignes à notre fonction d'initialisation de SDL. On peut alors utiliser la fonction TTF_OpenFont pour charger une police, puis les fonctions TTF_RenderText_Solid, TTF_RenderText_Shaded et TTF_RenderText_Blended pour créer une surface contenant le texte.

La première fonction permet d'obtenir du texte aliasé sur un fond transparent. La deuxième méthode produit un texte anti-aliasé sur un fond de couleur. La troisième méthode produit un texte anti-aliasé sur un fond transparent avec un canal alpha.

Voici un comparatif de ces trois méthodes :

MethodeCréationBlitFondQualité
TTF_RenderText_SolidRapideRapideTransparentMédiocre
TTF_RenderText_ShadedLenteRapideColoréBonne
TTF_RenderText_BlendedLenteLenteTransparentBonne

Voici le type de code qu'il faut rajouter pour créer une surface contenant "Hello World !". Je vous propose un exemple similaire. Notez que si vous utilisez beaucoup de texte, il peut être judicieux de ne pas charger/fermer la police à chaque fois, mais de la conserver en mémoire tant que vous en avez besoin.

#include <SDL/SDL.h>
#include <SDL/SDL_ttf.h>

...

void SDL_Init()
{
...

   if (TTF_Init() < 0)
   {
     fprintf(stderr, "Impossible d'initialiser SDL_TTF: %s\n",SDL_GetError());
     exit(-1);
   }
}
...

   TTF_Font *font = TTF_OpenFont("babelfish.ttf", 40);
   if (!font)
     fprintf(stderr, "Impossible de charger la taille %dpt depuis %s: %s\n", font_size, font_face, SDL_GetError());

   SDL_Surface *text = TTF_RenderText_Shaded(font, "Hello World !", fgColor, bgColor);
   if (text==NULL)
     fprintf(stderr, "Impossible de créer la surface contenant le texte : %s\n", SDL_GetError());

   TTF_CloseFont(font);

Dans ce code, fgcolor et bgcolor sont des variables de type SDL_Color représentant respectivement la couleur du texte et la couleur du fond de la texture.

5 - Une classe pour des animations

Nous allons illustrer nos connaissances en créant une classe en C++ permettant de charger et d'afficher assez facilement des animations. Nous proposerons dans un premier temps deux constructeurs : l'un chargeant les images à partir de fichiers, l'autre prenant des surfaces existantes. Nous proposerons aussi quelques methodes permettant de traiter d'un seul coup toute l'animation plutôt que d'avoir à traiter séparement chaque image. Voici le squelette de notre classe :

class animPict
{
public:
   animPict(int num, char **fichiers, int delay);
   animPict(int num, SDL_Surface **images, int delay);
   ~animPict();
   void setColorKey(Uint32 flags, Uint32 key);
   void setAlpha(Uint32 flags, Uint32 alpha);
   SDL_Surface *getPicture() { animer(); return Pictures[animPos % nbPict]; };
   void animer();

private:
   SDL_Surface **Pictures;
   int nbPict;
   int animPos;
   int delay;
   Uint32 nextTime;
};

Notre classe est donc composée d'un tableau de surfaces, l'une d'entre elles étant la surface courante (désignée par animPos). Le delai entre les images peut-être défini pour chaque animation. On peut regler certains parametres (couleur transparente, canal alpha) de la même manière que sur une simple surface.

Le principe de fonctionnement est donc de créer une animation à l'aide d'un des deux constructeurs, puis d'appeler la fonction getPicture dans la routine d'affichage pour obtenir la bonne image. Vous pouvez télécharger la classe avec un programme d'exemple.

A suivre ...