Programmer en C++
SDL2 - Moteur de jeu (partie 1)

1. Creation des fichiers

Dans cette partie, nous allons apprendre à utliser des fichiers séparés afin de rendre le code, le plus clair possible pour le programmeur.
Dans le chapitre "Premier projet" vous avez déjà appris à créer un projet. Cette fois-ci nous le nommerons "MoteurDeJeux". Attention à ne pas oublier d'intégrer les trois bibliothèques (libmingw32, libSDL2main, et libSDL2) dans l'onglet "Linker settings". Ces trois bibliothèques doivent être incluses à la fois pour "Debug" et "Release". Revoir ce châpitre si nécessaire.
Vous nommerez le premier fichier "main.cpp". Inutile d'écrire pour l'instant l'ensemble du code concernant la "SDL".
Vous devriez obtenir ceci :

La première ligne de code en commentaire, indiquera clairement sur quelle fichier nous travaillons. En effet c'est un code très court, mais les "#include" alors ? Nous n'en avons pas besoin ?
Si bien sur, mais comme le but est de rendre le programme le plus simple possible, ils seront déportés dans un autre fichier (Celui qu'on ne regardera plus ensuite).

// main.cpp

int main(int argc, char* argv[])
{
    return EXIT_SUCCESS;
}





Vous êtes lancés, alors c'est parti créez deux autres fichiers vides que vous nommerez : "MoteurDeJeux.h" et "MoteurDeJeux.cpp". Comme pour le premier, n'oubliez pas de répondre "Oui" à la demande de lier ces deux fichiers au projet. Si tout se passe bien, vous devriez voir ceci dans la petite fenêtre "Management", onglet "Projects" (qui est normalement déjà affiché) :


Au besoin développez l'arborescence en cliquant sur les petits "+" dans les carrés situés à gauche des dossiers. Ils deviennent des petits "-" lorsque les dossiers sont ouverts.

2. Une classe "MoteurDeJeux"

C'est cette classe "MoteurDeJeux" qui s'occupera des détails qui généralement allourdissent le programme, avec par exemple la gestion des fenêtres, les initialisations, etc ...
Revoir au besoin la notion de classe, de constructeur et de destructeur.

C'est parti, ouvrons maintenant le fichier d'en-tête (Headers) "MoteurDeJeux.h", et écrivons :

// MoteurDeJeux.h

#ifndef MOTEURDEJEUX_H
#define MOTEURDEJEUX_H

#include <iostream>
#include <SDL.h>

class MoteurDeJeux
{
private:
        SDL_Window *pFenetre; // Pointeur sur la fenetre
        SDL_Renderer *pRendu; // Pointeur sur ce qui s'affichera dans la fenetre
        bool EtatDuJeux;            // true (Actif) ou false (Inactif)

public:
        MoteurDeJeux();   // Constructeur
        ~MoteurDeJeux(); // Deconstructeur

        // Initialisation de la fenetre du jeu (titre et dimensions)
        void initialisation(const char* titre, int xPos, int yPos,
                                int largeur, int hauteur, bool pleinEcran);
        // Gestionnaire d'événements (clic, touches, ...)
        void gestionEvenements();
        // actualise le jeu (calcul des déplacements, ...)
        void actualisation();
        // Redessine le contenu de la fenêtre
        void rendu();
        // Nettoyage (quitte proprement le jeu)
        void nettoyage();
        // Récupère l'état du jeux
        bool recupereEtat();
};

#endif

Le groupe d'instructions "#ifndef, #define, et à la fin du programme, #endif" permet de s'assurer que ce fichier ne sera lu qu'une seule fois.
La classe "MoteurDeJeux" déclarée dans ce fichier, est définie avec 3 variables privées dont 2 seront des pointeurs. La transmission des différentes information par adresse permet une plus grande rapidité du programme.
Viennent ensuite les classiques "constructeur" et "destructeur", puis les différentes fonctions dont nous aurons besoin pour le fonctionnement du jeu. Je pense que les commentaires ajoutés dans le programme devraient se suffir à eux mêmes pour la compréhension de ces fonctions.
Voyons maintenant la partie la plus compliquée, c'est à dire la définition de toutes ces fonctions (que font-elles ? Comment ?, ...). Ces définitions seront cette fois écrites dans le fichier "MoteurDeJeux.cpp".
Commençons par le constructeur et le destructeur :

// MoteurDeJeux.cpp

#include "MoteurDeJeux.h"

MoteurDeJeux::MoteurDeJeux()
{
        // Constructeur
}

MoteurDeJeux::~MoteurDeJeux()
{
        // Deconstructeur
}

On notera la présence d'un "#include ..." permettant de lier la définition à la déclaration de notre classe "MoteurDeJeux". Nous avons fait le choix de ne pas initialiser la fenêtre avec notre constructeur. Nous laissons ce travail à la fonction membre "initialisation()" dont voici la définition. Elle est ajoutée à la suite.

void MoteurDeJeux::initialisation(const char* titre, int xPos, int yPos,
                                int largeur, int hauteur, bool pleinEcran)
{
        Uint32 tailleMax;
        if(pleinEcran)
        {
                tailleMax = SDL_WINDOW_FULLSCREEN;
        }
        else
        {
                tailleMax = SDL_WINDOW_SHOWN;
        }

        if(SDL_Init(SDL_INIT_EVERYTHING) == 0)   // Pas de fenêtre active
        {
                std::cout << "Initialisation de la SDL" << std::endl;
                pFenetre = SDL_CreateWindow(titre, xPos, yPos,
                                largeur, hauteur, tailleMax);
                if(pFenetre)
                {
                        std::cout << "La fenetre est cree" << std::endl;
                }
                pRendu = SDL_CreateRenderer(pFenetre, -1, 0);
                if(pRendu)
                {
                        SDL_SetRenderDrawColor(pRendu, 0, 255, 100, 255);
                        std::cout << "Le rendu est cree" << std::endl;
                }
                EtatDuJeux = true;
        }
        else
        {
                EtatDuJeux = false;
        }
}

Les arguments transmis à la fonction initialisation() sont le titre de la fenêtre, la position (x et y) où sera dessiné la fenêtre dans l'écran, la largeur et la hauteur de cette fenêtre. Un dernier argument est transmis pour préciser si nous désirons passer en plein écran ou pas.
Si la fenêtre est déjà ouverte, c'est que la situation est anormale. On bascule alors la variable "EtatDuJeux" en "false", ce qui provoquera, et on le verra plus loin, l'arrêt du jeu. Dans le cas contraire tout se passe bien. On procéde à l'ouverture de la fenêtre, et on applique un rendu (ici ce sera un fond de couleur verte). La variable "EtatDuJeux" est mise ensuite à "true" pour permettre au jeu de continuer.

Découvrons maintenant la définition de la fonction gestionEvenements() :

void MoteurDeJeux::gestionEvenements()
{
        SDL_Event evenement;
        SDL_PollEvent(&evenement);
        switch(evenement.type)
        {
        case SDL_QUIT:
                EtatDuJeux = false;
                break;

        default:
                break;
        }
}

La fonction "SDL_PollEvent()" est semblable à la fonction "SDL_WaitEvent()" utilisée dans le chapitre précédent. Elle permet de récupérer l'événement (clavier, souris, etc ...). Toutefois, en cas d'absence d'événements, le comportement de "SDL_PollEvent diffère radicalement en laissant le programme se poursuivre. C'est important pour que l'animation du jeu puisse continuer. A ce stade nous n'avons pas encore prévu le jeu que nous allons construire, nous laissons donc la fonction suivante vide pour l'instant :

void MoteurDeJeux::actualisation()
{
        // On mettra ensuite ici les calculs de déplacements, etc ...
}

La fonction "rendu()" ne contient pour l'instant que 2 lignes. La première efface le contenu de la fenêtre en ne gardant que la couleur de fond. La deuxième actualise le contenu avec tout ce qui a été dessiné.

void MoteurDeJeux::rendu()
{
        SDL_RenderClear(pRendu);
        // C'est dans cette partie que nous dessinerons le contenu de la fenêtre
        SDL_RenderPresent(pRendu);
}

On ne quitte pas un programme SDL sans refermer proprement ce qui a été ouvert. C'est le rôle de la fonction "nettoyage()". Ces fermetures se font dans l'ordre inverse de leur création (destruction du rendu, puis de la fenêtre).

void MoteurDeJeux::nettoyage()
{
        SDL_DestroyRenderer(pRendu);
        SDL_DestroyWindow(pFenetre);
        SDL_Quit();
        std::cout << "Jeu termine" << std::endl;
}

On arrive maintenant a la fin de notre MoteurDeJeux avec une fonction permettant de lire le contenu de la variable membre "EtatDuJeux". On aurait pu se passer de cette fonction en déclarant cette variable "public" mais autant garder de bonnes habitudes et garder l'encapsulation.

bool MoteurDeJeux::recupereEtat()
{
        return EtatDuJeux;
}

Tout est maintenant en place pour continuer. Revenons au programme principal, et voyons maintenant comment il est simple de lancer le jeu, d'ouvrir une fenêtre, d'appliquer une texture, ...
Premièrement dans l'en-tête du programme principal nous incluons le moteur de jeu dans notre programme avec :

// main.cpp

#include "MoteurDeJeux.h"

Nous allons avoir besoin d'un pointeur de type "MoteurDeJeux". Pourquoi ?
Pour commencer parce qu'en passant par une adresse, on augmente la rapidité du programme. De plus parce que ce pointeur va être le moyen d'accéder directement à toutes nos fonctions du moteur de jeu. Nous initialisons notre pointeur à 0 pour l'instant, et nous le déclarons juste avant la fonction "main()" de façon a lui donner un statut global.

// Creation d'un pointeur de type MoteurDeJeux
MoteurDeJeux *pMoteurDeJeux = 0;

dans la fonction "main()" maintenant nous créons une instance du MoteurDeJeux en réservant un espace mémoire dont l'adresse est transmise à notre pointeur.

// Initialisation du pointeur sur une instance du MoteurDeJeux
pMoteurDeJeux = new MoteurDeJeux;

En bref vous venez d'avoir la clef de la voiture, et il ne vous reste plus qu'à démarrer.
Et maintenant je fais quoi ? J'avance ?
Voilà tout à fait vous avancez en construisant une fenêtre avec votre clef :

// Creation de la fenêtre de jeu
pMoteurDeJeux ->initialisation("Mon super jeu", SDL_WINDOWPOS_CENTERED,
                SDL_WINDOWPOS_CENTERED, 600, 400, false);

Les deux nouveaux paramètres "SDL_WINDOWPOS_CENTERED" vont permettre de centrer la fenêtre aussi bien verticalement qu'horizontalement. Votre fenêtre fera 600 pixels de largeur, 400 pixels de hauteur, et aura pour titre "Mon super jeu". Le dernier paramètre "false" empêchera le passage en plein écran.
Voilà c'est fini votre fenêtre est créée. Pour fonctionner, votre jeu doit tourner en boucle. C'est ici que va intervenir notre variable membre "EtatDuJeux". Lorsque cette variable passera à "false" nous sortirons de cette boucle et le jeu s'arrêtera. Nous récupérons la valeur de cette variable grâce à la fonction "recupereEtat()".

while(pMoteurDeJeux->recupereEtat())
{
        pMoteurDeJeux->gestionEvenements();
        pMoteurDeJeux->actualisation();
        pMoteurDeJeux->rendu();
}

On retrouve dans l'ordre la récupération des événements (clavier, souris, joystick, etc ...), l'actualisation (calcul des nouvelles positions des personnages, des objets, ...), et finalement le rendu (affichage du contenu dans la fenêtre).

Lorsque nous sortons de cette boucle que va-t-il se passer ?
C'est simple on nettoie ! (on ferme tout proprement)

pMoteurDeJeux->nettoyage();

Il reste un détail ! Vous vous souvenez de la clef de la voiture ? Il faut la rendre. On va donc supprimer notre pointeur. C'est important car sinon votre ordinateur ne va pas récupérer l'espace mémoire utilisé par votre jeu.

delete pMoteurDeJeux;

Voilà c'est terminé, je donne ici le code complet du programme principal.

// main.cpp

#include "MoteurDeJeux.h"

// Creation d'un pointeur de type MoteurDeJeux
MoteurDeJeux *pMoteurDeJeux = 0;

int main(int argc, char* argv[])
{
    // Initialisation du pointeur sur une instance du MoteurDeJeux
    pMoteurDeJeux = new MoteurDeJeux;
    // Creation de la fenêtre de jeu
    pMoteurDeJeux ->initialisation("Mon super jeu", SDL_WINDOWPOS_CENTERED,
                SDL_WINDOWPOS_CENTERED, 600, 400, false);
    while(pMoteurDeJeux->recupereEtat())
    {
            pMoteurDeJeux->gestionEvenements();
            pMoteurDeJeux->actualisation();
            pMoteurDeJeux->rendu();
    }
    pMoteurDeJeux->nettoyage();
    delete pMoteurDeJeux;

    return EXIT_SUCCESS;
}

Dans le châpitre suivant, nous verrons comment ajouter des images a notre jeu.