Programmation ludique avec SDL

Ce tutoriel est paru dans Linux Magazine France, entre les numéros 65 et 73.

Tutoriel SDL: Introduction et graphisme de base

Paru dans Linux Mag 65, octobre 2004

Cette nouvelle série consacrée à SDL prend la suite des épisodes parus dans les numéros 48, 49 et 51 de Linux Mag. À travers ces articles, nous découvrirons comment utiliser les différentes fonctionnalités de SDL. Nous irons également au delà de l'utilisation d'une bibliothèque graphique pour aborder des éléments liés à la programmation des effets graphiques et des jeux 2D en général. Dans ces cas-là, SDL nous servira de support.

La bibliothèque Simple Directmedia Layer

SDL est une bibliothèque multi plate-formes qui offre une abstraction sur des éléments de programmation généralement considérés comme non-portables: vidéo, audio, entrées, évènements mais également threads, accès aux fichiers, et gestion du temps. Elle offre aussi des types de base standards quelle que soit l'architecture, des informations sur le système sur lequel tournent les applications, et un accès à OpenGL pour la programmation 3D. Ainsi, tout programme se servant correctement de SDL devrait compiler et se lancer sur tous les systèmes supportés par la bibliothèque: GNU/Linux bien sûr, mais aussi MS-Windows, MacOS, Free/OpenBSD, BeOS, etc... La plupart du temps, SDL utilise elle-même d'autres bibliothèques spécifiques aux systèmes sur lesquels elle tourne. Ainsi, sous GNU/Linux, on pourra choisir entre en rendu graphique par X-Window, DirectFB, aalib, ou d'autres. Cette portabilité ne se fait pas au détriment de la flexibilité puisque, comme nous allons le voir, SDL reste une bibliothèque d'assez bas niveau.

Non contente d'être portable entre les systèmes, SDL laisse également un choix impressionnant de langages pour l'utiliser. Écrite entièrement en C et compatible avec C++, elle dispose de bindings pour de nombreux langages, entre autres Perl, Python, Ruby, Ada et Java.

Enfin, SDL est dotée d'une importante collection de bibliothèques périphériques qui fournissent des fonctions d'un peu plus haut niveau: SDL_image, pour charger et sauvegarder des images dans un nombre impressionnant de formats, SDL_mixer pour facilement jouer et mixer sons et musiques, SDL_net qui offre des sockets réseau portables, etc. Vous pouvez retrouver la liste de ces bibliothèques sur le site officiel de SDL, http://www.libsdl.org.

À propos de ces articles...

Le but avoué de cette série est de donner les bases nécessaires pour commencer dans le développement de jeux. Cependant, pour ne pas tomber dans le piège de ne parler que d'un type de jeu en particulier, nous ne nous baserons pas sur un exemple que l'on construirait petit à petit à chaque nouvel article. Nous allons plutôt nous concentrer à chaque étape sur un aspect spécifique et indépendant, libre à vous ensuite d'assembler les éléments que nous aurons développés pour réaliser votre propre jeu (qui sera de toute façon bien plus intéressant que tout ce que nous aurions pu faire ici...).

Nous utiliserons le langage C car il permet d'appréhender au plus près SDL et sera accessible pour plus de lecteurs. Cette série ne donnera aucune notion de C ni d'algorithmique de base - elle s'adresse aux lecteurs familiarisés avec le C et les structures de données élémentaires comme les listes chaînées. Vous pouvez vous reporter à la série des « briques de bases en C » si vous manquez de connaissances en la matière. Nous tenterons d'être le plus modulaire possible. Chaque article sera dédié à la réalisation d'un ou de plusieurs « modules » qui pourra être connecté avec les autres pour réaliser un moteur de jeu. Pour commencer, nous parlerons un peu de quelques techniques d'affichage et de leur exploitation sous SDL.

Surfaces et formats d'image - un peu de théorie...

La structure C qui représente les éléments graphiques dans SDL s'appelle SDL_Surface. Cette structure est utilisée pour tout élément graphique, de l'écran aux images que nous aurons chargées. En effet, du point de vue de SDL, l'écran n'est qu'une surface presque comme une autre.

Une surface est définie entre autres par sa taille (longueur et hauteur en nombre de pixels), son format et ses données graphiques. Le format d'image est un élément important à prendre en compte. Il existe une multitude de manières de coder des images, que ce soit sur écran ou dans un fichier: on peut utiliser une palette et donner à chaque pixel une valeur qui référencera une des couleurs de cette palette (format palettisé type GIF) ; on peut aussi découper chaque pixel selon différents masques pour coder les valeurs de ses composantes rouge, verte et bleue (format en « couleurs vraies », comme le JPEG). Les formats palettisés conviennent aux images de peu de couleurs. En pratique de nos jours, bien peu de jeux utilisent encore ces formats qui étaient très populaires autrefois en raison du peu de mémoire vidéo qu'ils nécessitaient (1 octet par pixel pour un ensemble de 256 couleurs affichables simultanément). Nous travaillerons pour notre part en true color.

Il existe une multitude de formats true color. La plupart sont nommés en fonction du nombre de bits utilisés pour coder un pixel: Le mode 24 bits utilise 1 octet pour chacune des composantes rouge, verte et bleue d'un pixel, tandis que le mode 16 bits alloue 5 bits pour coder le rouge, 6 pour le vert et 5 pour le bleu.

Fig. 1: Utilisation des bits pour le codage des couleurs en formats 16 et 24 bits.
Fig. 1:
Utilisation des bits pour le codage des couleurs en formats 16 et 24 bits.

Cette générosité pour les nuances de vert en mode 16 bits s'explique par le fait qu'elles sont les plus évidentes à capter pour l'oeil humain. Les couleurs sont obtenues par mélange des trois composantes basiques (comme un peintre mélangerait ses couleurs pour en obtenir de nouvelles) et s'expriment en hexadécimal. Voici quelques couleurs et leurs expressions en 24 et 16 bits:

blanc
respectivement 0xffffff et 0xffff (tous les bits à 1),
noir
0x000000 et 0x0000 (tous les bits à 0),
bleu intense
0x0000ff et 0x001f (tous les bits de la composante bleue à 1, les autres à 0).

Le format « standard » pour exprimer les couleurs est le triplet hexadécimal 24 bits. Vous pouvez mélanger des couleurs et observer le triplet hexa correspondant avec l'outil palette de Gimp, par exemple.

Dessiner sur l'écran

La mémoire d'écran est une zone continue dans laquelle les pixels se suivent ligne par ligne. Ainsi pour une résolution de 640 * 480, le pixel (1, 0) se trouve juste après le (0, 0) et le pixel (0, 1) après le (639, 0). Connaissant le format utilisé par l'écran, modifier la couleur d'un pixel consiste donc à écrire dans la zone mémoire correspondante pour le mettre à la couleur désirée. La formule permettant de connaître l'offset d'un pixel (x, y) par rapport au début de l'écran est

(y x longueurEcran + x) x octetsParPixel

Ainsi, si screen est un pointeur de type Uint8 * (pointant des élément de 8 bits non-signés) vers le début d'une zone écran de dimensions 640 * 480 et utilisant un format de 16 bits par pixel, on pourra mettre le pixel aux coordonnées (50, 30) à la couleur blanche de la manière suivante:

*((Uint16 *)(screen + (30 * 640 + 50) * 2)) = 0xffff;

Dessiner une image complète sur l'écran consiste alors à copier chacun de ses pixels vers la mémoire d'écran. En général, le matériel fournit un support permettant de faire des copies rapides de blocs de mémoire, et c'est ce mécanisme qui est utilisé pour permettre de faire des affichages rapides. Cette opération de copie d'image d'une source vers une destination est appelée un blit. Mais bien évidemment, cette copie rapide ne peut se faire que si source et destination ont le même format d'image. Si l'on copiait directement une image 24 bits vers une mémoire d'écran de 16 bits, l'affichage serait totalement erroné. Pour avoir un affichage correct, il faudrait convertir un à un les pixels de la source vers du 16 bits avant de les copier. Vous l'aurez donc deviné, il est très lent de dessiner une image vers une surface qui ne partage pas le même format.

Lorsque l'on écrit dans la mémoire vidéo, l'affichage est immédiat, c'est à dire que les pixels modifiés apparaissent sous leur nouvelle couleur au prochain balayage de l'écran. Cela pose un gros problème dans le cas d'un jeu: imaginons qu'un rafraîchissement d'écran intervienne en plein milieu de l'affichage d'un sprite: durant un court instant, seule la moitié du sprite sera affichée!

La technique du double buffering permet de contourner ce problème.

Le double buffering

Cette technique consiste à avoir non pas une, mais deux zones d'écran: l'une représente l'écran affiché actuellement et l'autre le prochain écran en préparation. Dès que le nouvel écran est terminé, le pointeur de mémoire vidéo est modifié pour pointer sur celui-ci, et lors du prochain balayage vidéo le nouvel écran apparaît d'un coup. L'ancienne zone affichée devient alors la zone de travail dans laquelle est préparé le prochain écran à afficher, sur lequel re-pointera le pointeur de mémoire vidéo lorsqu'il sera terminé, et ainsi de suite.

Fig. 2: Utilisation du double buffering pour réaliser une animation: l'un des buffers est affiché tandis que l'image suivante est préparée dans l'autre.
Fig. 2:
Utilisation du double buffering pour réaliser une animation: l'un des buffers est affiché tandis que l'image suivante est préparée dans l'autre.

En plus de permettre un affichage sans faille, cette technique n'est absolument pas pénalisante (on ne change qu'un pointeur pour afficher la nouvelle image au moment ou l'écran effectue son retour de balayage vertical) en dehors du fait qu'elle nécessite plus de mémoire vidéo. Cependant le double buffering n'est vraiment applicable que pour un mode plein écran. Si l'application tourne dans une fenêtre, on ne peut modifier le pointeur de mémoire vidéo étant donné que notre application n'occupe qu'une partie de l'écran physique. Dans ce cas, le double buffering est simulé en copiant purement et simplement la zone de préparation vers la mémoire d'écran pour l'afficher, ce qui est un peu plus brutal et surtout plus lent.

Fig. 3: Simulation du double buffering par copie directe dans la mémoire d'écran.
Fig. 3:
Simulation du double buffering par copie directe dans la mémoire d'écran.

Le graphisme avec SDL

Voyons un peu comment tout cela se rapporte à SDL. Commençons par l'initialisation de l'écran, qui se fait par la fonction SDL_SetVideoMode.

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

Elle prend en paramètres les largeurs et hauteurs d'écran désirées en pixels, le nombre de bits par pixels voulu ainsi qu'un ensemble de flags, et nous retourne un pointeur vers la surface d'écran. Les flags les plus courants de cette fonction sont les suivants:

Une fois que l'écran est initialisé, il est possible de dessiner dessus et d'y copier d'autres surfaces.

Dessiner directement sur une surface

Pour dessiner directement sur une surface, il suffit d'affecter les bonnes valeurs aux bons endroits de la zone mémoire contenant ses pixels, en utilisant la formule décrite plus haut. En premier lieu, il faut donc obtenir un pointeur vers le début de l'image de la surface. Justement, le membre pixels de la structure SDL_Surface remplit ce rôle.

