Création du projet et premières lignes de code

Nous allons enfin attaquer le développement même du jeu. Dans cette partie du tutorial, nous allons écrire en C++ les classes qu'on a plus ou moins prévu pour la structure globale du projet. Il s'agit donc de créer le projet sous Code::Blocks, créer les fichiers et compiler le projet pour voir ce que ça donne (à la fin du chapitre, ça donnera pas grand chose mais le principal sera prêt pour la suite).

Création du projet

Lancez le logiciel Code::Blocks et cliquez sur Create a new project sur la page d'accueil. Nous allons choisir un Empty project, même si Irrlicht project paraît tentant, on va partir d'un projet complètement vide pour bien comprendre ce qu'on va coder. Cliquez ensuite sur Go.

Code::Blocks

La première étape de l'assistant vous souhaite la bienvenue et vous propose de le passer la prochaine fois que vous souhaiterez créer un nouveau projet, on va juste cliquer sur Next.

La fenêtre suivante vous demande quelques informations à commencer par le titre du projet. Dès que vous saisissez le titre, les autres champs se remplissent d'eux-même. Vous pouvez choisir de sauvegarder les fichiers du projet dans un répertoire spécifique dans le champ "Folder to create project in". Cliquez sur le bouton "..." pour choisir le dossier de vos projets, par exemple C:\Projets\.

Code::Blocks

Cliquez ensuite sur Next, Code::Blocks vous proposera quel compilateur choisir, par défaut ce sera celui que vous avez choisi au moment de l'installation de Code::Blocks, nous utiliserons GCC pour ce tutorial, rien à changer donc ici, on clique sur Finish.

La classe Game

Commençons par créer la classe principale du jeu, la classe Game qu'on avait imaginé dans le chapitre précédent. Dans le menu File, choisissez New puis Empty file. Une boîte de dialogue vous demande immédiatement si vous souhaitez ajouter ce fichier au projet. A chaque fois nous répondrons oui. Après cela, Code::Blocks vous demande d'enregistrer votre fichier, appelez-le Game.h

Classe Game

Le logiciel vous demandera ensuite de sélectionner les versions de votre jeu qui contiendront ces fichiers. Ici vous pouvez cliquer sur Select All car la version de développement (Debug) et la version finale (Release) du jeu se serviront de Game.h (en fait ce sera le cas de tous les fichiers sources).

Le fichier étant créé (et vide), nous allons commencer l'écriture du "header" de Game :

Classe Game (Game.h)
  1. #ifndef GAME_H
  2. #define GAME_H
  3.  
  4. class Game {
  5.   public:
  6.     Game();
  7.     void run();
  8.     ~Game();
  9. };
  10.  
  11. #endif

