Affichage de sprites avec SDL, partie 4

Un article de Mokona.

Sommaire

[modifier] Introduction

Dans la partie précédente, nous avions obtenu à l'écran des sprites dont l'animation change en fonction de leur direction. Nous avions aussi affiché un mini-texte, constitués de chiffres, représentant la performance globale du programme.

Dans cette quatrième partie, nous allons étudier l'affichage d'un fond grâce à l'utilisation de tuiles. En anglais, les tuiles sont connues sous le noms de « tiles » et les fonds constitués se nomment « tilemaps ». Les noms de cartes, tuiles, tilemaps et tiles seront utilisés.

Avant de commencer la lecture de cette partie, je vous conseille de lire bien évidemment les trois premières parties de cette série d'articles.

Les quatre 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 sprites11 sprites11.c `sdl-config --cflags` `sdl-config -libs`. Et de même pour les sources sprites12.c, sprites13.c, sprites14.c et sprites15.c.

[modifier] Première phase

[modifier] Première approche des tuiles

Les fonds dans une certaine catégorie de jeux sont fabriqués à partir de tilemaps. Les tilemaps partent du principe que beaucoup d'éléments du décors sont redondants, et qu'il est donc possible de factoriser l'affichage en réutilisant des petits bouts de dessins : les tuiles, ou tiles.

Les tuiles sont numérotées et une carte d'utilisation de ces tuiles est stockées. Ainsi, plutôt que d'avoir une grande image consomatrice de mémoire et longue à afficher, on reconstruit l'image à partir des tuiles et de la carte.

Un petit exemple. Dans ce tutorial, nous allons utiliser quelques tuiles très simples. Une tuile de fond, bleue, et des tuiles pour dessiner les murs. J'ai dessiné les dix tuiles que voici :

Tuiles

Et voici la carte complète créée avec les dix tuiles :

Carte entière

L'image est réduite, elle fait en réalité 1024 pixels par 1024 pixels.

Afin de créer cette carte, j'ai utilisé le logiciel GmBaAp tile/map editor. Il est simple d'accès et permet de configurer par des scripts Python la manière dont il exporte ses données.

Pour ce tutorial, j'ai gardé l'export par défaut, qui met tout dans un header. J'ai légèrement modifié le format de sortie pour qu'il me soit plus lisible. Dans un programme complet qui utilise la SDL sur une machine avec un disque dur, vous ne voudrez certainement pas faire comme cela, mais charger vos données depuis le disque. Comme cela n'est pas notre sujet, mettre les données dans le header (dont le nom est data_header.h) permet d'avoir les données en accès direct, car elles seront compilées dans l'exécutable. L'accès se fera grâce à des pointeurs.

C'est partie pour les changements dans le source. Nous travaillons avec sprites11.c.

On commence par nettoyer tout ce qui concerne l'hélicoptère des chapitres précédent, il ne nous intéresse pas (en fait, on enlève les appels aux fonctions de l'hélicoptère, mais on garde les fonctions). On garde par contre l'affichage des chiffres animés et des FPS. Puis on ajoute une structure décrivant la carte.

struct stMap {
  unsigned char * data;
  int width;
  int height;
  int displayWidth;
  int displayHeight;
  SDL_Surface ** tiles;
};
  • data est un pointeur sur les indexes de tuiles qui constituent la carte ;
  • width et height sont les largeurs et hauteurs de la carte, en tuiles ;
  • displayWidth et displayHeight sont les largeurs et hauteurs de la carte que l'on veut afficher, en tuiles aussi. Les tuiles faisant 16 pixels sur 16 pixels, il s'agit donc dans notre cas des tailles de la fenêtre divisées par 16 ;
  • tiles sera alloué comme un tableau de pointeurs sur les différentes surfaces dans lesquelles nous chargerons.

Une variable mapBackground de type stMap est créée dans la boucle principale et intialisée tel que décrit ci-dessus à partir des valeurs contenues dans le fichier de données.