Toutefois, nous devons nous assurer que cette zone est modifiable en l'état. Pour cela, nous demanderons un verrou avant de toucher à quoi que ce soit avec la fonction SDL_LockSurface. Nous pourrons le libérer une fois nos manipulations terminées avec SDL_UnlockSurface. Durant la période de verrouillage, nous pouvons modifier comme bon nous semble le contenu de la mémoire pointée par pixels, en prêtant particulièrement attention au format d'image. La fonction putPixel de l'exemple ci-après se charge d'afficher un pixel en prenant soin du format de la surface de destination et de l'ordre des octets de la machine. Son seul impératif est que la couleur du pixel à afficher soit déjà au bon format. Comme nous manipulons par convention les couleurs au format 24 bits, il faudra convertir la couleur à afficher avant d'appeler cette fonction. Cette opération sera remplie par la fonction SDL_MapRGB qui prend en paramètre le format vers lequel convertir un triplet de couleurs (le format d'une surface est accessible via son membre format) ainsi que les valeurs des composantes rouge, verte et bleue de la couleur à convertir. Elle retourne un entier qui correspond à la couleur convertie.

En pratique, il est plutôt rare d'accéder directement aux pixels d'une surface. C'est indispensable pour appliquer des effets graphiques (nous aurons l'occasion d'en découvrir quelques uns par la suite), mais totalement inefficace pour les opérations courantes telles que copier une image vers l'écran. Au lieu de cela, nous utiliserons les fonctions fournies par SDL qui tireront avantage du matériel chaque fois que ce sera possible. Voyons donc maintenant comment afficher une surface à l'écran.

Affichage d'une surface sur une autre: le blit

Pour créer nos surfaces, nous nous contenterons pour le moment d'utiliser la fonction SDL_LoadBMP qui charge une image BMP et retourne la surface correspondante. Cependant, il n'est absolument pas garanti que cette surface retournée aura le même format que notre écran. Pour pouvoir profiter d'un blit rapide, il faut la convertir au format de notre surface d'écran, de la même manière que nous convertirions la couleur d'un pixel avec SDL_MapRGB. SDL_DisplayFormat prend en paramètre une surface et en renvoie une autre qui est sa copie convertie au format de l'écran. C'est donc cette dernière surface que nous allons utiliser.

Nous pourrons libérer la mémoire utilisée par les surfaces que nous aurons allouées (à l'exception de la surface d'écran) avec SDL_FreeSurface.

Enfin, le plus important: le blit! C'est comme son nom l'indique le rôle de SDL_BlitSurface.

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

Celle-ci prend en paramètres la surface source, l'adresse d'un SDL_Rect permettant de ne sélectionner qu'une partie de la surface source à dessiner, la surface destination et l'adresse d'un autre SDL_Rect qui indique la position à partir de laquelle copier l'image source.

Fig. 4: Rôle des paramètres de la fonction SDL_BlitSurface.
Fig. 4:
Rôle des paramètres de la fonction SDL_BlitSurface.

La structure d'un SDL_Rect est on ne peut plus simple: il s'agit d'un bête rectangle avec ses coordonnées x et y, sa longueur w et sa hauteur h.

typedef struct {
Sint16 x, y;
Uint16 w, h;
} SDL_Rect;

Si le paramètre srcrect est à NULL, l'image source est alors copiée en entier. Si le paramètre dstrect est à NULL, l'image source est copiée aux coordonnées (0, 0) de la destination. Dans tous les cas, les problèmes de clipping (le fait qu'une image source « dépasse » des dimensions de la surface destination) sont pris en charge par cette fonction.

Certains paramètres de l'image peuvent influencer le blit. Ainsi, une image peut être masquée, c'est à dire qu'une de ses couleurs ne sera pas copiée lors du blit. Cette couleur est appelée le masque et est choisie par la fonction SDL_SetColorKey. Une couleur de choix pour remplir cette tâche ingrate est la couleur 0xff00ff, qui correspond à une espèce de mauve infâme qu'aucun graphiste sain n'oserait utiliser dans ses compositions. :)

Une image peut également se voir appliquer un paramètre d'alpha, c'est à dire de transparence. Lors du blit, l'image apparaîtra translucide au dessus de la zone qu'elle aurait dû recouvrir. Ce paramètre est appliqué par la fonction SDL_SetAlpha. Il est dans ce cas global à toute l'image. Une autre solution pour obtenir de la transparence consiste à donner un canal alpha à chaque pixel de l'image, au même titre que les composantes rouge, verte et bleue. On parle alors de format d'image 32 bits (8 bits pour chacune des 3 composantes de base et 8 autres pour la composante alpha).

Le masque et l'alpha ont un coût sur l'opération de blit, comme vous devez vous en douter, mais bien souvent le matériel offre une accélération à ces opérations... quand il est possible de l'utiliser. Faute de quoi, SDL fait tout de manière logicielle.

D'autres fonctions ont également une importance capitale: SDL_Flip échange les buffers vidéos dans les cas où le double-buffering est utilisé, ou copie la surface tampon vers la mémoire d'écran dans les autres cas. C'est la fonction à appeler lorsque le double-buffering est utilisé ou que l'écran a changé dans sa totalité.

Une autre fonction de mise à jour de l'écran est SDL_UpdateRect:

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

Elle permet de ne mettre à jour qu'une partie de l'écran. Si seule une petite zone a été modifiée par rapport à l'écran précédente, cette fonction est bien plus rapide que SDL_Flip si le double-buffering n'est pas utilisé. Ses paramètres sont la surface d'écran, la position de la zone à mettre à jour ainsi que ses longueur et hauteur. Elle a une petite soeur nommée SDL_UpdateRects qui met, elle, une liste de SDL_Rects à jour:

void SDL_UpdateRects(SDL_Surface *screen, int numrects, SDL_Rect *rects);

Enfin, une autre fonction bonne à connaître est SDL_FillRect qui remplit une zone d'une surface avec une certaine couleur:

int SDL_FillRect(SDL_Surface *dst, SDL_Rect *dstrect, Uint32 color);

Les arguments sont utilisés de la même manière que SDL_BlitSurface, et la couleur doit être au format de la surface destination.

Un premier exemple

Les bases étant posées, terminons cet aperçu rapide par un exemple reprenant ce que nous avons vu. Inaugurons un test.c par les habituels en-têtes auxquels nous ajouterons celui de SDL:

#include <stdio.h>
#include <unistd.h>
#include "SDL.h"

Voici la définition de la fonction putPixel décrite plus haut, qui allume un pixel donné d'une surface:

void putPixel(SDL_Surface * surface, Uint16 x, Uint16 y, Uint32 color)
{
/* Nombre de bits par pixels de la surface d'écran */
Uint8 bpp = surface->format->BytesPerPixel;
/* Pointeur vers le pixel à remplacer (pitch correspond à la taille
d'une ligne d'écran, c'est à dire (longueur * bitsParPixel)
pour la plupart des cas) */
Uint8 * p = ((Uint8 *)surface->pixels) + y * surface->pitch + x * bpp;
switch(bpp)
{
case 1:
*p = (Uint8) color;
break;
case 2:
*(Uint16 *)p = (Uint16) color;
break;
case 3:
if (SDL_BYTEORDER == SDL_BIG_ENDIAN)
{
*(Uint16 *)p = ((color >> 8) & 0xff00) | ((color >> 8) & 0xff);
*(p + 2) = color & 0xff;
}
else
{
*(Uint16 *)p = color & 0xffff;
*(p + 2) = ((color >> 16) & 0xff) ;
}
break;
case 4:
*(Uint32 *)p = color;
break;
}
}

Remarquez l'utilisation du membre pitch de la structure SDL_Surface qui représente la taille exacte, en octets, d'une ligne de la surface.

Vient ensuite la fonction principale du programme. La première chose à faire dans tout programme utilisant SDL est bien entendu de l'initialiser:

int main(int argc, char * argv[])
{
SDL_Surface * screen;
SDL_Surface * image, * tmp;
SDL_Rect blitrect = {0, 0, 0, 0};
int i, j;
if (SDL_Init(SDL_INIT_VIDEO) == -1)
{
printf("Erreur lors de l'initialisation de SDL: %s\n", SDL_GetError());
return 1;
}

SDL_init prend en paramètre la liste des modules de SDL à initialiser (nous nous contentons de la vidéo pour le moment) et retourne -1 en cas d'erreur, dont nous pouvons récupérer le message détaillé avec SDL_GetError.

Ensuite, sélectionnons le mode vidéo en utilisant SDL_SetVideoMode:

#define SDL_VIDEO_FLAGS (SDL_HWSURFACE | SDL_DOUBLEBUF | SDL_ANYFORMAT)

screen = SDL_SetVideoMode(640, 480, 24,
SDL_VIDEO_FLAGS);

printf("Mode vidéo: dx%d\n", screen->w, screen->h,
screen->format->BitsPerPixel);

La surface d'écran retournée sera pointée par la variable screen. Elle sera de dimensions (640, 480), si possible dans la mémoire de la carte vidéo, utilisera le double buffering et un format de 24 bits par pixel, sauf si SDL juge qu'un autre format true color sera plus approprié (flag SDL_ANYFORMAT). Ce sera notamment le cas si cet exemple est lancé à partir d'un serveur X configuré en 16 bits. Pour éviter d'avoir à faire des conversions coûteuses à chaque affichage d'écran, SDL choisira le même format que celui du serveur. L'inconvénient du flag SDL_ANYFORMAT est que nous aurons en revanche un format d'image différent selon les machines sur lesquelles tournera le programme: il faudra en tenir compte!

Si la surface a été allouée correctement (valeur de retour différente de NULL), nous pouvons dessiner dessus! Commençons par la remplir de bleu, en utilisant SDL_FillRect:

     SDL_FillRect(screen, NULL, SDL_MapRGB(screen->format, 0x00, 0x00, 0xff));

Notez d'une part que le paramètre dstrect vaut NULL (utilisé pour demander un remplissage complet de la surface) et d'autre part l'utilisation de SDL_MapRGB pour convertir notre triplet hexa au format de l'écran (ici les composantes rouges et vertes sont à zéro, la bleue est au maximum: nous aurons un bleu intense).

Ensuite, la surface est verrouillée puis une boucle simple est utilisée pour afficher un pixel sur deux de l'écran en blanc. Cela créera une sorte d'effet de grille similaire à une matrice de calibrage. Ce n'est pas particulièrement beau mais se montrera utile pour remarquer les effets de blit - et puis, nous aurons toujours l'occasion de faire des choses plus esthétiques par la suite!

    SDL_LockSurface(screen);
for (j = 0; j < screen->h; j++)
for (i = j % 2; i < screen->w; i+=2)
putPixel(screen, i, j, SDL_MapRGB(screen->format, 0xff, 0xff, 0xff));
SDL_UnlockSurface(screen);

Une image est ensuite chargée, convertie au format de la surface d'écran, puis dessinée de 4 manières: brute, masquée, avec un paramètre d'alpha puis avec l'alpha et le masque.

    tmp = SDL_LoadBMP("image.bmp");
image = SDL_DisplayFormat(tmp);
SDL_FreeSurface(tmp);

/* Affichage sans masque ni alpha */
blitrect.x = (screen->w - (image->w * 4)) / 2;
blitrect.y = (screen->h - image->h) / 2;
SDL_BlitSurface(image, NULL, screen, &blitrect);

blitrect.x += image->w;
/* Sélection de la couleur 0xff00ff comme masque */
SDL_SetColorKey(image, SDL_SRCCOLORKEY, SDL_MapRGB(image->format, 0xff, 0x00, 0xff));
SDL_BlitSurface(image, NULL, screen, &blitrect);

blitrect.x += image->w;
/* Suppression du masque de l'image */
SDL_SetColorKey(image, 0, 0);
/* Alpha mis à 128 (soit 50% de transparence) */
SDL_SetAlpha(image, SDL_SRCALPHA, 128);
SDL_BlitSurface(image, NULL, screen, &blitrect);

blitrect.x += image->w;
/* Sélection de la couleur 0xff00ff comme masque */
SDL_SetColorKey(image, SDL_SRCCOLORKEY, SDL_MapRGB(image->format, 0xff, 0x00, 0xff));
SDL_BlitSurface(image, NULL, screen, &blitrect);

L'écran est enfin affiché en utilisant SDL_Flip. Le programme attend alors 5 secondes, puis se termine. L'appel à SDL_Quit libère la surface d'écran et finalise l'utilisation de SDL dans notre programme.

    SDL_Flip(screen);
sleep(5);
SDL_Quit();
return 0;
}

Vous pouvez recopier ce programme et le compiler en suivant les instructions qui vont suivre. Les sources ainsi qu'une image de test sont disponibles sur le CD-ROM ou sur http://www.gnurou.org/documents/linuxmag/SDL/SDL-1.tar.gz.

Compiler pour SDL

SDL offre un script facilitant grandement la compilation des programmes qui l'utilisent. Ce script, nommé sdl-config, est créé lors de la compilation de SDL et permet de fournir tous les paramètres nécessaires au compilateur pour compiler ou lier des programmes utilisant SDL. Il s'appelle avec les arguments --cflags pour avoir les arguments de compilation, et --libs pour la liaison.

Si votre project utilise autoconf/automake, la macro AM_PATH_SDL vous permettra de détecter la présence de la bibliothèque et de vérifier que la version installée est suffisamment récente. Elle affectera également aux macros SDL_CFLAGS et SDL_LIBS les paramètres supplémentaires à passer lors de la compilation et de l'édition des liens.

Notre petit exemple se compilera pour sa part de la manière suivante:

gcc `sdl-config --cflags --libs` test.c -o test

En le lançant, vous constaterez les effets du masque et de l'alpha: le masque permet de donner des contours souples à une image, l'alpha permet de voir au travers de l'image ce qui se trouve derrière. Vous pouvez remplacer le fichier image par un autre de votre choix, au format BMP.

La prochaine fois...

Les bases étant posées, nous passerons à des choses plus dynamiques pour les prochains épisodes: nous verrons une manière de faire des sprites animés avec SDL, de lire les entrées du clavier ainsi qu'un moyen de gérer le temps pour obtenir une vitesse de jeu cohérente sur toute machine.

Tutoriel SDL: Gestion des entrées

Paru dans Linux Mag 66, novembre 2004

Notre premier article constituait une introduction encore très incomplète à SDL. Ce mois-ci, nous allons la compléter en étudiant la gestion des entrées qui nous permettra de réaliser des programmes plus réactifs.

Le plus gros défaut de notre programme précédent est qu'il est strictement linéaire: il dessine quelques bricoles, attend 5 secondes, et se termine. Pour donner un peu plus de goût à nos futurs programmes ludiques, il est nécessaire de disposer d'une bonne interaction avec l'utilisateur.

Les entrées avec SDL

La gestion des entrées dans SDL est, comme le reste, d'assez bas niveau. Contrairement à des APIs comme GTK ou Qt, les entrées ne sont pas gérées par un mécanisme de callback où une fonction est automatiquement appelée lorsqu'un événement survient. Ici, c'est au programmeur de prendre l'initiative de regarder si quelque chose s'est passé et de réagir en conséquence. Cela se fera pour notre cas par un appel régulier à une fonction de gestion des entrées dans notre boucle de jeu.

L'utilisation des entrées nécessite qu'un mode vidéo soit activé. Il est donc inutile de s'en préoccuper tant que SDL_SetVideoMode n'a pas été appelé.

SDL stocke les évènements arrivants dans une file d'attente. La fonction SDL_PollEvent permet de retirer l'élément en tête de file:

int SDL_PollEvent(SDL_Event * event);

Elle retourne 1 si il y avait des évènements en attente, 0 si la file d'attente était vide. Dans notre fonction de gestion des entrées, nous allons l'appeler tant qu'elle n'est pas vide pour traiter les évènements qui sont survenus depuis notre dernier passage. Il existe plusieurs autres fonctions liées à la gestion de la file des évènements, notamment pour la regarder sans la vider ou ajouter arbitrairement un événement. N'étant pas primordiales dans le cadre de la programmation ludique, nous les ignorerons lors de nos articles.

Dans le cas où SDL_PollEvent retire un événement, ce dernier est placé dans la structure SDL_Event donnée en argument. SDL_PollEvent va retourner absolument tous les évènements susceptibles d'arriver: appui d'une touche clavier, mouvement de la souris, mais aussi redimensionnement de la fenêtre SDL ou réception du focus. Tous ces évènements sont stockables dans la structure SDL_Event qui n'est rien d'autre qu'une union de tous les évènements gérés par SDL. Pour savoir à quel type d'évènement on a affaire, on regardera son champ type. Voici quelques-unes de ses valeurs qui nous intéressent dans un premier temps:

SDL_KEYDOWN
appui d'une touche clavier
SDL_KEYUP
relâchement d'une touche clavier
SDL_MOUSEMOTION
mouvement de la souris
SDL_MOUSEBUTTONDOWN
appui d'un bouton de la souris
SDL_MOUSEBUTTONUP
relâchement d'un bouton de la souris

En fonction du type d'évènement, on accèdera au membre du SDL_Event correspondant au membre d'union qui correspond. Pour un évènement clavier, ce sera key, pour un mouvement de souris motion, ou pour un click de souris button.

Chacun de ces membres a lui-même sa propre structure, que nous allons découvrir superficiellement. Pour plus de détails, et pour connaître les évènements gérés par SDL mais non-liés aux entrées, vous pouvez vous reporter à sa documentation.

Évènements clavier

Comme dit précédemment, les informations associées à un événement clavier sont accessibles via le membre key du SDL_Event correspondant. Il est lui-même d'un type structure (SDL_KeyboardEvent) et son seul membre intéressant (outre type) est keysym. C'est en effet lui qui va nous renseigner sur quelle touche a été actionnée ou relâchée. Il s'agit encore d'une structure (SDL_Keysym) dont voici les membres les plus utiles:

sym
nous donne le code de la touche concernée. Il peut être testé avec les macros de type SDLK_NOMDETOUCHE pour connaître précisément quelle touche a été enfoncée ou relâchée. La liste complète des noms de touche est disponible dans le fichier SDL_keysym.h du répertoire d'include de SDL, ou dans la documentation.
mod
nous informe sur l'état des modifieurs (touches CTRL, ALT, etc.). Son intérêt étant limité dans notre cadre, nous ne ferons que le mentionner.
unicode
fournit une information d'un peu plus haut niveau et nous donne, si le mode unicode de SDL a été activé, le caractère unicode qui a été émis par l'appui de cette touche. L'information est de plus haut niveau car le caractère retourné tient compte de l'état des modifieurs (majuscule si SHIFT est enfoncé, par exemple) et de la localisation du clavier. Il peut être utile si vous avez à lire du texte via le clavier. Pour activer le mode unicode (désactivé par défaut à cause du traitement supplémentaire qu'il induit), il suffit d'appeler SDL_EnableUNICODE(1).

Gestion de la souris

Les évènements souris sont gérés par les structures SDL_MouseMotionEvent (nom de membre: motion) pour les déplacements et SDL_MouseButtonEvent (nom de membre: button) pour les clics de souris. Concernant SDL_MouseMotionEvent, ses membres intéressants sont:

x et y
qui donnent les coordonnées absolues de la souris dans le repère graphique correspondant à la zone d'écran.
xrel et yrel
qui donnent le déplacement du curseur de souris, par rapport à sa position initiale.

Pour la gestion des boutons de la souris et la structure SDL_MouseButtonEvent:

Le membre button indique quel bouton a été cliqué ou relâché. Les macros SDL_BUTTON_LEFT, SDL_BUTTON_MIDDLE, SDL_BUTTON_RIGHT, SDL_BUTTON_WHEELUP et SDL_BUTTON_WHEELDOWN) peuvent être utilisées pour tester les trois principaux boutons ainsi que la molette. Les éventuels boutons additionnels portent des numéros supérieurs à 5.

On retrouve aussi les membres x et y qui jouent un rôle similaire à ceux de SDL_MouseMotionEvent et permettent de connaître quelle zone d'écran a été cliquée.

Comme nous venons de le voir, la gestion des deux périphériques d'entrée principaux est relativement aisée. Cependant, nombreux sont les types de jeux dont le gameplay est multiplié s'ils sont joués avec un joystick ou un joypad. Comme il se doit, SDL nous offre une gestion très complète de ces périphériques.

Gestion du joystick

La gestion du joystick, incontournable pour beaucoup de jeux, est plus complexe. En effet, si les claviers et souris sont relativement standardisés, les joysticks sont eux très variés. Les différences incluent notamment le nombre et le type des axes, le nombre de boutons, la présence ou non d'un POV ou d'un trackball, etc.

Commençons par standardiser le langage: un axe sera un élément de type manche à balai (ou axes à potentiomètre), un chapeau pourra être soit le « hat » que l'on trouve en haut d'un joystick et qui sert à gérer le point de vue dans les simulateurs de vol, soit le pad d'un joypad (ou tout type d'axe à 3 positions: gauche, centré ou droite). Les boutons comprennent tout ce qui est pressable, et les trackballs sont les petites boules orientables.

Pour activer la gestion des joysticks dans SDL, il faudra ajouter le flag SDL_INIT_JOYSTICK lors de notre appel à SDL_Init, récupérer le nombre de joysticks connectés au système avec SDL_NumJoysticks et les ouvrir avec SDL_JoystickOpen (faute de quoi ils n'émettront pas d'évènements). Cette dernière fonction prend l'index du joystick à ouvrir (qui doit être inférieur à la valeur retournée par SDL_NumJoysticks) et retourne un pointeur vers un SDL_Joystick qui pourra être utilisé pour obtenir plus d'informations sur les capacités du périphérique. Ces informations s'obtiennent grâce à une série de fonctions dédiées:

SDL_JoystickName
prend un index de joystick et retourne sa chaîne d'identifiant,
SDL_JoystickNumAxes, SDL_JoystickNumButtons, SDL_JoyStickNumBalls et SDL_JoyStickNumHats
prennent quant à elles le pointeur retourné par SDL_JoystickOpen et retournent respectivement le nombre d'axes, de boutons, de trackballs et de chapeaux disponibles sur le joystick.

Une dernière fonction d'intérêt est SDL_JoystickClose qui permet de faire « taire » un joystick.

Une fois nos joysticks ouverts, ils enverront des évènements au même titre que les autres périphériques d'entrée. Un joystick est capable de générer quatre types d'évènements, selon que l'on a touché à ses axes, ses chapeaux, ses boutons ou ses trackballs.

Un mouvement d'axe sera représenté par un événement de type SDL_JOYAXISMOTION, accessible via le membre jaxis de SDL_Event. Ses membres sont:

which
le numéro du joystick qui a émis l'évènement (ce membre se retrouvera dans tous les évènements liés aux joysticks).
axis
le numéro de l'axe ayant bougé.
value
la position de l'axe, comprise entre -32767 (complètement à gauche/en haut) et 32767 (complètement à droite/en bas).

Un chapeau émettra quant à lui un évèmenent de type SDL_JOYHATMOTION, accessible par le membre jhat. Outre le membre which indiquant le joystick émetteur, on trouvera:

hat
qui indique le numéro du chapeau concerné par l'évènement.
value
qui comme pour les évènements liés aux axes donnera la position du chapeau. Le nombre de valeurs possibles étant limité, les valeurs se testeront avec les macros SDL_HAT_CENTERED, SDL_HAT_UP, SDL_HAT_RIGHT, SDL_HAT_DOWN, SDL_HAT_LEFT, ces macros étant composables par ou logique pour en obtenir d'autres: SDL_HAT_RIGHTUP, SDL_HAT_RIGHTDOWN, SDL_HAT_LEFTUP, SDL_HAT_LEFTDOWN. En toute logique, SDL_HAT_RIGHTUP sera égal à (SDL_HAT_RIGHT | SDL_HAT_UP).

Les trackballs émettront des évènements SDL_JOYBALLMOTION lisibles via jball, très proches d'un mouvement de souris:

ball
indique le numéro du trackball émetteur.
xrel et yrel
donnent les mouvements du trackball par rapport à leur dernière position. Notez que, contrairement aux évènements souris, on ne dispose pas de la position absolue.

Enfin, les évènements boutons se gèrent pratiquement comme des appuis sur des touches clavier. Deux types d'évènements leur sont associés, SDL_JOYBUTTONDOWN pour un appui et SDL_JOYBUTTONUP pour un relâchement, et les informations utiles sont accessibles via le membre jbutton de SDL_Event:

which
toujours présent.
button
indique le numéro du bouton appuyé ou relâché.

Le programme de ce mois-ci

Notre programme d'exemple met toutes ces notions en application. Il analyse tous les évènements d'entrée venant du clavier, de la souris ou des joysticks. Pour chacun de ces évènements, il fait un rapport détaillé sur la sortie standard. De plus, les touches directionnelles du clavier, les mouvements de la souris, ainsi que le déplacement des axes, chapeaux et trackballs des joysticks connectés permettent de diriger le petit sprite affiché à l'écran. La touche Escape permet de quitter le programme. Il se compile et se lance de la même manière que l'exemple du mois dernier. Remarquez les différentes façons de mettre à jour la position de notre sprite (soit par sa vélocité, soit directement par sa position absolue selon le type d'évènement). Comme d'habitude, le source complet ainsi que l'image utilisée sont disponibles sur le CD-ROM du magazine ou à l'adresse http://www.gnurou.org/documents/linuxmag/SDL/SDL-2.tar.gz

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

/* Variable permettant de quitter la boucle de jeu */
unsigned char letsexit = 0;
/* Vélocité du sprite */
Uint16 xvel = 0, yvel = 0;
/* Position du sprite */
SDL_Rect spritepos;

/* Gestion des évènements d'entrée */
void process_events()
{
SDL_Event event;
/* Un évènement attend d'être traité? */
while (SDL_PollEvent(&event))
{
/* Si oui, quel type? */
switch (event.type)
{
/* Appui sur une touche */
case SDL_KEYDOWN:
printf("Touche %d enfoncée (caractère produit: %c)\n",
event.key.keysym.sym,
event.key.keysym.unicode);
switch (event.key.keysym.sym)
{
case SDLK_ESCAPE:
letsexit = 1;
break;
case SDLK_LEFT:
xvel = -1;
break;
case SDLK_RIGHT:
xvel = 1;
break;
case SDLK_UP:
yvel = -1;
break;
case SDLK_DOWN:
yvel = 1;
break;
default:
break;
}
break;
/* Relâchement d'une touche */
case SDL_KEYUP:
printf("Touche %d relâchée\n", event.key.keysym.sym);
switch (event.key.keysym.sym)
{
case SDLK_LEFT:
case SDLK_RIGHT:
xvel =0;
break;
case SDLK_UP:
case SDLK_DOWN:
yvel = 0;
break;
default:
break;
}
break;
/* Déplacement souris */
case SDL_MOUSEMOTION:
spritepos.x += event.motion.xrel;
spritepos.y += event.motion.yrel;
printf("Position souris: (d)\n", event.motion.x,
event.motion.y);
break;
/* Enfoncement bouton souris */
case SDL_MOUSEBUTTONDOWN:
printf("Click bouton souris %d\n", event.button.button);
break;
/* Relâchement bouton souris */
case SDL_MOUSEBUTTONUP:
printf("Relâchement bouton souris %d\n", event.button.button);
break;
/* Déplacement axe joystick */
case SDL_JOYAXISMOTION:
printf("Axe d positionné à %d\n",
event.jaxis.axis, event.jaxis.which, event.jaxis.value);
/* Les axes pairs sont verticaux, les impairs horizontaux.
Les valeurs de jaxis.value étant très grandes, on compense
en faisant un décalage à droite (division par 2^13) */
if (event.jaxis.axis % 2)
yvel = event.jaxis.value >> 13;
else
xvel = event.jaxis.value >> 13;
break;
/* Déplacement chapeau joystick */
case SDL_JOYHATMOTION:
printf("Chapeau d en position ",
event.jhat.hat, event.jhat.which);
switch (event.jhat.value)
{
case SDL_HAT_CENTERED:
xvel = 0; yvel = 0;
printf("centrée\n");
break;
case SDL_HAT_UP:
xvel = 0; yvel = -1;
printf("haute\n");
break;
case SDL_HAT_RIGHT:
xvel = 1; yvel = 0;
printf("droite\n");
break;
case SDL_HAT_DOWN:
xvel = 0; yvel = 1;
printf("basse\n");
break;
case SDL_HAT_LEFT:
xvel = -1; yvel = 0;
printf("gauche\n");
break;
case SDL_HAT_RIGHTUP:
xvel = 1; yvel = -1;
printf("haute/droite\n");
break;
case SDL_HAT_RIGHTDOWN:
xvel = 1; yvel = 1;
printf("basse/droite\n");
break;
case SDL_HAT_LEFTUP:
xvel = -1; yvel = -1;
printf("haute/gauche\n");
break;
case SDL_HAT_LEFTDOWN:
xvel = -1; yvel = 1;
printf("basse/gauche\n");
break;
}
break;
/* Déplacement trackball joystick */
case SDL_JOYBALLMOTION:
spritepos.x += event.jball.xrel;
spritepos.y += event.jball.yrel;
printf("Trackball d a bougé de (d)\n",
event.jball.ball, event.jball.which,
event.jball.xrel, event.jball.yrel);
break;
/* Appui bouton joystick */
case SDL_JOYBUTTONDOWN:
printf("Bouton d enfoncé\n",
event.jbutton.button, event.jbutton.which);
break;
/* Relâchement bouton joystick */
case SDL_JOYBUTTONUP:
printf("Bouton d relâché\n",
event.jbutton.button, event.jbutton.which);
break;
default:
break;
}
}
}

#define SDL_VIDEO_FLAGS (SDL_HWSURFACE | SDL_DOUBLEBUF | SDL_ANYFORMAT)

int main(int argc, char * argv[])
{
SDL_Surface * screen;
SDL_Surface * sprite, * tmp;
int nbjoysticks;
int i;

if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK) == -1)
{
fprintf(stderr, "Erreur lors de l'initialisation de SDL: %s\n",
SDL_GetError());
return 1;
}

screen = SDL_SetVideoMode(640, 480, 24, SDL_VIDEO_FLAGS);
printf("Mode vidéo: dx%d\n", screen->w, screen->h,
screen->format->BitsPerPixel);

/* Activation du support UNICODE */
SDL_EnableUNICODE(1);

nbjoysticks = SDL_NumJoysticks();
printf("Nombres de joysticks attachés: %d\n\n", nbjoysticks);

for (i = 0; i < nbjoysticks; i++)
{
SDL_Joystick * joy = SDL_JoystickOpen(i);
printf("Joystick s\n", i, SDL_JoystickName(i));
printf("Axes: %d\n", SDL_JoystickNumAxes(joy));
printf("Boutons: %d\n", SDL_JoystickNumButtons(joy));
printf("Trackballs: %d\n", SDL_JoystickNumBalls(joy));
printf("Chapeaux: %d\n\n", SDL_JoystickNumHats(joy));
}

/* Chargement du sprite */
tmp = SDL_LoadBMP("image.bmp");
sprite = SDL_DisplayFormat(tmp);
SDL_FreeSurface(tmp);
SDL_SetColorKey(sprite, SDL_SRCCOLORKEY, SDL_MapRGB(sprite->format, 0xff, 0x00, 0xff));

/* Position initiale du sprite */
spritepos.x = (screen->w - sprite->w) / 2;
spritepos.y = (screen->h - sprite->h) / 2;
spritepos.w = 0;
spritepos.h = 0;

/* Boucle de jeu */
while (!letsexit)
{
/* Mise à jour de l'état du jeu à partir des
évènements */
process_events();

/* Mise à jour de la position du sprite à partir
de sa vélocité */
spritepos.x += xvel;
spritepos.y += yvel;
/* Test de débordement d'écran */
if (spritepos.x < 0) spritepos.x = 0;
if (spritepos.x > screen->w - sprite->w) spritepos.x = screen->w - sprite->w;
if (spritepos.y < 0) spritepos.y = 0;
if (spritepos.y > screen->h - sprite->h) spritepos.y = screen->h - sprite->h;

/* Mise à jour de l'écran */
/* Remplissage de l'écran en noir */
SDL_FillRect(screen, NULL, 0);
/* Affichage du sprite puis mise à jour du
buffer d'affichage */
SDL_BlitSurface(sprite, NULL, screen, &spritepos);
SDL_Flip(screen);
}

SDL_Quit();
return 0;
}

