Canalblog
Suivre ce blog Administration + Créer mon blog

code-source

code-source
Publicité
Archives
Visiteurs
Depuis la création 162
12 février 2012

Gestion des entrées

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 ici.

#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;
}
Publicité
Publicité
12 février 2012

Introduction et graphisme de base

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&#39;un des buffers est affiché tandis que l&#39;image suivante est préparée dans l&#39;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&#39;é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:

  • SDL_HWSURFACE demande la création de la surface dans la mémoire de la carte vidéo, pour profiter de l'accélération matérielle. Cette demande n'est satisfaite que si possible: par exemple, dans une fenêtre X, SDL ne peut pas créer de surfaces en mémoire vidéo.
  • SDL_DOUBLEBUF demande l'utilisation du double buffering (applicable uniquement si SDL_HWSURFACE est satisfait).
  • SDL_FULLSCREEN demande un mode plein écran. Par défaut, sous X, une fenêtre est créée.
  • SDL_ANYFORMAT informe que nous sommes prêts à accepter un autre format d'image que celui que nous avons demandé, si cela permet d'être plus rapide. Ainsi, sous X, nous aurons toujours le même mode que celui du serveur X si ce flag est activé.

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 ici .

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.

Publicité
Publicité
Publicité