Big Tuto SDL 2 : Rabidja v. 3.0

Chapitre 4 : Ouvrons notre première fenêtre sur le monde !

 

Tutoriel présenté par : Jérémie F. Bellanger (Jay81)
Réécriture complète : 28 septembre 2014
Date de révision : 30 mai 2016

 

      Prologue

 

   Avant d'entrer dans le vif du sujet, je tiens à revenir sur la SDL 2.0.

   Quelles sont les différences entre la SDL 1.2 et la SDL 2.0 ? blush me demanderez-vous. Eh bien en fait la différence est juste énorme !cheeky


   Cela fait déjà plusieurs années que je l'attendais cette nouvelle SDL !  Pourquoi ? Parce que la SDL 1.2 commençait sérieusement à dater et à accuser de terribles limites : en effet, la SDL 1.2 n'exploite pas les cartes graphiques surpuissantes qu'on a aujourd'hui, laissant tout le boulot au processeur central ! Donc forcément, plus notre jeu devient exigeant graphiquement, plus le processeur rame pendant que la (ou les) énorme(s) carte(s) graphique(s) dorme(nt) ! Heureusement, la SDL 2.0 vient changer tout cela avec un tout nouveau moteur de rendu performant, et en plus multi-plateformes !

   En fait, la différence est même encore plus énorme si on se penche sur sa construction. Alors que la SDL 1.2 reposait sur de la vraie 2D, la SDL 2 gère en fait de la 3D (d'où l'apparition de Textures) aplaties pour créer de la 2D. Mais pourquoi ? angel Tout simplement parce que les cartes graphiques d'aujourd'hui ne savent faire que ça, et elles ont des tas de processeurs dédiés pour le faire vite et bien. On gagne donc énormément en vitesse d'exécution, car chaque texture est traitée en parallèle par un processeur de la carte graphique (et non plus l'une après l'autre comme auparavant). wink Bien sûr, la puissance dépend aussi forcément de la carte graphique et de son nombre d'unités de texturisation.

   Et c'est normal ? indecision A l'heure d'aujourd'hui, oui. XNA fait ça depuis longtemps avec DirectX et c'est pas différent avec OpenGl. L'avantage de la SDL, c'est qu'elle rend la chose simple. cheeky


    Mais ce n'est pas tout, la SDL 2 apporte aussi beaucoup d'autres nouveautés, dont vous pourrez trouver la liste ici (en anglais) et apporte ainsi un sacré coup de jeune à nos projets ! 

 

 


 

    On va enfin rentrer dans le vif du sujet : le code source. Notre but est ici d'ouvrir une fenêtre. Or on peut ouvrir une fenêtre avec un simple main() comprenant tout le code (vous l'aurez sûrement déjà testé dans d'autres tutoriels), mais comme notre but, à terme, n'est pas simplement de s'amuser à ouvrir des fenêtres (Ah bon ?! ) nous allons déjà mettre en forme l'ossature de notre programme pour qu'elle soit bien claire et donc plus facilement modifiable après !

    On va y aller progressivement, ensemble. Pour chaque partie du code, je vais vous donner le nom de la page à créer en cliquant sous Code::Blocks sur File / New / Empty File, en sauvegardant votre projet puis en rentrant le nom de la page à créer (c'est à peu près la même chose pour VS, vous pouvez vous référer au chapitre précédent, où on a créé notre fichier main.cpp, si besoin). Ensuite je vous donnerai le code commenté, puis une explication, plus ou moins longue selon le besoin. wink

 

Nom du fichier : defs.h

#ifndef DEF_DEFS
#define DEF_DEFS
 
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <SDL2/SDL.h>
 
/* On inclut les libs supplémentaires */
#include <SDL2/SDL_image.h>
#include <SDL2/SDL_ttf.h>
#include <SDL2/SDL_mixer.h>
 
// Taille de la fenêtre : 800x480 pixels 
#define SCREEN_WIDTH 800
#define SCREEN_HEIGHT 480
 
#endif


    Ce premier fichier va contenir toutes les définitions de notre jeu, utiles pour le préprocesseur à la compilation ainsi que les en-têtes des bibliothèques à inclure.

    Quel intérêt, me direz-vous ? surprise C'est tout simple, plus tard, par exemple, on va définir la vitesse moyenne de notre héros : plutôt que de devoir la changer partout dans le code quand on s'apercevra qu'elle ne colle pas, on écrira dans notre code PLAYER_SPEED, et quand on voudra changer la vitesse du héros, on viendra le faire ici en changeant la valeur du define. C'est donc super utile ! angel Et si vous voulez reprendre votre code plus tard, pour faire un autre jeu plus lent ou plus rapide, vous n'aurez presque rien à changer !

    Bon, pour l'instant, on ne définit que deux paramètres : la largeur et la hauteur de notre fenêtre, qui fera donc 800 x 480 pixels. Bien entendu vous êtes libres de l'ajuster, surtout si vous visez de la HD (mais vous devrez alors aussi créer vos propres graphismes wink).
 
   Vous remarquerez qu'on inclut aussi nos libs SDL_image, SDL_ttf et SDL_mixer. Nous allons les initialiser dès ce premier chapitre pour vérifier qu'elles fonctionnent bien. Si ce n'est pas le cas, vous pourrez vous reporter aux chapitres 2 ou 3, pour voir ce que vous avez manqué. cheeky
 

Nom du fichier : structs.h
#ifndef DEF_STRUCTS
#define DEF_STRUCTS

#include "defs.h"

/* Structures qui seront utilisées pour gérer le jeu */

// Structure pour gérer l'input (clavier puis joystick) 
typedef struct Input
{

	int left, right, up, down, jump, attack, enter, erase, pause;

} Input;

#endif
 

    Ce fichier contiendra toutes les structures utilisées par notre jeu. Une structure est très utile car elle permet de regrouper toutes les variables d'un même ensemble. Ainsi Input contiendra la valeur de toutes les touches enfoncées ou non, qui nous intéressent. On pourra alors, par exemple, tester input.jump pour savoir si notre héros doit sauter (1 = touche enfoncée) ou non (0 = touche relâchée).

    Passons maintenant au fichier main.c : supprimez les quelques lignes de code que nous avions précédemment écrites pour tester la compilation et remplacez-les par celles-ci :

 

Nom du fichier : main.c
#include "prototypes.h"


/* Déclaration des variables / structures utilisées par le jeu */
Input input;


int main(int argc, char *argv[])
{
	unsigned int frameLimit = SDL_GetTicks() + 16;
	int go;

   	// Initialisation de la SDL 
	init("Rabidja 3 - SDL 2 - www.meruvia.fr");

	// Appelle la fonction cleanup à la fin du programme 
	atexit(cleanup);

	go = 1;

	// Boucle infinie, principale, du jeu 
	while (go == 1)
	{
		//Gestion des inputs clavier
		gestionInputs(&input);

		//On dessine tout
        drawGame();

		// Gestion des 60 fps (1000ms/60 = 16.6 -> 16 
		delay(frameLimit);
		frameLimit = SDL_GetTicks() + 16;
	}

	// On quitte
	exit(0);

}

 
    Et le voilà enfin, notre main() !! Comme vous pourrez le voir, pour l'instant, il est très minimaliste , mais c'est normal, puisqu'on veut simplement ouvrir une fenêtre ! Cependant, il contient déjà l'ossature de notre jeu. En effet, d'abord on initialise la SDL 2 en appelant la fonction init() qui prend comme argument le titre du programme (à afficher en haut de la fenêtre), on prévoit le nettoyage à la sortie (cleanup) pour libérer la mémoire et quitter la SDL 2 et ses libs proprement, et puis on passe à notre boucle principale où on teste l'état du clavier avec gestionInputs(), à qui on envoie l'adresse de notre structure Input, puis on affiche tout (c'est à dire ici rien : un écran noir... cheeky) et enfin, on laisse respirer le processeur (pour éviter de prendre 100% de ses ressources) et pour bloquer l'affichage à 60 images/seconde, ce qui est très fluide pour un jeu vidéo !

    Voilà, il n'y a pas grand chose à dire de plus, c'est vraiment très basique, et ça ne devrait pas vous poser trop de problème normalement .
 
   Ah si ! Une dernière chose, vous aurez sans doute remarqué que le fichier n'a pas d'en-tête (main.h) mais plutôt qu'il fait appel à un fichier prototypes.hsurprise
 
   En fait, cela va nous simplifier la vie. L'idée m'est venue du C# qui crée automatiquement un catalogue de tous les prototypes, valable pour le projet en cours. Nous allons faire pareil, sauf que nous devrons créer ce fichier à la main... cheeky Mais concrètement, on n'aura plus à se soucier d'un prototype qu'on aura pu oublier dans tel ou tel fichier, car chaque fichier de notre programme va inclure ce fichier prototypes.h qui contiendra la liste de tous nos prototypes ! wink Il suffira simplement de le mettre à jour régulièrement.
   Nous allons le créer dès maintenant, parce que je connais déjà les prototypes dont nous allons avoir besoin pour ce chapitre, mais dès le chapitre suivant, nous le complèterons plutôt à la fin ! wink