La prochaine fois...

Ceci clôturera ce tour d'horizon de la gestion des entrées de SDL. Il semblait important de le faire tôt pour pouvoir profiter de l'interaction avec l'utilisateur, base de tout programme ludique. Vous l'aurez constaté, le panel d'entrées gérées par SDL est très large et extrêmement complet, en particulier pour les joysticks. Seul absente, la gestion du retour de force, très pauvre et anarchique sous Linux et totalement ignorée par SDL. Espérons que ce manque soit comblé un jour.

Le prochain épisode sera consacré aux animations et à la gestion du temps, du moins si l'ampleur des sujets n'oblige pas à revoir encore une fois la portée des articles à la baisse!

Références

Tutoriel SDL: Animation et gestion du temps

Paru dans Linux Mag 67, décembre 2004

Maintenant que nous savons dessiner sur l'écran et réagir aux entrées, nous pouvons nous lancer dans un peu de technique. Au programme cette fois-ci: l'animation de sprites.

Nous n'entendons pas ici animation au sens déplacement d'un élément graphique sur l'écran, mais plutôt comme une succession d'images fixes à un rythme donné. Chaque élément d'un jeu (vaisseau, projectile, ...) est susceptible de se déplacer à n'importe quel moment. Pour paraître crédible, le mouvement sur l'écran doit s'accompagner d'une animation de l'élément graphique représentant notre objet. Même les objets immobiles gagnent à être animés. Quoi de plus triste qu'un sprite désespérément figé dans l'attente d'un événement... Nous allons mettre au point quelques structures rudimentaires qui nous permettront de gérer facilement et de manière transparente (presque aussi simplement que les éléments non-animés) les sprites animés.