Voilà pour la carte, il nous faut nous occuper des tuiles. Il y a dix tuiles à charger, et nous créons donc un tableau de dix pointeurs sur SDL_Surface que nous nommons sdlLoadedTiles dans la boucle principale.

Pas de mystère pour la fonction sdlLoadTiles, qui construie les surfaces à partir des données. À noter la présence de SDL_LockSurface et SDL_UnlockSurface qui sont appelées si la surface l'exige. Dans certains cas, la surface doit en effet être « lockée », c'est à dire bloquée en mémoire, avant d'y accéder directement. C'est le cas par exemple si c'est une texture hardware.

On ajoute bien entendu aussi une fonction pour libérer ces surfaces à la fin du programme.

Ce genre d'initialisations ne devrait plus vous poser de problèmes à présent, à part peut-être quelques détails syntaxiques sur les pointeurs.

Reste l'affichage de la carte à l'écran. Pour cela, une fonction sdlDisplayMap est créée et appelée dans la boucle principale avant l'affichage des sprites.

Dans sprites11.c, nous n'affichons que le coin haut de la carte de manière à couvrir toute la fenêtre. La carte ne bouge pas à l'écran. Si vous avez bien compris le principe de ces cartes, il n'est pas bien compliqué de l'afficher : une double boucle se charge d'examiner toutes les positions d'affichage dans la fenêtre, en se basant sur les champs displayHeight et displayWidth de la structure de la carte.

(Il est à noter au passage que les coordonnées (0,0) se situent en haut à gauche de la carte, et que toutes les coordonnées sont positives en X et en Y).

Pour chaque position examinée, on trouve l'index de la carte à lire (index = x + (y * map->width);, width étant la largeur de la carte en tuiles) et on va ainsi lire le numéro de la tuile à afficher. Un SDL_BlitSurface avec les coordonnées de destination fait le reste.

Attention ! Cette fonction n'est pas protégée. C'est à dire qu'il n'y a pas de tests sur la validité du numéro de tuile que l'on veut afficher. S'il dépasse dix, la fonction va faire n'importe quoi, il n'y a pas plus de dix tuiles dans le tableau de surfaces que l'on a chargé. Ayez cela en tête si vous bidouillez la fonction pour votre propre utilisation.

[modifier] Deuxième phase

[modifier] Le fond bouge

Dans sprites12.c nous allons commencer à faire bouger le fond. Commencer car le déplacement sera grossier.

Pour cela, nous ajoutons dans la structure stMap quatre champs. Deux pour les coordonées que nous voulons atteindre, et deux pour les coordonnées courantes que nous devons afficher. Les coordonnées représentent toutes deux des coordonnées au milieu de l'écran. C'est un fonctionnement comme un autre, mais c'est celui que j'ai choisi car souvent, il faut déplacer la carte pour centrer à l'écran un sprite particulier.

Si nous voulons aller aux coordonnées (50,64), cela signifie donc que nous voulons placer la position (50,64) de la carte au milieu de la fenêtre. Ces positions sont en pixels, en non en tuiles.

Ces coordonnées seront toutes initilisées à (0,0). Comme cela commence à faire pas mal de données à initialiser dans la carte, et que c'est de toute façon plus propre, nous faisons tout cela dans une fonction du nom de sdlInitMap. Cette fonction prend en paramètres la structure à remplir, la structure des données générée par le programme d'édition de tuiles, et les surfaces contenant les tuiles.

De même que l'on met à jour les sprites, nous allons maintenant mettre à jour la carte grâce à une fonction sdlUpdateMap qui prend en paramètre une carte.

Cette fonction débute avec un test : sommes-nous arrivés à destination ? Si les coordonnées cibles et les coordonnées courantes sont identiques, alors c'est oui, et il est procédé à un nouveau tirage aléatoire de coordoonées cibles. Les coordonnées sont tirées entre 0 et 1024, qui sont les coordonnées limites de la carte. Si vous levez comme objection que ces données sont en dur et qu'il faudrait plutôt les lire dans les données de la carte, vous avez parfaitement raison.