Première chose : on n'oublie pas les instructions ifndef, define et endif du préprocesseur, cette technique permet d'éviter la ré-inclusion de headers lors de la compilation du projet, ça entraînerait des erreurs très difficiles à corriger. Tout ce qu'on a fait dans ce code, c'est de définir la signature des trois fonctions qu'on avait prévu : Game(), run() et ~Game(). Elles sont définies dans public car selon le principe de l'encapsulation, ce sont des commandes principales du jeu (comme le bouton "on" d'une télévision).

Après avoir écrit ce court header, on va créer un autre fichier, le source de cette classe, créez donc un nouveau fichier qui lui s'appellera Game.cpp

Classe Game (Game.cpp)
  1. #include "Game.h"
  2.  
  3. Game::Game() {
  4.  
  5. }
  6.  
  7. void Game::run() {
  8.  
  9. }
  10.  
  11. Game::~Game() {
  12.  
  13. }

Ici la première chose à faire mais normalement en tant que développeur C++ vous le savez, c'est d'inclure le fichier Game.h qui contient la définition de la classe Game. Donc ici on n'a fait que d'écrire les trois méthodes de Game, elles sont vides pour le moment mais très bientôt nous seront amenés à les remplir.

Un programme principal

Maintenant que la classe principale a sa structure bien définie et ses méthodes écrites, nous avons besoin du point de départ du jeu : le programme principal. Cela passe par la fameuse fonction main() par laquelle l'exécution d'un programme commence. Nous allons donc écrire un fichier main.cpp :

  1. #include "Game.h"
  2.  
  3. int main() {
  4.   Game *lsw = new Game();
  5.   lsw->run();
  6.   delete lsw;
  7.  
  8.   return 0;
  9. }

Le programme principal va se servir de Game pour l'initialiser, exécuter sa boucle principale puis le terminer. C'est donc main.cpp qui va appeler successivement les trois méthodes, tout bêtement.

On crée à la ligne 4 un pointeur sur un objet Game et par la même occasion on fait une allocation dynamique avec new. Puis on appelle les méthodes pour enfin détruire le pointeur de la mémoire (on fera ça pour chaque objet dont on a fait une allocation dynamique avec new).

Appuyez sur Ctrl+F9 pour compiler votre projet, si aucune erreur n'est mentionnée dans le Build Log, alors un fichier exécutable vient d'être créé dans le dossier bin/Debug de votre projet.

Executable du jeu

Si vous le lancez, vous verrez une fenêtre qui s'ouvre puis qui se referme aussitôt, en effet notre programme ne fait rien en fait, et c'est principalement dû au fait que le jeu ne contient pas de boucle principale.

Nous allons donc juste ajouter une boucle "infinie" à la méthode run() de Game pour bloquer le programme à cet endroit, jusqu'à ce qu'on clique sur le bouton de fermeture du programme.

Classe Game (morceau de Game.cpp)
  1. void Game::run() {
  2.   while(true) {
  3.   
  4.   }
  5. }

Si on recompile le projet (Ctrl+F9) et qu'on exécute le fichier exécutable, on a une console qui reste ouverte. Avant de recompiler votre projet, vérifiez que le programme n'est pas déjà en cours d'exécution, veillez donc à bien fermer la console à chaque fois que vous avez fini vos tests.

La classe Scene

La classe suivante à créer est la classe Scene, comme pour Game, on définit les méthodes de Scene :

Classe Scene (Scene.h)
  1. #ifndef SCENE_H
  2. #define SCENE_H
  3.  
  4. class Scene {
  5.   public:
  6.     Scene();
  7.     void updateAll();
  8.     ~Scene();
  9. };
  10.  
  11. #endif

Pour rappel, updateAll() mettra à jour l'état de tous les éléments de la scène. On peut considérer que updateAll() est la boucle principale de la scène en cours.

Puis nous allons créer le fichier Scene.cpp avec l'écriture des méthodes.

Classe Scene (Scene.cpp)
  1. #include "Scene.h"
  2.  
  3. Scene::Scene() {
  4.   
  5. }
  6.  
  7. void Scene::updateAll() {
  8.   
  9. }
  10.  
  11. Scene::~Scene() {
  12.  
  13. }

La classe SceneElement

On procède toujours de la même manière pour SceneElement : création d'un fichier header :

Classe SceneElement (SceneElement.h)
  1. #ifndef SCENEELEMENT_H
  2. #define SCENEELEMENT_H
  3.  
  4. class SceneElement {
  5.   public:
  6.     SceneElement();
  7.     void update();
  8.     ~SceneElement();
  9. };
  10.  
  11. #endif

Ainsi que le fichier source :

Classe SceneElement (SceneElement.cpp)
  1. #include "SceneElement.h"
  2.  
  3. SceneElement::SceneElement() {
  4.  
  5. }
  6.  
  7. void SceneElement::update() {
  8.  
  9. }
  10.  
  11. SceneElement::~SceneElement() {
  12.  
  13. }

Des ajustements dans les classes Game et Scene

Les trois classes que nous avions planifiées sont maintenant créées mais elles n'ont pas vraiment de lien entre elles. Nous allons tout d'abord lier Game et Scene. On avait dit au chapitre précédent qu'il ne pouvait y avoir qu'une seule scène à la fois. Nous allons donc définir une propriété dans la classe Game qui sera un pointeur vers une Scène. Ainsi on pourra savoir à tout moment quelle scène est en train de se dérouler. Nous procédons alors comme suit :

Classe Game (Game.h)
  1. #ifndef GAME_H
  2. #define GAME_H
  3.  
  4. #include "Scene.h"
  5.  
  6. class Game {
  7.   private:
  8.     Scene *currentScene;
  9.     
  10.   public:
  11.     Game();
  12.     void run();
  13.     ~Game();
  14. };
  15.  
  16. #endif

A la ligne 4, nous avons ajouté une instruction qui inclut le fichier Scene.h, car oui nous allons faire un pointeur vers un objet de ce type, il faut donc bien que le compilateur sache ce que c'est que Scene. Puis à la ligne 8 le fameux pointeur qui porte le nom de currentScene.

Classe Game

C'est justement ce qu'on avait prévu dans le chapitre précédent avec currentScene, c'est la méthode pour relier un Game avec une Scene.

Un autre ajustement à faire à Game est dans son constructeur : créer une scène. Cela aura pour effet de créer une scène dès le commencement du jeu. Pour notre projet, cela pourra être l'écran de logo de is06 ou le votre si vous le souhaitez.

Classe Game (Game.cpp)
  1. #include "Game.h"
  2.  
  3. Game::Game() {
  4.   currentScene = new Scene();
  5. }
  6.  
  7. void Game::run() {
  8.   while(true) {
  9.     currentScene->updateAll();
  10.   }
  11. }
  12.  
  13. Game::~Game() {
  14.   delete currentScene;
  15. }

On crée une instance de Scene pointée par currentScene, on pourra donc se servir de currentScene pour faire plein de choses, comme updateAll() dans la boucle principale du jeu ou l'opération de suppression de la scène dans le destructeur du jeu (au moment où on quitte le jeu).

Game n'est pas la seule classe à subir des modifications. Scene a aussi une liaison a satisfaire avec SceneElement. Sauf qu'ici on a une liaison qui a une multiplicité supérieure à 1. En effet il peut y avoir plusieurs SceneElement dans une Scene. Pour illustrer ça, on va devoir, dans Scene, stocker plusieurs pointeurs vers des SceneElement. On ne va pas créer autant de propriétés qu'on a besoin de SceneElement, et on ne va pas non plus utiliser un tableau car en C++, les tableaux doivent être créés avec un nombre fixe de cellules. Nous allons donc utiliser un conteneur disponible dans la bibliothèque C++ standard : la map.

La map est un conteneur tout à fait indiqué pour notre cas : on peut créer autant de pointeurs vers des SceneElement qu'on veut et en plus, on peut les identifier par un index, ça nous permettra (à l'inverse du vector que vous connaissez peut-être), de récupérer un SceneElement précis, car on l'aura ciblé dans la map.