Le module d'animation

Le module que nous allons réaliser sera facilement réutilisable et incorporable dans un projet existant. Il se composera d'un fichier de déclaration (animation.h) et d'un fichier d'implémentation (animation.c) définissant des structures et les fonctions permettant de les manipuler. Ces fonctions peuvent être vues comme des « méthodes » qui s'appliquent à la « classe » dont le nom compose leur préfixe. Le principe des animations est le suivant: des images qui sont affichées les unes après les autres, chaque image restant affichée durant un certain délai. Fixons-nous tout d'abord certaines limites. Toutes les images d'une animation auront la même taille, pour simplifier leur gestion. Les animations seront par défaut cycliques. Enfin, toutes les images d'une animation seront masquées par la couleur 0xff00ff, traditionnellement utilisée pour les masques de couleurs. Commençons par l'écriture du fichier animation.h, qui contiendra les déclarations des structures et fonctions de notre module d'animation. L'en-tête inclura les headers de SDL et définira une macro qui permettra d'éviter les inclusions multiples:

 #ifndef ANIMATION_H__
#define ANIMATION_H__

#include "SDL.h"

L'élément de base d'une animation sera la frame, qui comprend la référence de la surface à afficher et le délai à attendre avant d'afficher la frame suivante:

 typedef struct
{
SDL_Surface * image;
Uint16 delay;
} animation_frame;

Les frames pourront subir 3 opérations: initialisation, nettoyage (libération de la mémoire allouée), et blit sur une autre surface.

 void animation_frame_init(animation_frame * frame, SDL_Surface * image, Uint16 delay);
void animation_frame_cleanup(animation_frame * frame);
void animation_frame_draw(animation_frame * frame, SDL_Surface * dst, SDL_Rect * pos);

Bien que déclarées publiques, ces opérations ne seront vraisemblablement utilisées que de manière interne au module.

Regroupées dans un tableau, les frames forment une animation :

 typedef struct
{
Uint16 nbr_of_frames;
animation_frame * frames;
}animation;

La lecture d'une animation se fait en affichant la frame 0 pendant son délai, puis en passant à la 1, et ainsi de suite. Une fois la dernière frame affichée, on boucle sur la première. Une frame restera indéfiniment affichée si son délai est égal à 0, ce qui nous permettra de créer des animations non-cycliques. La figure 1 illustre les deux structures animation et animation_frame et leur interaction.

Fig. 1: Illustration des structures de l'animation utilisée dans l'exemple de ce mois-ci.
Fig. 1:
Illustration des structures de l'animation utilisée dans l'exemple de ce mois-ci.

De même que pour les frames, une animation aura des fonctions d'initialisation et de nettoyage. Le nombre de frames dont dispose une animation se décide lors de son initialisation. La troisième fonction, animation_setframe, permet d'affecter une surface et un délai à une frame donnée.

 void animation_init(animation * anim, Uint16 nbr_of_frames);
void animation_cleanup(animation * anim);
void animation_setframe(animation * anim, Uint16 pos, SDL_Surface * surface, Uint16 delay);

Les structures animation et animation_frame ne sont rien de plus qu'une description de l'animation: nombre d'images, surface à afficher pour chacune, délai à attendre avant de passer à l'image suivante. On peut comparer une animation à une cassette vidéo ou un DVD: seule, elle est incapable d'afficher quoi que ce soit. Il faut un moyen de lecture, qui fera avancer l'animation et gardera une trace de la position courante.

C'est le rôle de la structure animator. Cette structure est liée à une animation et permet de la jouer. Elle comprend un certain nombre de variables d'état permettant notamment de connaître la position courante dans l'animation:

 typedef struct
{
const animation * anim;
enum { STOP, PLAY } status;
Uint16 current_frame;
Uint16 counter;
}animator;

anim est la référence vers l'animation qui est jouée. status permet de mettre une animation en pause (STOP) ou au contraire de la laisser se jouer (PLAY). current_frame et counter sont respectivement l'index de la frame courante dans l'animation qui est jouée, et le temps écoulé par rapport au délai durant lequel cette frame doit être affichée. La structure animator permet en fait un scénario irréaliste avec notre métaphore de la cassette vidéo: plusieurs animators peuvent être associés à une animation, ce qui serait équivalent à une cassette vidéo qui serait jouée simultanément par plusieurs magnétoscopes, chacun à une position différente. L'avantage pour nous est clair: si une zone de jeu comprend 50 fois le même élément animé, nous n'aurons pas besoin de charger 50 fois en mémoire sa représentation graphique: elle sera présente une fois en mémoire, mais utilisée par autant d'animators qu'il y aura de présences à l'écran, comme l'illustre la figure 2.

Fig. 2
Fig. 2:
Les animators permettent de séparer l'état d'une animation de ses données, et ainsi de jouer plusieurs fois la même animation simultanément.

N'allouant pas de mémoire, animator dispose d'une fonction d'initialisation qui prend en argument l'animation à jouer, mais pas de fonction de nettoyage.

 void animator_init(animator * ator, animation * anim);

Trois autres fonctions permettent de manipuler l'état de lecture de l'animation, pour la jouer, l'arrêter et la remettre dans son état initial:

 void animator_play(animator * ator);
void animator_stop(animator * ator);
void animator_rewind(animator * ator);
void animator_nextframe(animator * ator);

La fonction nextframe, elle, passe directement à la frame suivante quel que soit l'état de l'animator. Elle est utilisée en interne mais permet aussi de faire des animations dont le déroulement est géré par évènements: pour cela, on mettra les délais de toutes les frames à 0 et on appellera nextframe lorsque l'on souhaite passer à la frame suivante.

Enfin, les deux dernières fonctions sont également les principales : update incrémente le compteur interne de l'animator et met à jour la frame à afficher si nécessaire. draw dessine la frame courante sur la surface et à la position données.

 void animator_update(animator * ator);
void animator_draw(animator * ator, SDL_Surface * dest, SDL_Rect * pos);

#endif

La fonction animator_update doit être appelée régulièrement pour obtenir des animations cohérentes. Le délai que nous définissons n'a en effet aucune valeur temporelle. Si une frame a un délai de 15, il faudra appeler 15 fois animator_update pour passer à la frame suivante.

L'implémentation de ces fonctions est réalisée dans le fichier animation.c, dont voici le listing, assez trivial si l'on a compris le fonctionnement des animations:

 #include "animation.h"
#include <stdlib.h>

void animation_frame_init(animation_frame * frame, SDL_Surface * image, Uint16 delay) {
frame->image = image;
frame->delay = delay;
}

void animation_frame_cleanup(animation_frame * frame) {
if (frame->image) SDL_FreeSurface(frame->image);
}

void animation_frame_draw(animation_frame * frame, SDL_Surface * dest, SDL_Rect * pos) {
SDL_BlitSurface(frame->image, (SDL_Rect *)NULL, dest, pos);
}

void animation_init(animation * anim, Uint16 nbr_of_frames) {
anim->frames = (animation_frame *) calloc(nbr_of_frames, sizeof(animation_frame));
anim->nbr_of_frames = nbr_of_frames;
}


void animation_setframe(animation * anim, Uint16 pos, SDL_Surface * surface, Uint16 delay) {
animation_frame_cleanup(&anim->frames[pos]);
animation_frame_init(&anim->frames[pos], surface, delay);
}