Ensuite, on fait un petit calcul pour trouver la nouvelle position de la carte qu'il faudra centrer à l'écran. On commence pour cela par calculer la différence entre la position courante et la position à atteindre et l'on divise cette différence par dix. Ces valeurs donnent le déplacement que l'on va opérer pour obtenir la nouvelle position, elles sont donc ajoutées aux coordonnées.

Il reste une petite chose à faire. En effet, en l'état, la position courante n'atteindra jamais la position cible. Comme nous faisons une division entière, lorsqu'un distance sera inférieure à dix, une fois divisée, elle sera égale à zéro. La carte ne bougera plus. Si une distance est inférieure à dix, on considère que le déplacement est la distance non divisée.

Encore une fois, c'est un choix de fonctionnement. Vous pouvez expérimenter d'autres manières d'atteindre la position cible. Vous pouvez tourner autour si cela vous chante. Vous pouvez aussi expérimenter des distances calculées avec des float pour éviter la division entière. Attention cependant aux flottants, ils entrainent d'autres problèmes. Expérimentez, c'est la meilleure manière de comprendre.

Passons à l'affichage, sdlDisplayMap doit être modifiée afin d'afficher la carte de façon à centrer la position courante sur la fenêtre.

Nous voulons connaître les coordonnées de la carte à afficher au centre de l'écran en tuiles. Nos tuiles faisant 16 pixels par 16 pixels, ces coordonnées sont obtenues en divisant par 16 les coordonnées en pixels contenues dans la structure. Comme la fonction va afficher les tuiles en partant du coin haut à gauche de la fenêtre, nous déduisons les coordonnées de cette tuile.

startTileX -= map->displayHeight >> 1;
startTileY -= map->displayWidth >> 1;

Pour ceux qui ne seraient pas encore très à l'aise avec ce genre de syntaxe C, voici le détail. L'opérateur >> opère un décalage de bits vers la droite. En binaire, décaler d'un bit vers la droite équivaut à diviser par deux. Ainsi, a >> 1 est la même chose que a / 2. Voyez un cours sur la représentation binaire des entiers pour mieux comprendre.

La syntaxe a -= b retranche b de a, cela évite d'avoir à écrire a = a - b.

Il n'est pas indispensable d'utiliser ces opérateurs, on aurait très bien pu écrire startTileX = startTileX - (map->displayHeight / 2);. Mais l'opérateur -= permet plus de concision et l'opérateur >> est une habitude qui vient du temps où les compilateurs n'optimisaient pas directement les divisions évidentes sous forme de décalages.

Refermons cette parenthèse, nous voici donc avec les coordonnées dans la carte de la tuile à afficher en haut à gauche. Il reste donc a modifier la double boucle sur les coordonnées pour afficher toutes les tuiles, comme précédement, mais cette fois en décalant les coordonnées.

Un test sur les coordonnées a aussi été ajouté. En effet, il faut vérifier avant de tenter d'afficher une tuile que les coordonnées carte que nous voulons sont valides. Imaginons que les coordonnées à afficher soient (0,0), ce qui est d'ailleurs le cas au démarrage. Les coordonnées de la carte en haut à gauche sont alors (-20,-15). Il n'y a pas de coordonnées (-20,-15) sur la carte ! Il ne faut donc rien afficher. Si les coordonnées dépassent de la carte, nous n'affichons donc pas de tuile.

Et voilà, le fond bouge ! Il scroll, pour employer l'anglicisme.

Seulement voilà, cela n'est pas bien fluide : le déplacement accroche, et n'est pas très agréable, on peut mieux faire. Est-ce un problème de vitesse ? À moins que vous utilisiez une très vieille machine (et non, je ne considère pas un PC à 400Mhz une très vieille machine), ce n'est pas un problème de vitesse. C'est tout simplement que l'affichage se fait avec une précision à la tuile. Nous avons en effet calculé quelle était la tuile à afficher au centre, et nous n'avons affiché que des tuiles complètes. Le déplacement interne de la carte, déterminé par sdlUpdateMap, est lui calculé en pixels. Résultat, le scrolling « saute ».

