Date de première publication : 2015/11/3
Vous allez ajouter une couche graphique au jeu dont vous avez développé le moteur texte.
Les phases d'un programme SDL2 sont des phases "classiques" :
- Déclarations et initialisations générales
- Connexion graphique et initialisations graphiques spécifiques
- Boucle principale pour la gestion des événements
- Libération des ressources
La bibliothèque SDL2 est installée sur les serveurs étudiants LINUX comme "nightmare".
Si vous voulez exécuter un programme graphique à distance, n'oubliez pas que vous avez besoin d'une serveur graphique et d'utiliser ssh -X
ou ssh -Y
Pour compiler un programme utilisant la SDL2, des informations supplémentaires sont requises à la compilation suivant vos besoins (la bibliothèque principale et des "composants" supplémentaires optionnels).
- Tout d'abord, il faut inclure les bons fichiers d'entête comme on le verra dans la partie suivante. Si ces fichiers ne sont pas dans un répertoire "système", il faudra préciser leur emplacement avec l'option
-I chemin
à la compilation - Ensuite, il faudra préciser les bibliothèques que vous utilisez à l'édition des liens :
gcc prog.c -o prog -lSDL2 -lSDL2_gfx -lSDL2_image -lSDL2_ttf -lSDL2_mixer -lSDL2_net
Là encore, suivant l'installation, il faut parfois spécifier le chemin des bibliothèques avec -L chemin
. Ce n'est pas nécessaire pour l'utilisation à l'ISIMA ! On n'utilise ni mixer ni net dans les TPs.
SDL2
est la bibliothèque principale, c'est la seule obligatoire pour utiliser la SDL 2SDL2_image
,SDL2_ttf
,SDL2_mixer
,SDL2_net
sont des greffons officielsSDL2_gfx
est un greffon un peu moins officiel que les autres :-)
Si vous cherchez à savoir si la bibliothèque est installée, il faut chercher un fichier dont le nom commence par libSDL2
Ce qu'il faut faire se trouve dans la partie 2. Mais nous devons commnencer par la découverte de la bibliothèque.
Un squelette de code est disponible ici ou encore par git : https://gitlab.com/kiux/sdl.git
Découverte de la bibliothèque
Les fonctions et types utiles sont définis dans les entêtes suivants :
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <SDL2/SDL_ttf.h>
Initialisations graphiques
La fonction SDL_Init() permet d'initialiser l'affichage graphique avec la bibliothèque SDL2 :
if (SDL_Init(SDL_INIT_VIDEO) < 0)
{
fprintf(stderr, "Erreur d'initialisation de la SDL : %s\n", SDL_GetError());
return EXIT_FAILURE;
}
C'est la TOUTE PREMIÈRE fonction de la SDL2 à appeler. Quelques messages peuvent apparaitre, comme l'absence de RANDR et wrast notamment. Cela ne perturbera pas le fonctionnement du programme.
Si cette fonction renvoie une erreur, vous ne pourrez pas lancer d'interface graphique.
Vous pouvez lister les systèmes à initialiser (SYS1 | SYS2
) ou tout initialiser avec SDL_INIT_EVERYTHING
En fin de programme, il faut quitter proprement en appelant la fonction suivante :
SDL_Quit();
Si l'initialisation s'est bien passée, vous pourrez alors avoir deux fenêtres : un terminal texte et une fenêtre graphique (qu'il faut créer et afficher). On peut toujours écrire sur le terminal avec les fonctions usuelles. En revanche, il y a un peu de travail pour afficher du texte en mode graphique. Il faut notamment initialiser la bibliothèque SDL2_ttf
Il faut normalement initialiser les autres bibliothèques juste après l'initialisation de la SDL2, mais passons tout de suite à la création d'une fenêtre.
Créer une fenêtre
La bibliothèque SDL2 permet de créer autant de fenêtres que nécessaires pour l'application avec CreateWindow()
(on peut travailler en plein écran avec
SDL_WINDOW_FULLSCREEN
)
SDL_Window * window;
window = SDL_CreateWindow("SDL2 Programme 0.1", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
width, height,
SDL_WINDOW_RESIZABLE);
if (window == 0)
{
fprintf(stderr, "Erreur d'initialisation de la SDL : %s\n", SDL_GetError());
/* on peut aussi utiliser SLD_Log() */
}
Dans les paramètres de la fonction de création, vous reconnaîtrez le titre de la fenêtre, le positionnement de la fenêtre par rapport à l'écran, la largeur (width) et la hauteur (height) de la fenêtre ainsi que certaines propriétés.
Si vous exécutez le programme maintenant, il est tout à fait probable que vous ne voyez rien (ou alors juste un clignotement). Ajoutez la ligne :
SDL_Delay(5000);
Quand elle n'est plus utile, la fenêtre doit être rendue proprement avec l'appel à la fonction suivante :
SDL_DestroyWindow(window);
Dessiner quelque chose
Pour dessiner, comme on l'a dit en cours il va nous falloir un contexte de moteur de rendu graphique ou renderer et garder à l'esprit que l'on dessine dans une mémoire tampon qui ensuite est affichée par la carte vidéo. Le renderer peut (voire doit) être créé avant la boucle des événements car c'est une opération qui prend du temps.
SDL_Renderer *renderer;
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED ); /* SDL_RENDERER_SOFTWARE */
if (renderer == 0) {
fprintf(stderr, "Erreur d'initialisation de la SDL : %s\n", SDL_GetError());
/* faire ce qu'il faut pour quitter proprement */
}
Il est possible que la carte video ne vous propose pas l'accélération graphique (libEGL warning: GLX/DRI2 is not supported), il faut alors utiliser l'"accélération logicielle" qui est bien moins rapide évidemment
Si vous comptez utiliser la transparence pour les opérations de dessin et de remplissage, il faudra l'activer avec la fonction suivante :
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
Les options possibles sont décrites ici
Pour dessiner, il faut procéder en trois étapes :
- Initialiser la mémoire tampon (
RenderClear
) - Sélectionner la couleur et afficher point(s), lignes(s) et rectangle(s)
- Mettre à jour la mémoire vidéo à partir de la mémoire tampon (
RenderPresent
void dessinerFenetre()
{
SDL_Rect rect;
/* on prépare/efface le renderer */
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
/* dessiner en blanc */
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
rect.x = rect.y = 0;
rect.w = rect.h = 600;
SDL_RenderFillRect(renderer, &rect );
/* afficher le renderer dans la fenetre */
SDL_RenderPresent(renderer);
}
Si vous exécutez maintenant le programme, vous aurez un joli carré blanc sur fond noir.
Si ce n'est pas le cas, il va vous falloir mettre le système des événements en place. Voici une version simple :
SDL_Event event;
compteur = 1000;
while (compteur>0)
{
while (SDL_PollEvent(&event))
{
dessinerFenetre();
}
--compteur;
SDL_Delay(1);
}
Une version plus opérationnelle est proposée dans le squelette et décrite plus bas.
La fonction dessinerFenetre()
doit être la plus rapide possible, il faut limiter la création et la destruction de ressources dans cette fonction (pas de création de Renderer
ou de chargement d'image/police de caractères par exemple)
Il ne faudra pas oublier de libérer la ressource quand elle n'est plus utile :
SDL_DestroyRenderer(renderer);
Si vous voulez afficher d'autres formes, il faudra vous pencher sur les possibilités offertes par la bibliothèque SDL2_gfx
.
Boucle des événements
C'est bien gentil tout cela, on a une jolie fenêtre graphique mais on ne peut pas interagir avec elle !!! C'est tout à fait normal : il va falloir écrire la boucle
des événements. Voici ce que recommande la documentation SDL : utiliser la fonction PollEvent()
qui n'est pas bloquante. On
insère OBLIGATOIREMENT un léger délai pour ne pas trop monopoliser de ressources processeur)
while (running) {
while (SDL_PollEvent(&event))
{
switch(event.type)
{
case SDL_WINDOWEVENT:
printf("window event\n");
switch (event.window.event)
{
case SDL_WINDOWEVENT_CLOSE:
printf("appui sur la croix\n");
break;
case SDL_WINDOWEVENT_SIZE_CHANGED:
width = event.window.data1;
height = event.window.data2;
printf("Size : %d%d\n", width, height);
default:
dessinerFenetre();
}
break;
case SDL_MOUSEBUTTONDOWN:
printf("Appui :%d %d\n", event.button.x, event.button.y);
// dessinerFenetre() ?
break;
case SDL_QUIT :
printf("on quitte\n");
running = 0;
}
}
SDL_Delay(1); // delai minimal
}
event
est de type SDL_Event
, une union de toutes les structures représentant tous les types d'événements.
Rappel : toutes les applications graphiques sont responsables de leur affichage. La fonction dessinerFenetre()
doit être la plus rapide possible à s'exécuter.
Bien entendu, il est possible de filtrer les événements qui concernent la fenêtre voire même de les désactiver...
Afficher une image (texture)
Nous allons montrer comment charger une image PNG - mon avatar - sur le disque pour l'afficher dans la fenêtre. En SDL2, on dit que l'on manipule une texture.
Tout d'abord, pour pouvoir charger des images dans un format autre que BMP, il faut initialiser la bibliothèque SDL2_image
en précisant les formats qui nous intéressent ( par un OU binaire entre les formats) :
int flags=IMG_INIT_JPG|IMG_INIT_PNG;
// images JPG ou PNG :-)
int initted= 0;
initted = IMG_Init(flags);
if((initted&flags) != flags)
{
printf("IMG_Init: Impossible d'initialiser le support des formats JPG et PNG requis!\n");
printf("IMG_Init: %s\n", IMG_GetError());
}
On peut alors charger l'image qui représente mon avatar qui se trouve dans le répertoire courant :
SDL_Texture *logo;
SDL_Rect rect;
SDL_Surface *image = NULL;
image=IMG_Load("loic.png");
/* image=SDL_LoadBMP("loic.bmp"); // fonction standard de la SDL2 */
if(!image) {
printf("IMG_Load: %s\n", IMG_GetError());
}
logo = SDL_CreateTextureFromSurface(renderer, image);
SDL_FreeSurface(image);
Pour la lecture, nous sommes passé par un type Surface
(SDL1.2) converti en texture (SDL2).
Elle est alors disponible à l'affichage (par exemple dans dessinerFenetre()
:
rect.x = 600;
rect.y = 110;
rect.w = rect.h = 128;
SDL_RenderCopy(renderer, logo, NULL, &rect);
/* L'image a ete copiee dans le renderer qui sera plus tard affiche a l'ecran */
Ne chargez pas l'image à chaque fois que vous voulez l'afficher, la fonction dessinerFenetre()
doit
être la plus rapide possible
Lorsque la bibliothèque n'est plus nécessaire (en fin de programme), il faut rendre les ressources :
SDL_DestroyTexture(logo);
IMG_Quit();
Écrire du texte à l'écran
Pour écrire du texte dans une fenêtre SDL2, on ne peut pas utiliser les fonctions classiques de la sortie standard. Il faut utiliser la bibliothèque SDL2_TTF qui permet de charger des polices de caractères au format True Type Font puis dessiner l'équivalent de ce que l'on veut écrire.
Dans la partie initialisation du programme, il va falloir rajouter le code suivant :
if (TTF_Init() != 0)
{
fprintf(stderr, "Erreur d'initialisation TTF : %s\n", TTF_GetError());
}
Il faut ajouter également le chargement en mémoire d'une police de caractères TTF. Par exemple :
TTF_Font * font1;
font1 = TTF_OpenFont("chlorinar.regular.ttf", 72 );
La fonction cherche à lire le fichier donné en paramètre. 72 est la taille de la police. Si le résultat (font1
) est nul, il y a eu un problème au chargement de la police. Lorsque la police n'est plus nécessaire, il
faut rendre la ressource comme suit :
TTF_CloseFont(font1);
Si vous avez besoin d'une police de caractères, je vous conseille de ne la charger qu'une seule fois pour tout le programme. Si vous avez besoin de plusieurs polices, faites un tableau !
Pour afficher, voici ce qu'il faut faire : utiliser une fonction de rendu de texte, récupérer la surface (image) résultat et la convertir en texture puis appliquer la texture au renderer. Si nécessaire, on peut obtenir des information sur la texture.
int iW, iH;
SDL_Color couleur = {0, 0, 255, 255};
SDL_Surface * surf = TTF_RenderText_Blended(font1, "Mon jeu", couleur);
SDL_Texture * texttext = SDL_CreateTextureFromSurface(renderer, surf);
SDL_QueryTexture(texttext, NULL, NULL, &iW, &iH);
SDL_RenderCopy(renderer, texttext, NULL, &rect);
Le rendu de ressources (en fin de programme par exemple) se fait par :
TTF_Quit();
Voici les deux polices visibles sur la copie écran de l'application :
Ce qu'il faut faire ...
Après ces "brèves explications", passons aux choses sérieuses : la réalisation de l'interface graphique en SDL2. Certaines fonctions du moteur texte sont reprises telles quelles, le mieux est de faire de la compilation séparée mais on peut faire sans.
- Récupérer le squelette et les ressources (tout est sur le git)
- Déclarer en variable globale la grille du floodit. Initialiser cette grille
- Modifier la fonction
dessinerFenetre()
pour afficher la grille [Note 1] - Réagir aux événéments comme le clic souris et faire appel à la mise à jour de la grille [Note 2]. A ce moment, il sera nécessaire d'appeler la fonction
remplir()
- Ajouter la gestion du nombre de coups et un message de fin de jeu [Note 3]
Note 1 : il faut associer une couleur (un quadruplet RVBA) à un entier. Cela peut se faire avec un switch (fastidieux), une matrice à deux dimensions ou un tableau de structures de type palette_t
. A vous de choisir !
Note 2 : pour que le joueur puisse sélectionner la couleur, je vous propose deux solutions : soit faire une "palette" de couleur, soit détecter la couleur de case sur laquelle a cliqué l'utilisateur (c'est à mon avis le plus simple !). Vous pouvez proposer les deux. Vous pouvez même aussi proposer une saisie clavier
Note 3 : ce n'est qu'à cette étape que vous avez besoin de savoir manipuler une chaine de caractères en SDL2, voire une image pour que cela soit joli.
Réagir à un clic souris
Pour détecter les clics de l'utilisateur, il faut découper l'écran en différentes zones de saisie puis affiner si le curseur souris est dans telle ou telle zone.
Par exemple, dans le cas ci-dessus, on vérifiera que le curseur (X,Y) se trouve dans la boîte de coordonnées (X0,Y0) à (X1, Y1). On pourra ensuite diviser (X-X0) par la largeur d'une case pour savoir dans quelle case l'utilisateur a cliqué.
En toute rigueur, une interface graphique devrait aussi être accessible par clavier avec des événements SDL spécifiques.
"État" du jeu
Pour la fin du jeu, on peut utiliser un "état" dans la fonction d'affichage de la fenêtre ou bien afficher une boîte de dialogue toute simple
void dessinerFenetre() {
switch(mode) {
INTRODUCTION : afficherIntroduction();
break;
JEU : afficherFenetreDeJeu();
break;
FIN : afficherFin()
}
}
code
est une variable entière dans une liste de variables (symboliques ou non) voire une énumération, c'est mieux !
enum ETAT {
INTRODUCTION, JEU, FIN
};
Gestion de la multitude des variables
Il y a un certain nombre de variables qu'il faut mémoriser et passer aux fonctions graphiques.
Il y a plusieurs manières de gérer tout cela :
- lister ce qui est nécessaire, même si c'est fastidieux
- utiliser des variables globales. On peut estimer que l'on est dans un cas "légitime"
- utiliser une structure de données graphiques (window, renderer, font, ...)
Si vous voulez sortir du programme avec de multiples return
ou exit()
(mais c'est pas une bonne idée), prenez vos précautions : il est possible d'enregistrer une fonction de fin de programme avec atexit()
.
En conclusion, j'espère que cela vous a plu, la SDL2 permet de faire plein d'autres choses, qui plus est, de manière relativement portable :-)