Pour déclarer la map, on inclut d'abord le fichier de la bibliothèque par une instruction include, puis on va se faciliter la vie en utilisant le namespace std.

Classe Scene (Scene.h)
  1. #ifndef SCENE_H
  2. #define SCENE_H
  3.  
  4. #include <map>
  5. #include "SceneElement.h"
  6.  
  7. using namespace std;
  8.  
  9. class Scene {
  10.   private:
  11.     map<char*,SceneElement*> sceneElementList;
  12.     map<char*,SceneElement*>::iterator seIterator;
  13.  
  14.   public:
  15.     Scene();
  16.     void updateAll();
  17.     ~Scene();
  18. };
  19.  
  20. #endif

Comme pour la classe Game, nous spécifions l'inclusion du fichier SceneElement.h car la map fait référence à des objets SceneElement. Nous venons donc de définir à la ligne 11 une map qui aura des index sous forme de chaînes de caractères et des valeurs sous la forme de pointeurs vers des SceneElement. Vous apercevez aussi à la ligne 12 une drôle de ligne qui en réalité ne redéclare pas une map mais un iterateur. Cette fonctionnalité de C++ permet de parcourir la map pour effectuer des actions à toutes les cellules de la map. Nous allons voir son utilisation plus bas.

Classe Scene (Scene.cpp)
  1. #include "Scene.h"
  2.  
  3. Scene::Scene() {
  4.   sceneElementList["logo"] = new SceneElement();
  5. }
  6.  
  7. void Scene::updateAll() {
  8.   for(seIterator = sceneElementList.begin(); seIterator != sceneElementList.end(); seIterator++) {
  9.     seIterator->second->update();
  10.   }
  11. }
  12.  
  13. Scene::~Scene() {
  14.   for(seIterator = sceneElementList.begin(); seIterator != sceneElementList.end(); seIterator++) {
  15.     delete seIterator->second;
  16.   }
  17. }

A la ligne 4 du fichier Scene.cpp, on a créé un nouvel objet SceneElement et on l'a placé dans la cellule "logo" de la map. Les map s'utilisent pratiquement comme des tableaux, on peut donc utiliser les crochets pour cibler une cellule de la map directement. Dans la méthode updateAll(), on a utilisé une boucle for pour parcourir toutes les cellules de la map. L'iterateur trouve ici toute son utilité : au départ de la boucle il contient le premier élément de la map avec la fonction begin() de notre sceneElementList, puis tant que l'iterateur est différent du dernier élément (qu'on obtient avec end()), on incrémente l'itérateur (donc on passe à l'élément suivant de la map). C'est ainsi que dans la boucle, on choisit d'appeler la méthode update de chaque SceneElement qu'on obtient avec second(). second() renvoie la valeur ciblée par l'iterateur, on aurait pu utiliser first() pour récupérer l'index.

Des repères dans le code

Si on compile le projet et qu'on l'exécute, on ne verra pas grand chose se passer depuis tout à l'heure. En effet à part instancier des objets et les détruire, le programme ne fait pas un très grand spectacle. On va essayer au moins d'afficher un peu de texte histoire que nous, humains, puissions se répérer un peu.

L'idée est d'écrire du texte dans la console à chaque fois qu'on instancie un objet, ainsi on saura ce qui a été créé. On peut aussi écrire quand on est dans une fonction run(), updateAll() ou update().

Dans le fichier Game.cpp, on peut inclure un nouveau fichier : iostream de la bibliothèque standard qui permet de contrôler les flux d'entrée/sortie. On utilise aussi le namespace std histoire de ne pas avoir à écrire à chaque fois std::cout mais cout.

Classe Game (morceau de Game.cpp)
  1. #include "Game.h"
  2. #include <iostream>
  3.  
  4. using namespace std;
  5.  
  6. Game::Game() {
  7.   cout << "In Game constructor" << endl;
  8.   currentScene = new Scene();
  9. }

Vous pouvez donc procéder de la même manière pour les constructeurs (et même destructeurs) des autres classes pour voir un peu comment s'exécute votre jeu. Essayez de placer le cout avant ou après l'instanciation des Scene (ou SceneElement) et de la map pour voir le résultat que cela peut produire.

Execution du jeu