Nom du fichier : prototypes.h

  #include "structs.h"

#ifndef PROTOTYPES
#define PROTOTYPES

#include "structs.h"

/* Catalogue des prototypes des fonctions utilisées.
   On le complétera au fur et à mesure. */

extern void cleanup(void);
extern void delay(unsigned int frameLimit);
extern void drawGame(void);
extern void gestionInputs(Input *input);
extern void getInput(Input *input);
extern SDL_Renderer *getrenderer(void);
extern void init(char *);


#endif

 
   Et voilà le fichier en question ! On y trouve la liste de tous nos prototypes de fonctions (pas très nombreuses pour l'instant wink) en ajoutant devant le mot-clef extern, pour préciser qu'elles se trouvent dans un fichier du projet, mais pas forcément celui en cours.
 
   Vous remarquerez aussi qu'il n'y a plus de variables globales comme dans le Big Tuto SDL 1.2/2, on va se débrouiller autrement. wink
   Passons maintenant au fichier init.c, dans lequel nous allons initialiser la SDL 2 et ses libs, mais aussi les fermer à la fin du programme :


Nom du fichier : init.c


#include "prototypes.h"


SDL_Window *screen;
SDL_Renderer *renderer;


SDL_Renderer *getrenderer(void)
{
	return renderer;
}


void init(char *title)
{
	/* On crée la fenêtre, représentée par le pointeur jeu.window en utilisant la largeur et la
	hauteur définies dans les defines (defs.h).
	Nouveautés SDL2 : on peut centrer la fenêtre avec SDL_WINDOWPOS_CENTERED, et choisir la taille
	de la fenêtre, pour que la carte graphique l'agrandisse automatiquement. Notez aussi qu'on peut
	maintenant créer plusieurs fenêtres. */

	screen = SDL_CreateWindow(title,
		                          SDL_WINDOWPOS_CENTERED,
								  SDL_WINDOWPOS_CENTERED,
								  SCREEN_WIDTH, SCREEN_HEIGHT,
								  SDL_WINDOW_SHOWN);

	//On crée un renderer pour la SDL et on active la synchro verticale : VSYNC
	renderer = SDL_CreateRenderer(screen, -1, SDL_RENDERER_PRESENTVSYNC);

	// Si on n'y arrive pas, on quitte en enregistrant l'erreur dans stdout.txt
    if (screen == NULL || renderer == NULL)
    {
        printf("Impossible d'initialiser le mode écran à %d x %d: %s\n", SCREEN_WIDTH, 
				                                                    SCREEN_HEIGHT, SDL_GetError());
        exit(1);
    }

	//Initialisation du chargement des images png avec SDL_Image 2
	int imgFlags = IMG_INIT_PNG;
	if( !( IMG_Init( imgFlags ) & imgFlags ) )
	{
		printf( "SDL_image n'a pu être initialisée! SDL_image Error: %s\n", IMG_GetError() );
		exit(1);
	}

	//On cache le curseur de la souris 
	SDL_ShowCursor(SDL_DISABLE);

	//On initialise SDL_TTF 2 qui gérera l'écriture de texte
	if (TTF_Init() < 0)
	{
		printf("Impossible d'initialiser SDL TTF: %s\n", TTF_GetError());
		exit(1);
	}

	//On initialise SDL_Mixer 2, qui gérera la musique et les effets sonores
	int flags = MIX_INIT_MP3;
	int initted = Mix_Init(flags);
	if ((initted & flags) != flags)
	{
		printf("Mix_Init: Failed to init SDL_Mixer\n");
		printf("Mix_Init: %s\n", Mix_GetError());
		exit(1);
	}

	/* Open 44.1KHz, signed 16bit, system byte order,
	stereo audio, using 1024 byte chunks (voir la doc pour plus d'infos) */
	if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 1024) == -1) {
		printf("Mix_OpenAudio: %s\n", Mix_GetError());
		exit(1);
	}

	// Définit le nombre de pistes audio (channels) à mixer
	Mix_AllocateChannels(32);

}