void animation_cleanup(animation * anim) {
int i;
for (i = 0; i < anim->nbr_of_frames; i++)
animation_frame_cleanup(&anim->frames[i]);

free(anim->frames);
}


void animator_init(animator * ator, animation * anim) {
ator->anim = anim;
animator_rewind(ator);
}

void animator_play(animator * ator) {
ator->status = PLAY;
}

void animator_stop(animator * ator) {
ator->status = STOP;
}

void animator_rewind(animator * ator) {
ator->current_frame = 0;
ator->counter = 0;
animator_stop(ator);
}

void animator_nextframe(animator * ator) {
/* Retour à la frame 0 si nous sommes à la dernière */
if (++ator->current_frame == ator->anim->nbr_of_frames) ator->current_frame = 0;
ator->counter = 0;
}

void animator_update(animator * ator) {
const animation_frame * frame;

/* Ne mettre à jour l'animation que si elle est jouée */
if (ator->status != PLAY) return;

frame = &ator->anim->frames[ator->current_frame];
if (frame->delay == 0) return;

/* Passage à la frame suivante? */
if (++ator->counter == frame->delay) animator_nextframe(ator);
}


void animator_draw(animator * ator, SDL_Surface * dest, SDL_Rect * pos) {
animation_frame_draw(&ator->anim->frames[ator->current_frame], dest, pos);
}

Gestion du temps

Nous allons également nous préoccuper un peu du caractère temporel de nos programmes. Ils seront amenés à s'exécuter sur des machines de puissance et de configuration différentes, et ne fonctionneront donc pas exactement de la même manière. SDL offre une abstraction permettant de ne pas avoir à prendre en compte les différences d'architectures et de matériel, il nous reste cependant à gérer le cas où une machine fait tourner notre programme tellement vite que cela en devient injouable ou, plus fréquent, n'est pas assez puissante l'exécuter de manière optimale.

Il s'agit d'une situation connue de tous les joueurs: un jeu se met à « ramer ». Deux attitudes de sa part sont alors possibles: soit la cadence du jeu se ralentit, et le jeu devient plus « lent », soit elle reste la même mais des « saccades » apparaissent alors.

Bien qu'aucune de ces situations ne soit vraiment agréable, il est en général préférable de tomber dans la seconde que dans la première. Le premier comportement est ce qui arrive si aucune prise en charge du temps n'a été prévue. Nous allons voir comment implémenter le second.

Il existe plusieurs manières de procéder, mais la suivante est à la fois simple et efficace. Tout d'abord, nous allons fixer un « rythme » pour notre programme, exprimé en cycles par secondes. Un cycle correspondant à la mise à jour de son l'état interne (position des sprites, mise à jour des animations, etc). Ce rythme, quoi qu'il en coûte, devra toujours être respecté. Si notre programme détecte qu'il a pris du retard, il lui faudra trouver un moyen de faire moins de calculs pour rattraper ce retard. Qu'allons-nous alors sacrifier? Le feedback à l'utilisateur, c'est à dire l'affichage. En général, la mise à jour de l'écran prend un temps très condérable, souvent la plus grosse partie du temps d'exécution d'un jeu, en particulier si la cible n'est pas accélérée. Nous allons donc avoir un deuxième « rythme », le nombre d'images par secondes, qui idéalement sera égal au nombre de cycles de jeu par secondes mais pourra être inférieur si besoin est. Autrement dit, si la machine sur laquelle tourne le programme est trop lente pour honorer ses délais, nous « sauterons » le nombre d'images nécessaires pour que l'état interne du programme puisse au moins être mis à jour en temps et en heure.

Afin de pouvoir en arriver là, il est nécessaire de bien séparer l'affichage du jeu en lui-même. C'est d'ailleurs une bonne pratique en programmation et le reflet d'un bon design. Un jeu n'a besoin de l'affichage que pour montrer son état interne au joueur. Il peut et doit être capable de fonctionner sans prendre ce dernier en compte.

Notre petit module d'animation prend déjà cette séparation en compte. La structure animator contient l'état d'une animation, la structure animation contient ses éléments graphiques. animation dépend de SDL dont elle utilise des éléments, animator en est indépendante (en dehors des types primitifs comme Uint16, mais ceux-ci peuvent être redéfinis). On pourrait ainsi remplacer notre implémentation de animation utilisant SDL par une autre utilisant une autre bibliothèque graphique et continuer à l'utiliser avec animator sans changer cette dernière. C'est un de effets de bord bénéfiques d'un bon design et donc d'une bonne séparation des préoccupations.

Mais revenons-en à notre gestion du temps. Nous allons l'implémenter dans time.h et time.c, de la même manière que nous avons fait pour les animations. SDL offre une fonction qui permet de connaître le nombre de millisecondes qui se sont écoulées depuis l'initialisation de la bibliothèque:

 Uint32 SDL_GetTicks(void);

Nous pouvons nous servir de cette fonction pour mesurer le temps qui s'est écoulé entre deux cycles. Pour cela, il nous faudra une fonction qui soit appelée à chaque cycle de jeu pour battre la cadence. C'est le rôle de notre fonction de synchronisation, time_update, qui a pour vocation d'être appelée dans la boucle de jeu et va réaliser les tâches suivantes:

Deux autres fonctions font partie de notre module de gestion du temps, time_init qui l'initialise aux valeurs par défaut, et time_set_game_speed qui permet de changer la vitesse du jeu, en nombre de cycles par secondes.

Voici les listings de time.h et time.c:

time.h:

 #ifndef TIME_H__
#define TIME_H__

#include "SDL.h"

extern Uint16 cycles_to_calculate;
extern Uint16 game_speed;
extern Uint16 cycle_length;

void time_init();
void time_set_game_speed(Uint16 speed);
void time_update();

#endif

time.c:

 #include "time.h"

static Uint32 timer1, timer2;

/* Nombre maximum de cycles à passer - au delà, le jeu
sera ralenti */
#define MAX_SKIPPED_FRAMES 20
/* Variable globale indiquand le nombre de cycles à
calculer avant d'afficher une image */
Uint16 cycles_to_calculate = 0;

/* Vitesse par défaut du jeu, en cycles par seconde */
#define DEFAULT_GAME_SPEED 70
/* Vitesse courante du jeu */
Uint16 game_speed;
/* Durée d'un cycle en millisecondes */
Uint16 cycle_length;

/* Initialise le système temporel */
void time_init() {
time_set_game_speed(DEFAULT_GAME_SPEED);
}

/* Affecte la vitesse du jeu, en cycles par seconde */
void time_set_game_speed(Uint16 speed) {
if (speed == 0) speed = 1;
game_speed = speed;
cycle_length = 1000 / game_speed;
timer1 = SDL_GetTicks();
}

/* Mise à jour du temps et des variables associées */
void time_update() {
/* S'assurer que l'on ne va pas trop vite... */
while (1) {
timer2 = SDL_GetTicks() - timer1;
if (timer2 >= cycle_length) break;
else SDL_Delay(3);
}

/* Mise à jour de l'état des timers et des variables
globales - timer1 va contenir le "surplus" de
timer2 par rapport au temps que l'on aurait exactement
dû passer, cycles_to_calculate le nombre de cycles à
faire jouer avant d'afficher une nouvelle image */
timer1 = SDL_GetTicks() - (timer2 % cycle_length);
cycles_to_calculate = timer2 / cycle_length;
/* Vérifier la limite des cycles à sauter */
if (cycles_to_calculate > MAX_SKIPPED_FRAMES) cycles_to_calculate = MAX_SKIPPED_FRAMES;
}

Le programme du mois

Comme il se doit, notre programme d'exemple va utiliser ces deux modules pour créer un petit effet graphique. Il crée une animation correspondant à l'explosion illustrée en figure 1 et l'afficher plusieurs fois à l'écran. Le nombre d'explosions simultanées est contrôlé par deux tableaux, l'un d'animators, l'autre de SDL_Rects. Chaque couple correspond à une lecture de l'animation et à sa position à l'écran. Les touches + et -du pavé numérique permettent d'augmenter ou de diminuer le nombre d'explosions simultanées. Le module de gestion du temps est utilisé pour donner le rythme du jeu. Par défaut, le jeu tourne à 70 cycles par secondes. En utilisant les touches PageUp et PageDown, il est possible d'augmenter ou de diminuer de rythme par palliers de 5 cycles par seconde. Pour permettre de mesurer les performances de la machine, le nombres d'images par secondes est affiché toutes les secondes sur la console. Idéalement, il sera à peu près égal au nombre de cycles par secondes (une légère imprécision peut donner des chiffres légèrement supérieurs ou inférieurs), mais si le rythme est trop soutenu, ou que la machine est chargée par d'autres programmes, vous allez voir qu'il va rapidement tomber. Le jeu, en revanche, gardera la même vitesse. Voici le listing de ce programme, à mettre dans main.c:

 #include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "SDL.h"

#include "animation.h"
#include "time.h"

char letsexit = 0;

/* Nombre d'explosions simultanées et valeur
par défaut */
#define DEFAULT_EXPLO 10
static int nb_explo;

/* L'animation et les tableaux contenant les
animators ainsi que leurs positions */
static animation anim;
static animator * player = NULL;
static SDL_Rect * pos = NULL;

SDL_Surface * screen;

/* Réalloue la taille des tableaux pour
n explosions */
void allocate_explosions(int n) {
nb_explo = n;
player = (animator *) realloc(player, sizeof(animator) * nb_explo);
pos = (SDL_Rect *) realloc(pos, sizeof(SDL_Rect) * nb_explo);
}


/* Initialise une animation nouvellement allouée */
void init_explosion(int i) {
/* Position aléatoire sur l'écran */
pos[i].x = rand() % (screen->w - player[i].anim->frames[0].image->w);
pos[i].y = rand() % (screen->h - player[i].anim->frames[0].image->h);
/* Position aléatoire dans l'animation */
player[i].current_frame = rand() % player[i].anim->nbr_of_frames;
player[i].counter = rand() % player[i].anim->frames[player[i].current_frame].delay;
}

/* Gestion des entrées clavier */
void input_update() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_KEYDOWN:
switch (event.key.keysym.sym) {
/* Quitter */
case SDLK_ESCAPE:
letsexit = 1;
break;
/* Plus d'explosions */
case SDLK_KP_PLUS:
allocate_explosions(nb_explo + 1);
animator_init(&player[nb_explo - 1], &anim);
animator_play(&player[nb_explo - 1]);
init_explosion(nb_explo - 1);
printf("Nombre d'explosions: %d\n", nb_explo);
break;
/* Moins d'explosions */
case SDLK_KP_MINUS:
if (nb_explo == 0) break;
allocate_explosions(nb_explo - 1);
printf("Nombre d'explosions: %d\n", nb_explo);
break;
/* Plus rapide */
case SDLK_PAGEUP:
time_set_game_speed(game_speed + 5);
printf("Vitesse de jeu: %d cycles/seconde\n", game_speed);
break;
/* Moins rapide */
case SDLK_PAGEDOWN:
if (game_speed < 5) time_set_game_speed(1);
else time_set_game_speed(game_speed - 5);
printf("Vitesse de jeu: %d cycles/seconde\n", game_speed);
break;
default:
break;
}
break;
default:
break;
}
}
}


int main(int argc, char * argv[]) {
SDL_Surface * img;
int i;
if (SDL_Init(SDL_INIT_VIDEO) == -1) {
printf("Erreur lors de l'initialisation de SDL!\n");
return 1;
}
screen = SDL_SetVideoMode(640, 480, 24,
SDL_HWSURFACE | SDL_DOUBLEBUF | SDL_ANYFORMAT);

/* Création de l'animation */
animation_init(&anim, 3);
/* Frame 0, délai 10 cycles */
img = SDL_LoadBMP("explo1.bmp");
SDL_SetColorKey(img, SDL_SRCCOLORKEY, SDL_MapRGB(img->format, 0xff, 0x00, 0xff));
animation_setframe(&anim, 0, img, 10);
/* Frame 1, délai 15 cycles */
img = SDL_LoadBMP("explo2.bmp");
SDL_SetColorKey(img, SDL_SRCCOLORKEY, SDL_MapRGB(img->format, 0xff, 0x00, 0xff));
animation_setframe(&anim, 1, img, 15);
/* Frame 2, délai 30 cycles */
img = SDL_LoadBMP("explo3.bmp");
SDL_SetColorKey(img, SDL_SRCCOLORKEY, SDL_MapRGB(img->format, 0xff, 0x00, 0xff));
animation_setframe(&anim, 2, img, 30);
/* Allocation de initialisation des explosions et de leur position */
allocate_explosions(DEFAULT_EXPLO);
for (i = 0; i < nb_explo; i++) {
animator_init(&player[i], &anim);
animator_play(&player[i]);
init_explosion(i);
}
/* Initialisation du temps avant la boucle principale */
time_init();
/* Ces variables nous permettront de calculer les FPS
(frames per second, images par seconde) */
Uint32 timer1 = SDL_GetTicks(), timer2;
int frames_per_second = 0;
/* Boucle de jeu */
while (!letsexit) {
int k;
/* Mise à jour des entrées et du temps */
input_update();
time_update();
/* Remplissage de l'écran en bleu */
SDL_FillRect(screen, NULL, SDL_MapRGB(screen->format, 0x00, 0x00, 0xff));
/* Mise à jour de l'état interne du jeu. cycles_to_calculate nous indique
de combien de cycles avancer pour garder une vitesse cohérente */
for (k = 0; k < cycles_to_calculate; k++) {
/* Mise à jour des explosions */
for (i = 0; i < nb_explo; i++) {
animator_update(&player[i]);

/* Si l'animation vient de boucler... */
if (player[i].current_frame == 0 && player[i].counter == 0) {
/* ... on lui change sa position aléatoirement */
pos[i].x = rand() % (screen->w - img->w);
pos[i].y = rand() % (screen->h - img->h);
}
}
}
/* Dessiner les explosions sur l'écran */
for (i = 0; i < nb_explo; i++)
animator_draw(&player[i], screen, &pos[i]);
/* Afficher l'écran que l'on vient de dessiner */
SDL_Flip(screen);
/* Calculer les fps */
frames_per_second++;
timer2 = SDL_GetTicks();
/* Une seconde s'est écoulée? Afficher le nombre d'écrans
dessinés et réinitialiser */
if (timer2 - timer1 > 1000) {
printf("FPS: %d\n", frames_per_second);
timer1 = timer2;
frames_per_second = 0;
}
}
/* Libération des ressources allouées */
free(player);
free(pos);
animation_cleanup(&anim);
SDL_Quit();
return 0;
}