Nous allons régler ce problème avec sprites13.c.

[modifier] Troisième phase

[modifier] Le fond bouge mieux

Dans le programme précédent, le calcul des tuiles à afficher était arrondi à la tuile la plus proche. Cela provoquait des effets désagréables dans le mouvement. La solution est simple : afficher la carte des tuiles pour afficher au centre de la fenêtre les coordonnées voulues exactes et non arrondies.

Il n'y a pas grand chose à modifier dans sprites12.c pour obtenir sprites13.c, tout se passe dans sdlDisplayMap().

La tuile de départ, celle de haut à gauche, est calculée de la même manière, même si les noms des variables sont changés pour garder au passage les coordonnées de la tuile centrale.

Ce qu'il faut trouver ensuite, c'est le décalage en X et en Y nécessaire à l'affichage de la carte. C'est l'offset de la carte. Pour trouver ce décalage, on commence par remultiplier les coordonnées de la tuile centrale par 16. Cela revient en fait à trouver les coordonnées en pixels que l'on affichait dans sprites12.c. En faisant la différence, en X et en Y, avec les coordonnées que l'on veut vraiment avoir au centre de la fenêtre, on trouve le décalage qu'il faudra appliquer à l'affichage.

Il suffit ensuite, pour chaque tuile affichée, de décaler les coordonnées d'affichage de l'offset que l'on vient de calculer.

Et voilà, le scrolling de la carte est fluide. À noter cependant que le déplacement n'est pas calculé en fonction de la vitesse d'exécution du programme, comme nous l'avions fait pour le déplacement des sprites. Ce n'est pas une bonne manière de faire, car le scrolling sera fonction directe de la rapidité d'exécution, ce qui n'est généralement pas souhaitable. Nous réglerons cela plus tard. Ou bien vous pouvez le régler en exercice.

[modifier] Quatrième phase

[modifier] Les sprites changent de repère

Pendant le scrolling précédent, nous avions toujours les chiffres qui se baladaient à l'écran. C'est une bonne base pour, à terme, remplacer ces chiffres par des éléments de jeux, comme des personnages. Mais, problème, ces chiffres se déplacent dans la fenêtre et ne sont pas affectés par les déplacements du fond.

Nous allons donc ajouter aux sprites la capacité de se déplacer dans le « monde ». Plutôt que de donner les coordonnées dans le repère de la fenêtre, on les donne par rapport à la carte, qui représente le monde. Cependant, on veut garder les coordonnées relatives à la fenêtre, pour pouvoir afficher les FPS par exemple. La structure se voit donc ajouter un nouveau champ, int worldSprite qui sera à 1 si les coordonnées sont exprimées dans le monde, 0 sinon (mis par défaut dans la construction du sprite).

Le fonctionnement du sprite change donc suivant le type et sdlUpdateSprite() doit être modifié en conséquence. Les sprites dans la fenêtre rebondissent sur les bords de la fenêtre. Les sprites dans le monde rebondissent sur le bord du monde. Les limites inférieures sont toujours zéro, mais les limites supérieures sont décidées en fonction du type du sprite (boundMaxX et boundMaxY). L'algorithme de rebond en lui même ne change pas.

La fonction d'affichage de sprite sdlDisplaySprite() doit elle aussi être modifiée. En effet, les coordonnées d'un sprite dans le monde doivent être transformées en coordonnées fenêtre pour être affichées et cette transformation nécessite de connaître la position de la carte dans cette fenêtre.

La première modification est donc d'ajouter un paramètre à cette fonction : une carte de type struct stMap *. La seconde modification est de prendre en compte la carte si le sprite est de type monde. Il suffit dans ce cas de soustraire aux coordonnées du sprite les coordonnées actuelles du monde. Cela donne les coordonnées fenêtres des sprites. Reste à afficher le sprite avec ces coordonnées fenêtre.

Bien entendu, si le sprite n'est pas en coordonnées monde, celles-ci sont utilisées directement.

