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 ? me demanderez-vous. Eh bien en fait la différence est juste énorme !
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 ? 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). 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 ? 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.
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 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.
#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
|
Quel intérêt, me direz-vous ? 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 ! 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 ).
#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 :
#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); }
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 .
#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
#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(); }
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 ? 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.
#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; } } }
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.
#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); } }
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 ). Tadaaaaam ! On a notre fenêtre noire !!!
Hum, c'est pas encore super top, mais c'est le début ! Alors vivement la suite !!!