void cleanup()
{
	//On quitte SDL_Mixer 2 et on décharge la mémoire
	Mix_CloseAudio();
	Mix_Quit();

	//On fait le ménage et on remet les pointeurs à NULL
	SDL_DestroyRenderer(renderer);
	renderer = NULL;
	SDL_DestroyWindow(screen);
	screen = NULL;

	//On quitte SDL_TTF 2
	TTF_Quit();

	//On quitte la SDL 
	SDL_Quit();
}


 
    Ce fichier contient 3 fonctions : la première renvoie simplement le renderer, un peu comme on le ferait dans une classe en C#. L'avantage, c'est que le renderer reste une variable locale, mais on peut y avoir recours épisodiquement pour dessiner dessus dans une fonction externe au fichier, auquel cas, notre fonction renvoie son adresse pour pouvoir le modifier. wink C'est l'une des pricipales nouveautés de ce Big Tuto, qui va procéder davantage de façon orientée objet, même si le C n'a pas été conçu pour, à la base. cheeky
 
   La seconde initialise la SDL 2 et ses libs, crée la fenêtre, définit son titre et cache le curseur de la souris.
 
   La troisième fonction, enfin, fait le ménage en libérant la mémoire occupée par notre window et notre renderer et quitte la SDL et ses libs.

    On notera que la SDL 2 ne s'initialise plus de la même façon que la 1.2 : elle nécessite de déclarer un (ou plusieurs) écran(s) et un renderer pour afficher cet écran (= faire un rendu). Selon les systèmes, la SDL peut choisir automatiquement si elle doit utiliser OpenGl (Open), Direct3D (Microsoft) ou OpenGl ES (smartphones), ce qui la rend efficace sur toutes les plateformes. Qui plus est, elle peut adapter le rendu à l'écran en le calculant avec la carte graphique !

    Kézako ? surprise Eh bien, ça veut dire, que comme notre jeu va tourner en 800 x 480, si on veut le mettre en plein écran sur un téléviseur Full HD d'1m de diagonale, l'image sera lissée et bien plus belle !

    Pour notre exemple, j'ai donc choisi cette résolution en mode fenêtre. Maintenant, si ces paramètres ne vous plaisent pas, vous êtes libres de les modifier en regardant dans la doc de la SDL.
 
   Passons maintenant au fichier input.c qui contiendra notre fonction de détection des entrées clavier (puis plus tard du joystick wink).

 