Afin de mieux voir les sprites, qui vont couvrir toute la carte, le nombre de sprites, déterminé par le #define au début du programme, est passé à 100. Comme la construction des sprites aléatoires n'a pas été changée, les cent sprites seront au début générés dans la partie en haut à gauche de la carte. Par la suite, ils iront se balader dans le reste du monde.

Au niveau des initialisations, il faut bien entendu mettre le champ worldSprite à 1 pour tous ces sprites contenus dans arraySprites. On ne touche par contre pas aux sprites des chiffres qui servent à afficher les FPS.

[modifier] Une optimisation légère de l'affichage

Certains l'auront peut-être déjà remarqué, sdlDisplayMap fait des chose inutiles. La première est de balayer l'ensemble des coordonnées de la fenêtre, ce qui entraine un test pour savoir si oui ou non, la tuile vaut la peine d'être affichée. La seconde est de recalculer la transformation des coordonnées tuiles en coordonnées pixels à chaque fois.

Prenons tout d'abord le problème des boucles trop grandes. Dans sprites13.c, il a une boucle qui parcourt les abscisses, une autre les ordonnées sur l'intégralité des tuiles de la fenêtre. Cependant, dans notre cas, il peut très bien n'y avoir aucune tuile à afficher ; lorsque la carte est affichée près de l'un de ses bords. Dans sprites13.c, la solution est de vérifier si les coordonnées de la tuile qui devrait être affichée est bien valide au sens de la carte. Sinon, on n'affiche rien.

Cela fait beaucoup de tests inutiles. Il serait plus intéressant de donner aux boucles des coordonnées de départ et d'arrivée valides. Ainsi, on s'assure que toutes les tuiles que l'on veut afficher sont bien valides.

Pour cela, on utilise quatre nouvelles variables : loopStartX, loopStartY, loopEndX et loopEndY. Elles serviront de bornes aux deux boucles. Les deux loopStart sont calculées de la même manière, en fonction des coordonnées de la tuile en haut à gauche, que l'on calcule déjà : si l'une des coordonnées est supérieure à zéro, alors la boucle associée devra démarrer à zéro car cela signifie que la carte dépasse de la fenêtre dans cette direction. Si par contre une des valeurs est négative, la boucle doit commencer à l'opposé de cette valeur. Par exemple, si le X de la tuile en haut à gauche est de -3, la boucle devra commencer à 3, qui est la première abscisse valable (3 - 3 = 0) dans la carte.

Note : nous devons vérifier la validité des coordonnées car, dans ces exemples, la carte peut être affichée partiellement dans la fenêtre. Un autre moyen d'éviter ce problème est d'interdire à la carte de s'afficher partiellement, en changeant la mise à jour de la carte. C'est ce que font beaucoup de jeux ; on ne voit jamais les bords de la carte.

L'autre optimisation légère est d'éviter de calculer à chaque fois les coordonnées en pixels. C'est une optimisation très classique. Dans la boucle précédente, on multiplie systématiquement le numéro de l'itération (x ou y) par 16, et on y ajoute l'offset. Une multiplication est une suite d'additions, alors pourquoi ne pas faire uniquement une addition par tour de boucle ?

C'est ce qui est fait. Le destY initial, celui de la première ligne, est calculé avant la boucle sur y et destX est calculé pour la première colonne, avant la boucle sur x.

Puis à chaque itération de x, on ajoute 16 à l'abscisse. À chaque itération de y, on ajoute 16 à l'ordonnée.

Note : la variable index pourrait être sujet au même type de calcul.

Autre note : c'est là une micro optimisation. Si votre programme tourne lentement, ce n'est probablement pas à cause de détails de ce dernier type. À moins qu'il y en ait vraiment beaucoup.

Les sprites se meuvent à présent sur toute l'étendue de la carte, la carte scroll de manière fluide. Nous avons atteint notre but.

[modifier] Bonus stage