Comme d'habitude, vous retrouverez le listing complet ainsi que les images utilisées se trouvent sur le CD-ROM, ou sur http://www.gnurou.org/documents/linuxmag/SDL/SDL-3.tar.gz. La compilation se fait de la manière suivante (ou mieux, en utilisant un Makefile):

 $ gcc `sdl-config --cflags --libs` animation.c time.c main.c -o test

Fig. 3: Le programme du mois
Fig. 3:
Le programme du mois.

La prochaine fois

Nous voilà maintenant avec les bases nécessaires pour créer des animations plus évoluées. La prochaine fois, nous changerons un peu de registre en étudiant des effets graphiques calculés, comme l'effet de feu ou le champ d'étoiles. Ces effets sont étonnants à deux titres: leur simplicité d'implémentation et le résultat visuel. D'ici là, codez bien!

Tutoriel SDL: Effet de feu!

Paru dans Linux Mag 68, janvier 2005

Ce mois-ci, nous allons nous plonger de manière plus approfondie dans la manipulation des surfaces de SDL en implémentant un effet de feu. Cet effet, l'un des plus vieux effets graphiques qui existent, est très simple à implémenter et la multitude de variantes qui existent autour de lui le rendent particulièrement intéressant pour le débutant en programmation graphique qui peut l'expérimenter avec de multiples paramètres.

Principe de l'effet

L'effet de feu est algorithmiquement très simple, pourtant il donne un résultat visuel très convainquant (voir figure 1). Mais s'il est facile à implémenter, il n'en reste pas moins qu'il coûte assez cher en temps de calcul, pour la simple raison que chaque pixel de la zone sur laquelle s'applique l'effet doit être recalculé en fonction de 4 de ses voisins. Cela implique un nombre très conséquent d'accès mémoire et le réservera en général à des zones assez petites.

Fig. 1
Fig. 1:
L'exemple du mois.

L'algorithme calcule l'état de la nouvelle zone sur laquelle s'applique l'effet en fonction de son état précédent. L'effet de base parcourt la zone ligne par ligne, du haut vers le bas, et affecte à chaque pixel la moyenne des pixels inférieur gauche, inférieur, inférieur droit ainsi que du pixel situé deux lignes en dessous, comme l'illustre la figure 2. C'est tout!

Fig. 2: Principe de l'effet de feu. La nouvelle couleur du pixel gris, en haut, sera la moyenne des couleurs des 4 autres pixels.
Fig. 2:
Principe de l'effet de feu. La nouvelle couleur du pixel gris, en haut, sera la moyenne des couleurs des 4 autres pixels.

Comme il n'y a pas beaucoup de sens à additionner directement des pixels, ce sont les composantes rouge, verte et bleue des quatre pixels adjacents qui seront moyennées pour obtenir les nouvelles valeurs des composantes du pixel en cours de calcul. Il faudra donc à chaque fois les décomposer.

Bien évidemment, puisque l'aspect du feu dépend de l'état précédent de la zone, il va falloir donner à cette dernière un état initial. En fait, comme pour de vraies flammes, il va nous falloir un « départ » du feu, ou un foyer. Celui-ci sera représenté par les deux lignes en dessous de notre effet de feu et aura une mise à jour totalement différente.

Le foyer, comme son nom l'indique, constitue le point de départ de nos flammes. L'état de ces deux lignes va totalement déterminer le contour des flammes et, sans elles, l'effet de feu ne ferait rien d'autre que calculer des pixels noirs. Le foyer ainsi que la manière dont il est mis à jour est donc tout aussi important que l'effet de feu en lui-même.

La manière de le gérer restera cependant elle aussi d'une simplicité enfantine: il suffira d'allumer et d'éteindre au hasard les pixels de ces deux lignes. Cela sera suffisant pour démarrer l'effet et conditionnera totalement l'aspect des flammes. Pour garder un ensemble homogène, les pixels allumés le seront avec une couleur dont les trois composantes sont uniformes (un gris plus ou moins clair). Bien évidemment, plus le gris du pixel sera clair, plus la flamme qui l'exploitera montera haut. De même, plus il y aura de pixels adjacents allumés, plus les flammes seront denses. On distinguera trois paramètres de l'évolution du foyer qui nous permettront d'expérimenter plusieurs variantes de flammes:

La figure 3 illustre un état du foyer. Sur l'exemple montré, les flammes monteront très haut, car beaucoup de pixels sont allumés avec une intensité suffisante.

Fig. 3: Un exemple de foyer de 22 pixels de long sur deux lignes. À chaque mise à jour, des pixels sont allumés ou éteints aléatoirement.
Fig. 3: Un exemple de foyer de 22 pixels de long sur deux lignes. À chaque mise à jour, des pixels sont allumés ou éteints aléatoirement.

Implémentation avec SDL

L'implantation de cet effet avec SDL nous donne l'occasion de découvrir les surfaces un peu plus en détail. Lors du premier épisode, nous avions écrit des fonctions permettant de lire ou d'écrire un pixel d'une surface, et nous avions utilisé les fonctions que fournit SDL pour obtenir la valeur d'un pixel pour un format de surface donné en fonction de ses trois composantes. Cette fois-ci, nous allons écrire nos propres macros nous permettant de faire ces opérations, ce qui sera autrement plus performant que d'appeler une fonction à chaque manipulation de pixel, en plus d'être instructif. En contrepartie, notre implantation aura quelques limites :

Les surfaces en profondeur

Jusqu'à présent, notre utilisation des surfaces se limitait à les passer en paramètres de fonctions définies par SDL et, parfois, à accéder directement aux pixels via le champ homonyme. Il est cependant tout aussi important, en particulier pour l'implantation d'effets graphiques, de connaître précisément le format de stockage des pixels. On peut facilement savoir si une surface utilise 16 ou 24 bits par pixels, mais cela ne veut pas dire grand chose (bien que dans 99% des cas on retrouve la même structure). Par exemple, en format 24 bits, quel est l'ordre dans lequel sont stockées les composantes? Nous sommes habitués au format RGB (Red, Green, Blue) mais rien n'empêche le matériel d'utiliser autre chose. C'est le membre format de SDL_Surface qui va nous renseigner sur les subtilités du format d'une surface. Il est du type SDL_PixelFormat et fournit notamment les champs suivants :

Fig. 4: Utilisation des membres Rmask et Rshift pour isoler la composante rouge d'un pixel.
Fig. 4: Utilisation des membres Rmask et Rshift pour isoler la composante rouge d'un pixel.

Ces informations nous permettront d'extraire les composantes des pixels et de construire des pixels à partir de ces composantes de manière plus efficace que si l'on appelait directement SDL_GetRGB et SDL_MapRGB.

Implémentation de l'effet

Comme vous l'aurez peut-être déjà deviné, l'effet de feu sera en fait constitué de deux effets : la montée des flammes et leur génération, c'est-à-dire le foyer. Chacun de ces effets sera géré par une fonction qui prendra en paramètres la surface sur laquelle appliquer l'effet ainsi qu'un SDL_Rect pour indiquer sa zone de délimitation. Nous allons implémenter nos effets dans deux fichiers fire.h et fire.c. Voici le contenu de fire.h, qui déclare nos deux fonctions:

#ifndef FIRE_H__ 
#define FIRE_H__

#include "SDL.h"

/**
* Génère un foyer gris sur la surface donnée, dans les limites de
* rect, en fonction des paramètres intensity, vivacity et résistance.
* Attention: la surface doit être verrouillée!
*/
void fireRoot(SDL_Surface * surface, SDL_Rect * rect, Uint8 intensity, Uint8 vivacity,
Uint8 resistance);

/**
* Applique l'effet de feu sur la surface donnée, dans les limites de
* rect. Attention: la surface doit être verrouillée!
*/
void fire(SDL_Surface * surface, SDL_Rect * rect);

#endif

fire.c quant à lui commencera par les déclarations des macros nous permettant de manipuler les pixels. REDVALUE, GREENVALUE et BLUEVALUE isolent les composantes de couleurs d'un pixel comme décrit précédemment et donnent leur valeur comprise entre 0 et 255.

#include <stdlib.h>
#include <stdlib.h>
#include "fire.h"

/* Retourne les valeurs sur 8 bits des composantes rouges, vertes et
bleues par rapport au format donné */
#define REDVALUE(format, pixel) (Uint8) (((pixel & format->Rmask) << format->Rloss) \
>> format->Rshift)
#define GREENVALUE(format, pixel) (Uint8) (((pixel & format->Gmask) << format->Gloss) \
>> format->Gshift)
#define BLUEVALUE(format, pixel) (Uint8) (((pixel & format->Bmask) << format->Bloss) \
>> format->Bshift)

Les opérations inverses, obtenir le masque correspondant à une composante de couleur à partir de sa valeur sur 8 bits, sont également fournies, ainsi qu'une autre macro pour obtenir un pixel directement utilisable à partir des composantes:

/* Crée les masques correspondants pour les composantes de couleur
données sur 8 bits */
#define REDMASK(format, red) (Uint32) (((red << format->Rshift) \
>> format->Rloss) & format->Rmask)
#define GREENMASK(format, green) (Uint32) (((green << format->Gshift) \
>> format->Gloss) & format->Gmask)
#define BLUEMASK(format, blue) (Uint32) (((blue << format->Bshift) \
>> format->Bloss) & format->Bmask)

/* Retourne la valeur d'un pixel selon ses 3 composantes de couleur,
données sur 8 bits */
#define MAKEPIXEL(format, red, green, blue) (Uint32) (REDMASK(format, red) \
| GREENMASK(format, green) | BLUEMASK(format, blue))

Enfin, deux dernières macros s'occupent d'écrire et de lire correctement un pixel codé sur 24 bits à partir d'un pointeur donné en paramètre:

/* Lit et affecte un pixel à partir d'un pointeur */
#define READPIXEL(pointer, pixel) { pixel = *(Uint16 *)(pointer) + \
(*(Uint8 *)((pointer) + 2) << 16); }
#define STOREPIXEL(pointer, pixel) { *(Uint16 *)(pointer) = (Uint16)(pixel); \
*(Uint8 *)((pointer) + 2) = (Uint8)((pixel) >> 16) & 0xff; }

Ces macros étant définies, nous pouvons attaquer les effets proprement dits. Le foyer ne posera pas de problèmes particuliers. Il s'agira juste, dans un premier temps, d'allumer des pixels à des positions et des intensités aléatoires en fonction des paramètres vivacity et intensity, puis, dans un deuxième temps, d'en éteindre d'autres en fonction du paramètre resistance.

/* Effet de foyer */ 
void fireRoot(SDL_Surface * surface, SDL_Rect * rect, Uint8 intensity, Uint8 vivacity, \
Uint8 resistance) {
Uint32 i, j;

/* Pour chaque ligne du foyer, la vivacité détermine combien de
pixels mettre à jour */
for (j = rect->y; j < rect->y + rect->h; j++)
for (i = 0; i < vivacity; i++) {
/* Déterminer la valeur de chaque composante en fonction de
l'intensité - elles auront les mêmes valeurs, ce qui nous
fournira un gris */
Uint8 color = (intensity + (rand() % (256 - intensity)));
/* Choisir une position au hasard */
Uint32 position = (rand() % rect->w) + rect->x;

/* Et y affecter le pixel */
STOREPIXEL(surface->pixels + j * surface->pitch + position *
surface->format->BytesPerPixel,
MAKEPIXEL(surface->format, color, color, color));
}

/* Même chose avec la résistance - on éteint aléatoirement des
pixels */
for (j = rect->y; j < rect->y + rect->h; j++)
for (i = 0; i < resistance; i++) {
Uint32 position = (rand() % rect->w) + rect->x;

STOREPIXEL(surface->pixels + j * surface->pitch + (position *
surface->format->BytesPerPixel), 0);
}
}