Nom du fichier : input.c


#include "prototypes.h"


void gestionInputs(Input *input)
{
	//On gère le clavier (on rajoutera plus tard la gestion
	//des joysticks)
	getInput(input);
}


void getInput(Input *input)
{
	SDL_Event event;

	/* Keymapping : gère les appuis sur les touches et les enregistre
	dans la structure input */

	while (SDL_PollEvent(&event))
	{
		switch (event.type)
		{

			case SDL_QUIT:
				exit(0);
			break;

			case SDL_KEYDOWN:
				switch (event.key.keysym.sym)
				{
					case SDLK_ESCAPE:
						exit(0);
					break;

					case SDLK_DELETE:
						input->erase = 1;
					break;

					case SDLK_c:
						input->jump = 1;
                    break;

                  	case SDLK_v:
						input->attack = 1;
                    break;

					case SDLK_LEFT:
						input->left = 1;
					break;

					case SDLK_RIGHT:
						input->right = 1;
					break;

					case SDLK_DOWN:
						input->down = 1;
					break;

					case SDLK_UP:
						input->up = 1;
					break;


					case SDLK_RETURN:
						input->enter = 1;
					break;


					default:
					break;
				}
			break;

			case SDL_KEYUP:
				switch (event.key.keysym.sym)
				{
					case SDLK_DELETE:
						input->erase = 0;
					break;

                    case SDLK_c:
						input->jump = 0;
                    break;

					case SDLK_LEFT:
						input->left = 0;
					break;

					case SDLK_RIGHT:
						input->right = 0;
					break;

					case SDLK_DOWN:
						input->down = 0;
					break;

					case SDLK_UP:
						input->up = 0;
					break;

					case SDLK_RETURN:
						input->enter = 0;
					break;

					default:
					break;
				}
			break;

		}

	}
}

 

    Vous remarquerez 2 fonctions : la première - gestionInputs() - renvoie pour l'instant simplement à la deuxième, mais elle va se complexifier plus tard, quand nous rajouterons les joysticks (sinon, elle serait inutile ! cheeky).
 
   La seconde - getInput() - est assez longue mais néanmoins très simple : elle enregistre tous les events/événements, c'est-à-dire ici les entrées clavier (on verra plus tard comment rajouter d'autres events pour le joystick ou la gestion de la fenêtre). Ensuite, elle traite ces événements dans une boucle (while) jusqu'à ce qu'il n'y en ait plus. Là, il y a 3 grands cas traités dans un switch : ou on veut quitter en cliquant sur la croix, ou on appuie sur une touche, ou on la relâche. Si on appuie ou on relâche une touche, on met la valeur correspondante dans l'input à 1 ou à 0, selon qu'elle est pressée ou pas.

    Ainsi, tant que la touche C est enfoncée, input.jump vaut 1 et quand on la relâche, elle vaut 0. L'utilisation qu'on en fera se trouvera plus tard dans la fonction doPlayer() qui s'occupera de déplacer notre héros en fonction des touches du clavier. wink

   Passons maintenant au fichier draw.c, qui contiendra par la suite nos fonctions essentielles de dessin et de traitement des images :
 