Pourrait-on aller plus loin ? Oui. Cet article donne des pistes pour faire des choses assez communes dans la programmation 2d, mais tout n'est pas exploré dans les moindres détails. De plus, il est très probable que lors d'une programmation complète, des algorithmes soient plus ou moins adaptés à ce programme en particulier. Le choix va dépendre du nombre de sprites à afficher, du nombre de ces sprites qui bougent, de la dynamique de la carte, est-ce qu'elle bouge, est-ce qu'elle a des éléments qui changent,...

Il faut aussi prendre en compte les limitations du hardware. Nous allons voir rapidement dans sprite15 une optimisation qui vous est peut-être venue à l'esprit, mais qui n'est pas forcément intéressante.

L'idée de cette optimisation vient du fait que notre carte n'est pas dynamique dans le sens où aucun de ses éléments ne change. La tentation est donc grande : pourquoi ne pas construire une fois pour toute une surface de taille de la carte, d'y dessiner la carte, et d'utiliser cette surface pour le fond.

C'est ce que fait sprite15 que je vous invite à regarder mais que je ne commenterai que brièvement. Lors de l'initialisation de la carte (sdlInitMap), une surface de la taille de la carte est créée. Dans l'exemple, elle est créée en 32 bits car mon écran est en 32 bits. Attention cependant, cette carte pour être optimale doit avoir la même profondeur que la fenêtre. C'est une surface soft.

Dans sdlDisplayMap, on teste un drapeau qui, s'il n'est pas mis, provoque l'affichage de la carte complète dans la surface annexe, puis met le drapeau. Dans tous les cas, cette surface annexe est affichée.

En moyenne, il y a donc beaucoup moins de calculs dans le sdlDisplayMap de sprite15. Et pourtant, sur ma machine, le programme tourne plus lentement que sprite14. Et pas qu'un peu ! Globalement deux fois moins vite.

Que se passe-t-il ?

Il se passe que la surface annexe est énorme. Dans la version sprite14, on affiche plein de petites tuiles, mais on n'affiche que les tuiles qui vont être visibles. Dans sprite15, on n'affiche qu'une surface, mais c'est une surface nettement plus grande que la fenêtre. Cela fait beaucoup de pixels à bouger et à afficher pour rien.

D'accord ! Mais j'ai fait des surfaces software. En hardware, cela devrait être plus rapide !

Je n'ai pas inclus le programme, vous pouvez adapter sprite15 pour cela. Attention de mettre la surface annexe ET la surface de la fenêtre en hardware, sinon, cela sera encore pire.

Là, le résultat va dépendre de votre carte et de votre driver. Certains ont fait des mesures sur la mailing list de la SDL et ont trouvé que cette méthode restait plus lente. Au mieux, elle avait les mêmes performances qu'une version où les tuiles étaient en hardware.

Mais il y a un autre problème à créer la surface annexe en hardware : la mémoire ! Tout le monde n'a pas une carte vidéo avec beaucoup de mémoire. Et cette mémoire va vite être consommée par ce genre de surfaces annexes.

Voilà la raison de la note de la section précédente sur la micro optimisation. Changer la multiplication en addition soulage un tout petit peu le code et le CPU. Mais il est surtout intéressant d'optimiser l'affichage en ayant en tête ce qu'il se passe « derrière » : minimiser les échanges avec la carte vidéo, ne pas utiliser de trop grosses surfaces inutilement,...

[modifier] Conclusion

Nous voici à présent avec un programme qui fait défiler un fond dans tous les sens. Nous ne sommes plus loin d'une gestion bien sympathique de sprites permettant d'implémenter un jeu 2d. Il reste à donner du sens au fond, à pouvoir bouger les sprites à l'aide du clavier (d'un pad, de la souris ou d'un autre moyen).

N'oubliez pas que cet article est inscrit dans une série dans laquelle le programme exemple évolue. Pris en cours de route, ce programme peut sembler étrange, avec des fonctions qui ne servent pas. Un article futur proposera une mise à plat de tout ce qui a été vu.

À bientôt.

Écrit par Mokona pour Prografix, 20031228

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

  • Auteur Original : Mokona
  • Date de publication : 28 Décembre 2003

(aucun commentaire actuellement)