Affichage de sprites avec SDL, partie 1

Un article de Mokona.

Sommaire

[modifier] Introduction

Cet article est le premier d'une série dans laquelle nous allons étudier, depuis le début, l'affichage de sprites avec la SDL. Pour rappel, un sprite est un morceau d'image qui peut être déplacé, déformé et animé sur l'écran.

Avant de commencer la lecture de celui-ci, je vous conseille de lire Première application avec SDL sur ce même site, qui explique les bases de l'initialisation d'un programme en SDL.

Au cours de cette série d'articles, nous verrons comment afficher des morceaux d'images à l'écran, comment les animer, les déplacer avec les touches du clavier, ainsi que probablement d'autres choses, seul le premier article étant écrit pour le moment. Dans cette première partie, nous allons partir d'une application affichant une série d'images, nous allons ensuite voir comment gérer leur vitesse d'affichage, puis comment afficher plusieurs instances d'un même type de sprite animé.

Les trois programmes sont disponibles dans l'archive fournie avec cet article. Afin de les compiler, vous devez avoir la librairie SDL installée et savoir comment compiler un programme avec cette librairie. Si vous utilisez gcc sous un environnement Unix, vous compilerez très simplement chaque programme de la manière suivante : gcc -o sprites1 sprites1.c `sdl-config --cflags` `sdl-config -libs`. Et de même pour les sources sprites2.c et sprites3.c.

[modifier] Première phase

Le but du premier programme est d'afficher une suite d'images qui composeront l'animation de notre sprite. Je ne suis pas graphiste, la séquence d'animation se compose donc tout simplement d'images de 32 pixels sur 32 pixels avec les chiffres 1, 2, 3, 4, 5, 6, 7, 8, 9 et 0 mises bout à bout.

Cela donne ceci :

le sprite animé

Afin d'afficher l'animation, nous pouvons partir du squelette de l'application SDL de base (cf. article précédent) et nous devons y ajouter :

  • le chargement de l'image ;
  • l'affichage cyclique de l'image.

Pour un programme aussi simple, nous mettons tout le programme dans la fonction main().

Voici les variables utilisées :

  • SDL_Surface * sdlMainScreen : sdlMainScreen est un pointeur sur la surface d'affichage. On la récupère par la fonction SDL_SetVideoMode qui initialise l'affichage. Ici, on demande une surface de 640 par 480 pixels avec une profondeur de 16 bits. La surface que l'on demande est en double buffer. Pour la signification des options passées à la fonction, se référer à l'article précédent.
  • SDL_Surface * sdlLoadedPicture : c'est aussi un pointeur sur une surface, mais celle-ci représente l'image que nous allons charger. Chaque accès à l'image fera intervenir cette variable.
  • SDL_Rect sdlSrcSprite : lorsque l'on affiche une image, on a besoin de passer à la fonction d'affichage les dimensions de la surface à recopier de l'image source vers la surface d'affichage. SDL_Rect est une structure qui contient cette information.
  • int displayedSpriteIndex = 0 : nous allons afficher successivement les 10 étapes du sprite. Cette variable contient le numéro de l'étape en cours. Elle est initialisée à zéro.
  • int quitProgram = 0 : drapeau de fermeture du programme. Lorsque cette variable est différente de zéro, la boucle principale s'arrête.

[modifier] Charger l'image

La SDL offre une fonction permettant de lire un fichier au format BMP. De base, on ne peut utiliser que ce format. Il existe des librairies qui étendent la SDL pour lire d'autres formats, et la SDL elle-même est assez versatile pour que l'on puisse soit même implémenter le chargement de formats autres. Nous ne nous servirons ici que des fonctions de base de la SDL, nous chargeons donc une image BMP.

La fonction prend en paramètre le nom de l'image, et renvoie un pointeur sur une SDL_Surface. C'est tout simple. On vérifie juste que le pointeur est bien différent de NULL, ce qui signifierait qu'une erreur est apparue lors du chargement.