Enfin, l'effet de feu. Il sera implémenté fidèlement à notre description. Chaque pixel de la zone parcourue est re-calculé en faisant la moyenne de ses quatre voisins.

/* Effet de feu */
void fire(SDL_Surface * surface, SDL_Rect * rect) {
Uint32 i, j;

/* Chaque pixel de la zone doit être mis à jour */
for (j = rect->y; j < rect->y + rect->h; j++)
for (i = rect->x; i < rect->x + rect->w; i++) {
/* Pointeur vers le pixel courant */
void * curpixel;
/* Nouvelle valeur du pixel courant */
Uint32 pixvalue = 0;
/* Addition des valeurs des composantes rouges, vertes et bleues
des pixels adjacents au pixel courant */
Uint16 red = 0, green = 0, blue = 0;
/* Le pixel à mettre à jour */
curpixel = surface->pixels + (surface->pitch * j) +
(i * surface->format->BytesPerPixel);
/* Addition des composantes des 4 pixels adjacents */
/* Inférieur gauche */
if (i > 0) {
READPIXEL(curpixel + surface->pitch - surface->format->BytesPerPixel,
pixvalue);
red += REDVALUE(surface->format, pixvalue);
green += GREENVALUE(surface->format, pixvalue);
blue += BLUEVALUE(surface->format, pixvalue);
}

/* Inférieur */
READPIXEL(curpixel + surface->pitch, pixvalue);
red += REDVALUE(surface->format, pixvalue);
green += GREENVALUE(surface->format, pixvalue);
blue += BLUEVALUE(surface->format, pixvalue);

/* Deux lignes en dessous */
READPIXEL(curpixel + surface->pitch * 2, pixvalue);
red += REDVALUE(surface->format, pixvalue);
green += GREENVALUE(surface->format, pixvalue);
blue += BLUEVALUE(surface->format, pixvalue);

/* Inférieur droit */
if (i < surface->w - 1) {
READPIXEL(curpixel + surface->pitch + surface->format->BytesPerPixel,
pixvalue);
red += REDVALUE(surface->format, pixvalue);
green += GREENVALUE(surface->format, pixvalue);
blue += BLUEVALUE(surface->format, pixvalue);
}

/* Altération des composantes afin d'obtenir la teinte de la
flamme */
if (red > 2) red -= 2; else red = 0;
if (green > 5) green -= 5; else green = 0;
if (blue > 150) blue -= 150; else blue = 0;

/* Division par quatre de l'addition des composantes */
red /= 4; green /= 4; blue /= 4;

/* Affectation de la nouvelle valeur du pixel */
STOREPIXEL(curpixel, MAKEPIXEL(surface->format, red, green, blue));
}
}

Vous aurez remarqué que, avant la division de la somme des quatre pixels adjacents, il y a une phase d'altération. Le bleu est sévèrement reduit, le vert un peu moins, et le rouge encore moins. C'est ce qui donnera la teinte de couleur à nos flammes et les fera tourner très vite au jaune, puis au rouge en fin de course. Sans cette étape, les flammes resteront grises et l'effet ressemblera alors plus à de la fumée. Vous pouvez expérimenter en supprimant ou modifiant cette étape.

Enfin, comme de coutume, voici le programme principal utilisant le module que nous avons implémenté. Il offre plusieurs voies d'interaction: le paramètre d'intensité de foyer peut être modifié à l'aide des touches haut et bas, la résistance avec les touches gauche et droite et la vivacité avec pageup/pagedown. La touche entrée fera s'afficher sur l'écran l'image du logo SDL qui subira automatiquement l'effet de feu et se « vaporisera » en flammes. Le programme en lui-même n'a rien de compliqué et ne fait que reprendre des éléments déjà vus précedemment. Voici son listing, à mettre dans main.c:

#include <stdio.h>
#include <stdlib.h>
#include "SDL.h"

#include "fire.h"

/* Variables d'intensités, de vivacité et de résistance */
#define DEFAULT_INTENSITY 150
#define DEFAULT_VIVACITY 128
#define DEFAULT_RESISTANCE 75

static Uint8 intensity = DEFAULT_INTENSITY;
static Uint8 vivacity = DEFAULT_VIVACITY;
static Uint8 resistance = DEFAULT_RESISTANCE;

static Sint8 intensity_delta = 0;
static Sint8 vivacity_delta = 0;
static Sint8 resistance_delta = 0;

/* Mise à 1 s'il faut quitter */
Uint8 letsexit = 0;

SDL_Surface * screen, * image;

/* Gestion des entrées clavier */
void input_update() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_KEYDOWN:
switch (event.key.keysym.sym) {
/* Quitter */
case SDLK_ESCAPE:
letsexit = 1;
break;
/* Enflammement du logo SDL - il suffit de le dessiner
et de laisser l'effet de feu faire le reste */
case SDLK_RETURN:
{
SDL_Rect blitRect = { 0, 0, 0, 0 };
blitRect.x = (rand() % (screen->w - image->w));
blitRect.y = (rand() % (screen->h - image->h));

SDL_BlitSurface(image, NULL, screen, &blitRect);
break;
}
/* Augmenter intensité */
case SDLK_UP:
intensity_delta = 1;
break;
/* Diminuer intensité */
case SDLK_DOWN:
intensity_delta = -1;
break;
/* Augmenter résistance */
case SDLK_RIGHT:
resistance_delta = 1;
break;
/* Diminuer résistance */
case SDLK_LEFT:
resistance_delta = -1;
break;
/* Augmenter vivacité */
case SDLK_PAGEUP:
vivacity_delta = 1;
break;
/* Diminuer vivacité */
case SDLK_PAGEDOWN:
vivacity_delta = -1;
break;
default:
break;
}
break;
/* Remise à zéro du delta des variables de paramètre du foyer si
une touche leur correspondant est relâchée */
case SDL_KEYUP:
switch (event.key.keysym.sym) {
case SDLK_UP:
case SDLK_DOWN:
intensity_delta = 0;
break;
case SDLK_RIGHT:
case SDLK_LEFT:
resistance_delta = 0;
break;
case SDLK_PAGEUP:
case SDLK_PAGEDOWN:
vivacity_delta = 0;
break;
default:
break;
}
break;
default:
break;
}
}
}

int main(int argc, char * argv[]) {
/* Rectangles dans lesquels vont s'appliquer le foyer et l'effet de
feu */
SDL_Rect rootRect, fireRect;
/* Surface temporaire pour charger l'image */
SDL_Surface * tmp;

if (SDL_Init(SDL_INIT_VIDEO) == -1) {
printf("Erreur lors de l'initialisation de SDL!\n");
return 1;
}

screen = SDL_SetVideoMode(320, 240, 24,
SDL_SWSURFACE);

/* Le foyer est appliqué aux deux dernières lignes */
rootRect.x = 0;
rootRect.y = screen->h - 2;
rootRect.w = screen->w;
rootRect.h = 2;

/* L'effet de feu s'applique sur tout l'écran, à l'exception
des deux dernières */
fireRect.x = 0;
fireRect.y = 0;
fireRect.w = screen->w;
fireRect.h = screen->h - 2;

/* Création de l'image du logo SDL */
tmp = SDL_LoadBMP("image.bmp");
image = SDL_DisplayFormat(tmp);
SDL_FreeSurface(tmp);
SDL_SetColorKey(image, SDL_SRCCOLORKEY, SDL_MapRGB(image->format, 0xff, 0x00, 0xff));

/* Boucle principale */
while (!letsexit) {
/* Mise à jour des entrées */
input_update();

/* Mise à jour des paramètre du foyer */
if ((intensity + intensity_delta) < 0) intensity = 0;
else if ((intensity + intensity_delta) > 255) intensity = 255;
else intensity += intensity_delta;
if (intensity_delta != 0) printf("Intensité: %d\n", intensity);

if ((vivacity + vivacity_delta) < 0) vivacity = 0;
else if ((vivacity + vivacity_delta) > 255) vivacity = 255;
else vivacity += vivacity_delta;
if (vivacity_delta != 0) printf("Vivacité: %d\n", vivacity);

if ((resistance + resistance_delta) < 0) resistance = 0;
else if ((resistance + resistance_delta) > 255) resistance = 255;
else resistance += resistance_delta;
if (resistance_delta != 0) printf("Résistance: %d\n", resistance);

/* Ne pas oublier de verrouiller la surface que l'on va directement
modifier! */
SDL_LockSurface(screen);
fireRoot(screen, &rootRect, intensity, vivacity, resistance);
fire(screen, &fireRect);
SDL_UnlockSurface(screen);

/* Mise à jour de l'écran */
SDL_Flip(screen);
}

SDL_Quit();
return 0;
}

Le Makefile suivant permet de compiler tout cela vers l'exécutable fire:

CFLAGS = `sdl-config --cflags` -Wall -ansi -g -O3 
LIBS = `sdl-config --libs`

OBJECTS = main.o fire.o

all: $(OBJECTS)
$(CC) $(OBJECTS) -o fire $(LIBS)

clean:
rm -f *.o fire

Comme d'habitude, toutes les sources se trouvent sur le CD qui accompagnent le magazine, ou à l'adresse http://www.gnurou.org/documents/linuxmag/SDL/SDL-4.tar.gz.

Variantes

Il existe une multitude de variantes à l'effet de feu. Vous pouvez jouer avec la variation de teinte des flammes, les paramètres du foyer, mais aussi avec le nombre de pixels adjacents pris en compte (3, 6, ...). En partant de la logique qui consiste à calculer le nouvel état de chaque pixel en fonction de l'état précédent de ses voisins, il y a beaucoup d'autres effets à découvrir. N'hésitez pas à expérimenter!

Tutoriel SDL: Faites du bruit!

Paru dans Linux Mag 70, mars 2005

Durant les prochains mois, nous allons découvrir une facette de SDL encore inconnue pour nous: la gestion de l'audio. L'article de ce mois-ci posera les bases qui nous permettront d'aller plus loin. Commençons donc par comprendre comment nos machines produisent de si jolis sons...

Théorie sur l'audio numérique

L'élément de base du son numérique est l'échantillon. Il correspond à une prise de mesure de la pression sur l'air générée par la source sonore à un instant donné. Cette dépression ou surpression sera restituée lors de la lecture par une vibration de la membrane des enceintes. L'échantillon est au son ce que le pixel est à l'image: comme pour un pixel, un échantillon possède un format, et comme pour un pixel celui-ci conditionne le nombre de nuances qu'il est possible d'obtenir. SDL gère les formats sonores 8 et 16 bits, les formats supérieurs étant réservés aux cartes sonores haut de gamme. Pour plus d'informations sur le son numérique: http://www.commentcamarche.net/audio/son.php3

Tout comme une image est le résultat de l'assemblage de nombreux pixels, un son se compose d'un certain nombre d'échantillons qui sont joués à une certaine fréquence. Cette fréquence s'exprime en hertz, une fréquence de 11025 Hz correspondant à 11025 échantillons joués en une seconde. La fréquence sonore est en quelque sorte l'équivalent de la résolution d'une image: plus une image comporte de pixels, plus elle est nette. De la même manière, une fréquence sonore importante garantit la pureté du son. Dans les deux cas, une résolution ou une fréquence plus élevée est également synonyme de consommation mémoire plus importante pour le stockage des médias.

Un autre paramètre est à prendre en compte: le nombre de canaux, autrement dit le nombre d'échantillons pris en même temps à partir de sources différentes. Un son mono ne dispose que d'un canal, le son stéréo offre un canal par oreille, le son 5.1 6 canaux. En imagerie, l'équivalent du son sur deux canaux correspondrait aux lunettes se branchant sur la sortie moniteur d'une carte vidéo. Ces périphériques d'affichage coûteux, aujourd'hui peu utilisées en dehors de la recherche en imagerie, offrent une image différente à chaque oeil pour donner une impression de relief.

À titre d'information, un CD audio a une fréquence d'échantillonage de 44100 Hz, un format d'échantillonnage de 16 bits et deux canaux. Le terme « qualité radio » s'applique quant à lui souvent à une fréquence de 22050 Hz.

Vous le voyez donc, nous allons vite retrouver les marques que nous avons acquises lorsque nous avons appris à manipuler des images.

Jouer un son avec SDL

Ce mois-ci, nous allons simplement étudier l'architecture sonore de SDL et la tester par un programme très basique. Comme pour la vidéo, SDL propose des fonctions de très bas niveau, et il nous faudra les maîtriser avant d'envisager de réaliser des programmes vraiment utiles...

L'utilisation des fonctions audio de SDL passe, comme à l'habitude, par le passage du flag correspondant (ici, SDL_INIT_AUDIO) à la fonction SDL_Init. Puis nous allons définir les paramètres du périphérique audio avec la fonction SDL_OpenAudio:

 int SDL_OpenAudio(SDL_AudioSpec *desired, SDL_AudioSpec *obtained);

