tete du loic

 Loïc YON [KIUX]

  • Enseignant-chercheur
  • Référent Formation Continue et alternance ingénieur
  • 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
Institut d'informatique 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 "turing". 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 bibliohtè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.

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

Pour dessiner, il faut procéder en trois étapes :

SDL_Rect rect;

/* couleur de fond */
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 à l'ecran */
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 (décrit plus loin)

// le renderer est créé
	
while (SDL_PollEvent(&event))
{
	/* couleur de fond */
	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 à l'ecran */
	SDL_RenderPresent(renderer);
}
	SDL_Delay(1);

On doit pouvoir redessiner la fenêtre à chaque fois que le système le demande (au travers des événéments). On placera donc ce code dans une fonction afficherEcran()

La fonction afficherEcran() 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:
						afficherEcran();
				}   
			    break;
			case SDL_MOUSEBUTTONDOWN:
				printf("Appui :%d %d\n", event.button.x, event.button.y);
				// afficherEcran() ?
				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 afficherEcran() 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

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;
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());
}

Le code présenté permet l'utilisation d'images au format JPG et PNG.

Voici maintenant le code pour afficher une image (mon avatar, par exemple :-))

SDL_Texture  *avatar;
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());
}

avatar = SDL_CreateTextureFromSurface(renderer, image);
SDL_FreeSurface(image);

rect.x = 600;
rect.y = 110;
rect.w = rect.h = 128;
SDL_RenderCopy(renderer, avatar, NULL, &rect); 
/* L'image a ete copiee dans le renderer qui sera plus tard affiche a l'ecran */

Pour lire une image dans un fichier, il faut passer par un type d'image Surface (SDL1.2) puis convertir en texture (SDL2).

Ne chargez pas l'image à chaque fois que vous voulez l'afficher, la fonction afficherEcran() 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(avatar);
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'un 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.

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, "FloodIt", 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, alors j'espère que vous avez fait de la compilation séparée

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

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 quel 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 dessinerEcran() {
	switch(mode) {
	   INTRODUCTION : afficherIntroduction();
	      break;
	   JEU : afficherEcranDeJeu();
	   	  break;
	   FIN : afficherFin()
	}
}

code est une variable entière dans une liste de variables (symbolique ou nom) 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 / 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 :-)