Attention : dans un programme plus complexe, il faudra penser à libérer la mémoire utilisée par la surface, en utilisant la fonction SDL_FreeSurface(). Ici, nous nous en passerons. Dans le troisième programme de cet article, nous nous en servirons.

Juste avant le début de la boucle principale, on pré-remplie une partie de la variable sdlSrcSprite, de type SDL_Rect. En effet, à chaque fois qu'un sprite sera affiché, il aura des dimensions de 32 par 32 pixels, et son coin supérieur gauche est en haut de l'image. C'est ce que signifie le code suivant.

sdlSrcSprite.w = 32;
sdlSrcSprite.h = 32;
sdlSrcSprite.y = 0;

[modifier] Boucle principale

Nous n'observons ici qu'un seul événement : la demande de fermeture du programme.

Puis on affiche le sprite en lui-même. Pour cela, on doit trouver la position dans notre image de l'étape actuelle que nous voulons afficher. La position est contenue dans la variable displayedSpriteIndex, et les étapes font chacunes 32 pixels de large. La position en x de l'image à afficher est donc displayedSpriteIndex*32. L'affichage en lui-même se fait par la fonction SDL_BlitSurface. Cette fonction recopie un morceau d'une SDL_Surface vers une autre SDL_Surface. Cela tombe bien, nos deux éléments graphiques, l'image contenant les sprites et la surface d'affichage sont de type SDL_Surface.

SDL_BlitSurface prend quatre paramètres.

  • premier paramètre : il s'agit de la surface source. Ici, nous prenons notre sprite dans l'image que nous avons chargée, le paramètre est donc sdlLoadedPicture ;
  • second paramètre : c'est un pointeur vers une variable de type SDL_Rect qui représente le rectangle que nous allons « découper » dans la surface en premier paramètre pour la copier ;
  • troisième paramètre : il s'agit de la surface cible. Ici, c'est la surface d'affichage : sdlMainScreen ;
  • quatrième paramètre : c'est aussi un pointeur vers une variable de type SDL_Rect, qui donne la position dans la surface cible vers laquelle sera copiée l'image. Ici, le paramètre mis à NULL signifie que l'on ne s'en préoccupe pas, et que la position sera (0,0).

Maintenant qu'on a affiché cette étape, on incrémente le numéro de l'étape d'animation a afficher et on teste s'il n'a pas dépassé le numéro de la dernière étape, auquel cas on le remet à zéro.

Un appel à SDL_Flip() permet l'inversion du double buffer, et c'est repartie pour un tour.

[modifier] Résumé

Ce programme a initialisé une surface d'affichage, a chargé une image contenant plusieurs sprites représentant une « animation » puis les a affiché tour à tour par « blit ». Ce programme très basique présente un problème évident que nous allons corriger par la suite : la vitesse de l'animation n'est pas régulée, elle va aussi vite que l'ordinateur peut aller. La seconde étape va présenter une méthode permettant de régler ce problème.

[modifier] Deuxième phase

Dans ce deuxième programme, nous allons présenter une manière de contrôler la vitesse de l'animation de sprite.

Beaucoup seraient tentés de chercher comment mettre une boucle d'attente après le SDL_Flip(), pour ralentir l'exécution. Voici comment cela pourrait être fait :

int heure_actuelle;
heure_actuelle = SDL_GetTicks();
while(heure_actuelle +1000 > SDL_GetTicks());

SDL_GetTicks() retourne le nombre de millisecondes écoulées depuis l'initialisation de la SDL. Ici, on note la valeur actuelle, puis on boucle jusqu'à ce que cette valeur plus 1000 soit dépassée par la nouvelle « heure ». On attend donc une seconde avant de continuer le programme.

