tete du loic

 Loïc YON [KIUX]

  • Enseignant-chercheur
  • Référent Formation Continue
  • Responsable des contrats pros ingénieur
  • Référent entrepreneuriat
  • Responsable de la filière F2 ingénieur
  • Secouriste Sauveteur du Travail
mail
loic.yon@isima.fr
phone
(+33 / 0) 4 73 40 50 42
location_on
ISIMA
  • twitter
  • linkedin
  • viadeo

[C] Flood It avec la SDL2

Date de première publication : 2015/11/3

Vous allez ajouter une couche graphique au jeu dont vous avez développé le moteur texte.

exemple de resultat pour le TP

Les phases d'un programme SDL2 sont des phases "classiques" :

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

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.

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 :

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.

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.

  Écran
(x0, y0)
 
 
 
(x1, y1)

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 :

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 :-)