Nom du fichier : draw.c

#include "prototypes.h"


void drawGame(void)
{

	// Remplit le renderer de noir, efface l'écran et l'affiche.
	//SDL_RenderPresent() remplace SDL_Flip de la SDL 1.2

	SDL_SetRenderDrawColor(getrenderer(), 0, 0, 0, 255);
	SDL_RenderClear(getrenderer());
	SDL_RenderPresent(getrenderer());

    // Délai pour laisser respirer le processeur
    SDL_Delay(1);
}


void delay(unsigned int frameLimit)
{
	// Gestion des 60 fps (images/seconde)
	unsigned int ticks = SDL_GetTicks();

	if (frameLimit < ticks)
	{
		return;
	}

	if (frameLimit > ticks + 16)
	{
		SDL_Delay(16);
	}

	else
	{
		SDL_Delay(frameLimit - ticks);
	}
}

 
    Et voici les fonctions qui vont se charger de l'affichage. Pour l'instant, c'est très embryonnaire, puisqu'on affiche... rien ! Draw() se contente donc de remplir notre renderer de la couleur noire, avec un canal alpha (= transparence) à 255 pour que la couleur soit complètement opaque : (0, 0, 0, 255) = (Rouge, Vert, Bleu, Alpha) et de l'afficher (présenter) à l'écran.

    On voit donc que cela s'est un peu complexifié depuis la SDL 1.2, avec l'arrivée de notre renderer, mais cela fonctionne un peu de la même façon qu'avec XNA et permet beaucoup plus de choses, et surtout, on va enfin pouvoir faire péter le framerate avec des graphismes HD ! On reviendra sur ces points-là plus tard, quand on essaiera d'afficher des sprites à l'écran.

    Quant à la fonction delay, elle permet d'attendre le temps nécessaire pour respecter les 60 images/seconde (donc une image toutes les 16ms à peu près).


    Et voilà, on a maintenant l'ossature complète de notre jeu !!! Il ne reste plus qu'à compiler, en choisissant BUILD puis à tester en choisissant RUN (on peut aussi faire les 2 à la suite avec BUILD AND RUN wink). Tadaaaaam ! On a notre fenêtre noire !!!

 


    Hum, c'est pas encore super top, mais c'est le début ! Alors vivement la suite !!!




 

 

 

Connexion

CoalaWeb Traffic

Today96
Yesterday182
This week572
This month4388
Total1738603

28/03/24