Il est à noter que SDL fournit aussi une fonction SDL_Delay(int nombreMs) qui fait attendre le programme nombreMs millisecondes. Toutefois, du fait que la précision de cette fonction dépend fortement de la charge de l'ordinateur son usage n'est pas adapté pour assurer un taux de rafraîchissement régulier.

C'est peut-être une bonne idée si votre programme ne fait qu'afficher un seul sprite, mais je suis persuadé que votre programme fera autre chose, comme afficher d'autres sprites, calculer des déplacements, jouer des sons,...

Nous allons donc faire légèrement différemment. Plutôt que de bloquer le programme avec une boucle, on va effectuer le test seul. Si le test est vrai, on ne fait rien de spécial, on continue une nouvelle boucle d'affichage complète. S'il est faux, alors c'est qu'il faut modifier le numéro de l'étape d'animation du sprite.

C'est ce qui est fait dans :

if(currentTime>nextUpdateTime)
{
  /* On sélectionne le sprite suivant */
  displayedSpriteIndex++;
  if(displayedSpriteIndex>9)
  {
    displayedSpriteIndex = 0;
  }
  /* On fixe la date du prochain changement : une demi seconde après */
  nextUpdateTime = currentTime + 500;
}

On a deux variables, initialisées avant la boucle une première fois. La variable currentTime contient l'heure actuelle, obtenue avec SDL_GetTicks(). La variable nextUpdateTime contient l'heure à laquelle devra être effectué le prochain changement d'étape d'animation du sprite.

Si currentTime dépasse nextUpdateTime, c'est qu'il est temps de changer. On incrémente displayedSpriteIndex, on test si la valeur maximale n'a pas été atteinte, et on fixe la nouvelle heure de changement d'état, ici 500 millisecondes plus tard.

Puis on continue.

[modifier] Résumé

Nous avons à présent une méthode qui permet de régler la vitesse d'animation d'un sprite sans bloquer le fonctionnement du programme. Parfait, nous allons donc pouvoir afficher plusieurs sprites animés !

[modifier] Troisième étape

Dans cette dernière étape de cet article, nous allons afficher plusieurs instances du même sprite animé. Chaque instance possède sa propre position sur la surface d'affichage ainsi que sa propre vitesse. Grâce à l'étape précédente, nous allons pouvoir gérer tous ces sprites indépendamment.

Afin de stocker ces informations simplement, nous utilisons une structure 'stSprite' dont la définition est la suivante :

struct stSprite {
  SDL_Surface * source;
  int numStep;
  int destX;
  int destY;
  int speed;
  int nextUpdateTime;
};
  • source est le pointeur vers la surface de l'image chargée contenant les images du sprites ;
  • numStep est le numéro de l'étape d'affichage. En effet, comme nous voulons que les vitesses des sprites soient indépendantes, chacun des sprites aura son propre état actuel ;
  • dextX et destY sont les coordonnées du sprite dans la surface d'affichage ;
  • speed est une variable un peu mal nommée, il s'agit en fait du nombre de millisecondes entre deux états de ce sprite ;
  • nextUpdateTime est l'heure du prochain changement d'étape pour ce sprite.

La fonction main() est allégée de tout ce qui concernait directement le chargement, la mise à jour et l'affichage du sprite. Tout cela est remplacé par un tableau de pointeurs sur des structures de type stSprite. Le tableau a une taille de NB_SPRITES, qui est le nombre de sprites que nous allons afficher.

Ce tableau est rempli en appelant la fonction createRandomSprite() qui a pour paramètre le pointeur vers la surface de l'image chargée. La fonction createRandomSprite() se charge de :

  • allouer la place pour la structure du sprite, avec malloc() ;
  • initialiser les valeurs de ce sprite. La vitesse et les coordonnées sont des nombres aléatoires.

À propos de ces nombres aléatoires, nous avons initialisé le générateur de nombres aléatoire avant la création de sprites, avec srand(SDL_GetTicks()), et une fonction getRandom() a été ajoutée pour tirer un nombre entier entre deux bornes.