Elle prend en paramètre deux pointeurs: l'un pointant vers la description du format sonore désiré, le deuxième étant utilisé pour renseigner le programmeur sur le format sonore effectivement obtenu, pour le cas où le matériel ne supporterait pas le format demandé. Si ce pointeur vaut NULL, et que le format demandé n'est pas supporté par le matériel, SDL met alors en place un mécanisme d'émulation. Le format visible pour le programmeur sera bien le format qu'il a demandé, mais SDL fera une conversion transparente vers le format effectivement supporté avant de l'envoyer à la carte son. La valeur de retour sera 0 en cas de succès, ou -1 en cas d'échec.

La structure SDL_AudioSpec comporte les champs nécessaires à la description du format sonore tel que nous l'avons décrit plus haut:

D'autres champs, non moins utiles, sont disponibles:

Les trois derniers champs méritent quelques explications supplémentaires. Contrairement à l'affichage, pour lequel nous avions un contrôle total sur la fréquence des images affichées (grâce à SDL_Flip, notamment), l'audio requiert de la régularité dans l'envoi des données sonores au matériel. De plus, la fréquence du média sonore est bien plus élevée que pour le média visuel (jusqu'à 44100 Hz pour l'audio, en général 70 Hz pour l'affichage). Il est donc difficilement envisageable d'envoyer manuellement chaque échantillon vers le matériel. À la place, nous allons remplir un tampon audio dans lequel le matériel ira continuellement lire les données sonores. Il est par conséquent très important de remplir ce tampon de manière régulière: s'il n'est plus rempli, le son va « boucler » jusqu'à ce que de nouvelles données y soient placées. Vous avez probablement déjà remarqué ce comportement dans certains jeux, notamment lors de plantages.

La régularité du remplissage du tampon est assurée par la fonction de rappel. Celle-ci est appelée automatiquement par SDL, via un thread dédié, lorsque cela devient nécessaire. Elle reçoit en paramètres le pointeur userdata, un pointeur vers le buffer audio, et le nombre d'octets à y placer. Son rôle, bien souvent, est de mixer les différentes source sonores et de copier le résultat dans le tampon audio.

La fréquence d'appel de la fonction de rappel est directement liée à la taille du tampon audio. Il y a un compromis à trouver ici: plus le tampon audio est petit, plus la fonction de rappel devra être appelée souvent, et plus il y aura de chances de remplir le tampon audio en retard et donc d'obtenir un son haché. Au contraire, un grand tampon garantit un son mixé en temps et en heure – par contre, la latence sera plus importante. Une trop grande latence se traduira par un décalage perceptible entre une action faite par l'utilisateur (par exemple, l'appui d'une touche au clavier) et l'effet sonore qui lui est associé. Cet effet, très désagréable, est intolérable dans un jeu.

Fort heureusement, des tailles de tampon faibles restent exploitables en pratique, et il est ainsi très rare de rater une échéance. Nous pourrons utiliser un taille de tampon de 512 échantillons sans problèmes.

La taille réelle du tampon sonore sera égale au champ samples multiplié par la taille d'un échantillon, fois le nombre de canaux.

Ces précisions devraient nous permettre de remplir correctement la structure SDL_AudioSpec passée à SDL_OpenAudio. Une fois le périphérique audio ouvert, la fonction de rappel n'est pas immédiatement appelée. Le son est mis en « pause » par défaut pour permettre au programmeur d'initialiser ses propres structures sonores. Un appel à SDL_PauseAudio avec un paramètre de 0 suffira pour commencer le mixage sonore.

Le chargement d'un son, via un fichier .wav, est assez similaire à l'ouverture d'une image .bmp. Dans un premier temps, le son est chargé par la fonction SDL_LoadWAV:

 SDL_AudioSpec *SDL_LoadWAV(const char *file, SDL_AudioSpec *spec, 
Uint8 **audio_buf, Uint32 *audio_len);

Le premier paramètre contient le nom du fichier .wav à ouvrir. Le second est un pointeur vers un SDL_AudioSpec qui sera rempli avec le format du fichier chargé. audio_buf est un pointeur qui sera alloué par SDL_LoadWAV et rempli avec les données audio. Enfin, l'entier indiqué par le pointeur audio_len contiendra, à l'issue de l'appel, la taille de audio_buf en octets.

Si le fichier a pu être chargé, SDL_LoadWAV retourne le même pointeur que spec, ou NULL si le chargement a échoué. Les sons ainsi chargés peuvent être libérés par SDL_FreeWAV.

Le format des sons chargés ne sera vraisemblablement pas le même que celui du périphérique audio. Comme pour une image, il faudra passer par une phase de conversion avant d'envoyer nos données au matériel. C'est le rôle de la fonction SDL_ConvertAudio:

 int SDL_ConvertAudio(SDL_AudioCVT *cvt);

La conversion sonore demandant un grand nombre de paramètres, SDL passe par une structure dédiée pour cette opération. La structure SDL_AudioCVT doit être préalablement remplie par un appel à SDL_BuildAudioCvt:

 int SDL_BuildAudioCVT(SDL_AudioCVT *cvt, Uint16 src_format, Uint8 src_channels, int src_rate, 
Uint16 dst_format, Uint8 dst_channels, int dst_rate);

Les paramètres à passer sont la structure à initialiser, suivie des formats, nombre de canaux et fréquences de la source et de la destination. Une fois cette opération réalisée, il reste à copier les données audio à convertir dans le champ buf de SDL_AudioCVT, qu'il faudra au préalable allouer nous-mêmes. La taille à allouer sera égale à la taille du tampon source en octets, multiplié par le champ len_mult de la structure SDL_AudioCVT remplie. Ce facteur multiplicatif permet d'assurer suffisamment de place à SDL pour effectuer la conversion.

Une fois toutes ces étapes accomplies, l'appel à SDL_ConvertAudio transformera les données contenues dans buf du format source vers le format destination. La longueur du nouveau buffer sera égale au champ len_cvt de SDL_AudioCVT.

À présent que notre son est dans le bon format, comment le jouer? Tout simplement en attendant que notre fonction de rappel soit appelée, et en y copiant les données dans le tampon audio. Si notre son est plus long que le tampon audio (ce qui sera vraisemblablement le cas), il faudra procéder en plusieurs fois, chaque appel de la fonction de rappel correspondant à la copie d'un bout du son de la même taille que le tampon audio.

Le programme du mois

Le programme du mois illustre tous ces principes, en jouant un fichier sonore .wav avant de quitter:

 #include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include "SDL.h"

/* Les données du fichier son chargé */
Uint8 * sounddata;
/* La taille du fichier son chargé, en octets */
Uint32 soundlength;
/* Position courante de lecture dans le fichier son */
Uint32 soundpos;

/* Fonction de rappel qui copie les données sonores dans le tampon audio */
void mixaudio(void * userdata, Uint8 * stream, int len)
{
/* Attention à ne pas déborder lors de la copie */
Uint32 tocopy = soundlength - soundpos > len ? len : soundlength - soundpos;

/* Copie des données sonores dans le tampon audio... */
memcpy(stream, sounddata + soundpos, tocopy);

/* Mise à jour de la position de lecture */
soundpos += tocopy;
}

int main(int argc, char * argv[])
{
SDL_AudioSpec desired, obtained, soundfile;
SDL_AudioCVT cvt;

if (SDL_Init(SDL_INIT_AUDIO) == -1) {
printf("Erreur lors de l'initialisation de SDL!\n");
return 1;
}

/* Son 16 bits stéréo à 44100 Hz */
desired.freq = 44100;
desired.format = AUDIO_U16SYS;
desired.channels = 2;

/* Le tampon audio contiendra 512 échantillons */
desired.samples = 512;

/* Mise en place de la fonction de rappel et des données utilisateur */
desired.callback = &mixaudio;
desired.userdata = NULL;

if (SDL_OpenAudio(&desired, &obtained) != 0) {
printf("Erreur lors de l'ouverture du périphérique audio: %s\n", SDL_GetError());
return 1;
}

printf("Paramètres audio obtenus: d canaux, fréquence d échantillons.\n",
obtained.format & 0xff, obtained.channels, obtained.freq, obtained.samples);

/* Chargement du fichier .wav */
if (SDL_LoadWAV("KDE_Startup_new.wav", &soundfile, &sounddata, &soundlength) == NULL) {
printf("Erreur lors du chargement du fichier son: %s\n", SDL_GetError());
return 1;
}

printf("Propriétés du fichier audio: d canaux, fréquence d octets.\n",
soundfile.format & 0xff, soundfile.channels, soundfile.freq, soundlength);

/* Conversion vers le format du tampon audio */
if (SDL_BuildAudioCVT(&cvt, soundfile.format, soundfile.channels, soundfile.freq,
obtained.format, obtained.channels, obtained.freq) < 0) {
printf("Impossible de construire le convertisseur audio!\n");
return 1;
}

/* Création du tampon utilisé pour la conversion */
cvt.buf = malloc(soundlength * cvt.len_mult);
cvt.len = soundlength;
memcpy(cvt.buf, sounddata, soundlength);

/* Conversion... */
if (SDL_ConvertAudio(&cvt) != 0) {
printf("Erreur lors de la conversion du fichier audio: %s\n", SDL_GetError());
return 1;
}

/* Libération de l'ancien tampon, création du nouveau,
copie des données converties, effacement du tampon de conversion */
SDL_FreeWAV(sounddata);
sounddata = malloc(cvt.len_cvt);
memcpy(sounddata, cvt.buf, cvt.len_cvt);
free(cvt.buf);

soundlength = cvt.len_cvt;
printf("Taille du son converti: %d octets\n", soundlength);
soundpos = 0;

/* La fonction de rappel commence à être appelée à partir de maintenant. */
printf("Démarrage de la lecture...\n");
SDL_PauseAudio(0);

/* On attend que l'autre thread ait fini la lecture du son... */
while (soundpos < soundlength);

/* On cesse d'appeler la fonction de rappel */
SDL_PauseAudio(1);

/* Fermer le périphérique audio */
SDL_CloseAudio();

SDL_Quit();
return 0;
}

Comme d'habitude, vous pourrez trouver de programme sur le CD du magazine ou sur http://www.gnurou.org/documents/linuxmag/SDL/SDL-5.tar.gz.

Attention: pour vos tests, il est recommandé de désactiver les démons sonores tels que arts ou esd. SDL sait les utiliser, et ainsi mixer ses sa sortie sonore avec d'autres programmes, mais les tailles de tampon utilisées sont en général très grandes augmentent ainsi grandement la latence, faussant le résultat.

Vous pouvez essayer de changer les valeurs de la structure desired et observer les effets qu'ont la fréquence, la nombre de canaux et le format sur la qualité du son joué et sur la taille des échantillons sonores. Vous ne pourrez pas vous permettre n'importe quelle valeur, notamment pour la fréquence ; mais les classiques 44100, 22050 et 11025 Hz devraient fonctionner correctement.

Ce qui nous attend par la suite: mixage de différentes sources sonores, moteur de son plus sophistiqué et surtout plus simple à utiliser, et effets sur les sons. D'ici là, joyeux code!

Tutoriel SDL: Mixage et effets sonores

Paru dans Linux Mag 73, juillet 2005

La dernière fois, nous avions évalué les fonctions audio mises à notre disposition par SDL. Force est de constater, qu'en s'y limitant strictement on ne parvient pas à des résultats très sophistiqués. Nous allons cette fois-ci construire un véritable moteur sonore à partir des fonctions de base offertes par SDL.

Il ne fallait de toute façon pas s'attendre à des miracles – tout comme pour le graphisme et la gestion des évènements, SDL nous démontre qu'elle est une bibliothèque de bas niveau. C'est au programmeur d'en faire ce qu'il veut – au moins n'est-il pas enfermé dans un carcan prédéfini par les développeurs de la bibliothèque. En ce qui nous concerne, réaliser nous même notre moteur sonore a un intérêt avant tout didactique.

Un moteur de son plus évolué

Notre dernier programme d'exemple se contentait de charger un son, de le convertir au format audio du périphérique, et de le lui envoyer progressivement au travers d'une fonction de rappel appelée par SDL lorsque nécessaire. Nous avions retenu trois paramètres (qui se sont traduits par trois variables globales) pour décrire le son :

 /* Les données du fichier son chargé */
Uint8 * sounddata;
/* La taille du fichier son chargé, en octets */
Uint32 soundlength;
/* Position courante de lecture dans le fichier son */
Uint32 soundpos;

Si nous faisons une analogie avec l'article sur les animations, nous pouvons dire que les variables sounddata et soundlength correspondent aux données du son, tandis que soundpos est un paramètre de lecture. Nous pouvons donc en déduire deux structures comparables à celles que nous avions mises en place pour gérer les animations. L'une, sound, contiendra les données sonores ainsi que la longueur du son. L'autre, soundplayer, pointera vers un son joué et contiendra la position courante du lecteur dans le son. Voilà pour les structures de base, nous pourrons leur associer les fonctions permettant de charger un son déjà codées le mois dernier. Les déclarations se feront dans audio.h :

 #ifndef _AUDIO_H__
#define _AUDIO_H__

#include "SDL.h"

/* Contient un son chargé */
typedef struct
{
/* Données sonores */
Uint8 * data;
/* Longueur du son */
Uint32 length;
} sound;

/* Chargement d'un son à partir d'un fichier .wav. Le son est converti au format
donné par la variable hw_spec, qui doit être le format utilisé par le matériel.
Le son chargé est retourné dans loaded_sound. La fonction ret