La boucle principale du programme doit maintenant afficher chaque sprite contenu dans le tableau. Pour cela, nous faisons une boucle qui traverse le tableau et appelle la fonction sdlDisplaySprite() avec pour paramètres la surface d'affichage, la structure du sprite concernée et l'heure actuelle, que nous avons obtenue juste avant la boucle.

Note : ne relever qu'une seule fois la date actuelle est non seulement plus efficace (car cela évite d'appeler trop souvent SDL_GetTicks()) mais permet de s'assurer que tous les sprites sont modifiés avec la même heure.

La fonction sdlDisplaySprite() fait un peu plus que son nom l'indique. En effet, elle commence bien avec l'appel de SDL_BlitSurface(), en précisant, contrairement au programme précédent, les coordonnées d'affichage dans la surface d'affichage, mais elle enchaine sur la mise à jour de la structure.

La mise à jour est fait de façon identique à ce que l'on a fait dans le programme précédent. Mais les variables utilisées pour le test et la mise à jour sont celles de la structure de sprite. Ainsi chaque instance de ce sprite est indépendante.

[modifier] Petit ajout

À la fin de ce troisième programme, on trouve la libération de la mémoire allouée lors du programme. Ça n'est pas indispensable dans un programme aussi court, mais prenons à présent les bonnes habitudes, car ce programme commence à prendre de l'importance.

Une première boucle se charge de libérer, avec free(), les structures allouées par malloc(), puis un appel à SDL_FreeSurface() permet de libérer l'espace alloué pour le chargement de l'image.

Nous ne nous occupons pas de la surface d'affichage.

[modifier] Résumé

Dans cette troisième phase, nous avons vu que grâce à une temporisation qui ne bloque pas le programme, nous pouvons afficher plusieurs fois le même sprite dans des états différents, et de façons indépendantes les unes des autres.

Les problèmes : vous ne l'avez pas forcément vu tout de suite, mais ce troisième programme présente un nouveau problème. Afin qu'il soit beaucoup plus visible, augmentez le nombre de sprites affichés en modifiant NB_SPRITES. Mettons à 100.

Vu ?

Les sprites qui s'affichent l'un sur l'autre s'écrasent ! En effet, nous affichons le sprite dans son intégralité, avec la couleur noire comme une vraie couleur noire, et aucune couleur « transparente ». Ça n'est pas génant si les sprites que nous voulons afficher ne s'affichent pas les uns sur les autres, mais cela sera rarement le cas, à moins d'avoir un sprite parfaitement rectangulaire, sans transparence.

Le deuxième problème n'est pas visible dans ce troisième programme, mais le serait si nous voulions bouger les sprites sur l'écran. Contrairement à l'article précédent, nous n'effaçons pas la surface d'affichage à chaque début de boucle. Résultat, si les sprites bougeaient, ils laisseraient derrière eux une trace.

Nous verrons comment améliorer ça dans l'article suivant.

[modifier] Améliorations

En attendant l'article suivant, je vous propose de vous pencher sur ce troisième programme et de l'améliorer. Souvenez-vous, dans le premier et second programme, nous remplissions à l'avance les SDL_Rect nécessaires à l'affichage, alors qu'ici, pour un soucis de simplicité de compréhension, les deux SDL_Rect sont re-remplis à chaque appel de sdlDisplaySprite(). Comme vous vous en doutez, c'est hautement innefficace, et l'on peut bien entendu précalculer certaines choses. À vous de voir comment.

À bientôt pour la seconde partie de cette série d'articles.

[modifier] Questions ?

Voir la FAQ des tutoriaux SDL, ou utilisez l'onglet discussion en haut de cette page.

Écrit par Mokona pour Prografix, 20030428


Ce document a été publié sur la version 3 du G.C.N. par Mokona.

  • Auteur Original : Mokona
  • Date de publication : 28 Avril 2003

(aucun commentaire actuellement)