Gestion des Game States en C plus plus

Un article de Ceacy.

Ce document est une traduction du tutoriel de tonyandpaige.com, disponible ici. Un grand merci à l'auteur ....

La première fois que je pris réellement conscience des différents états d'un jeu, c'était il y a des années, en regardant une démo. Non pas une démo en tant que "version limitée d'un jeu sur le point de sortir", mais une démo de l'"ancienne école", venant de la scène. Quoi qu'il en soit, ces démos avaient une manière bien à elles de passer d'un effet à un autre. Elles passaient ainsi d'un effet tourbillonnant en 2D à un environnement 3D. Je me rappelle avoir pensé que c'était comme s'ils avaient pris plusieurs programmes différents et les avaient mis bout à bout.

Les états multiples ne sont pas seulement importants dans les démos de ce type, mais aussi dans les jeux en général. Chaque jeu commence en effet par une phase d'introduction, puis laisse place à un menu, quel qu'il soit, avant que, finalement, le jeu ne commence. Quand vous finissez par perdre la partie, le jeu entre dans un état de game-over, habituellement suivi par un retour au menu initial. Sale loser. Dans la plupart des jeux, il est possible d'être simultanément dans plus d'un état à la fois : ainsi, on peut généralement afficher le menu pendant le déroulement de la partie.

La méthode traditionnellement utilisée pour gérer ces états multiples est d'utiliser une série de if(), de switch(), et de boucles. Le programme démarre dans la phase d'intro et boucle jusqu'à ce qu'une touche soit pressée. Puis le menu est affiché tant qu'un choix n'a pas été fait. Puis le jeu commence, et boucle jusqu'à ce que le joueur perde - ou gagne. A chaque itération de la boucle principale du jeu, le programme doit vérifier s'il doit montrer le menu ou simplement afficher la scène suivante. De plus, la partie du programme qui s'occupe des entrées utilisateur doit faire un test pour savoir si ce que vous avez fait affecte le menu ou le jeu. Tout ceci contribue à former une boucle principale confuse, et difficile à suivre, mais aussi à débugguer et à faire évoluer.

[modifier] Qu'est-ce qu'un état ?

Comme je l'ai mentionné plus haut, un état est un peu comme un programme distinct à l'intérieur même du jeu. Chaque état gérera les entrées utilisateur différemment, et affichera quelque chose de différent. Chaque état s'occupera de ses propres évènements, mettra à jour l'environnement du jeu, et portera la scène suivante à l'écran. Nous avons donc identifié les trois fonctions que notre classe game state devrait contenir. Un état de jeu doit également pouvoir charger des ressources (textures, sons ...) et s'initialiser, ainsi que libérer la mémoire et lesdites ressources lorsqu'il se termine. De plus, il est utile à certains moments de pouvoir mettre un état en pause, et de le redémarrer un peu plus tard. Par exemple, pour mettre le jeu en pause lors de l'affichage du menu. Pour l'instant, notre classe game state ressemble à ça :

class CGameState
 {
 public:
   void Init();
   void Cleanup();
 
   void Pause();
   void Resume();
 
   void HandleEvents();
   void Update();
   void Draw();
 };


Une interface de ce type devrait combler nos besoins. Cela fait une bonne classe de base, de laquelle nous pouvons dériver autant de classes que d'états nécessaires au jeu - phase d'introduction, menu, déroulement de la partie, etc.

[modifier] Le Gestionnaire d'état

Nous avons désormais besoin d'un moyen de contrôler ces états - un gestionnaire d'état. Dans mon code, le gestionnaire d'état fait partie du moteur de jeu lui-même. D'autres pourraient préférer créer une classe séparée dans ce but, mais j'ai trouvé bien plus simple de l'intégrer directement au moteur. Encore une fois, nous allons regarder ce qu'un moteur de jeu a besoin de faire, puis développer une classe game engine pour s'en occuper.

Dans notre example, très simple, tout ce que le moteur a besoin de faire est d'initialiser SDL, et de faire le ménage à la fin de l'exécution. Comme la boucle principale utilise le moteur, il nous faut également vérifier s'il est en cours d'exécution, lui envoyer le signal d'arrêt, et gérer les évènements habituels, mettre le monde à jour, et afficher la scène.

La partie du moteur gérant les états est en fait très simple. Afin de permettre aux états de cohabiter l'un au dessus de l'autre, nous avons besoin d'une "pile d'états". Pour implémenter ceci, j'ai utilisé un vector de la STL. De surcroît, il nous faut des fonctions pour changer d'état, et également ajouter (push) et retirer (pop) des états de la pile.

La classe game engine ressemble donc, à ce point, à quelque chose de ce genre :

class CGameEngine
 {
 public:
 
   void Init();
   void Cleanup();
 
   void ChangeState(CGameState* state);
   void PushState(CGameState* state);
   void PopState();
 
   void HandleEvents();
   void Update();
   void Draw();
 
   bool Running() { return m_running; }
   void Quit() { m_running = false; }
 
 private:
   // the stack of states
   vector<CGameState*> states;
 
   bool m_running;
 };

Plusieurs de ces fonctions sont triviales à implémenter. HandleEvents(), Update() et Draw() seront juste des appels à la fonction correspondante de l'état actuel (celui au sommet de la pile). Comme elles auront souvent besoin d'accéder aux données du moteur de jeu, nous allons modifier la classe game state et ajouter comme paramètre à chacune de ces fonctions un pointeur vers le moteur de jeu.

Le dernier détail est la manière de changer d'état. Comment le moteur sait-il quand passer d'un état à un autre ? La réponse est simple - il ne le sait pas. Seul l'état actuel peut savoir quand il est temps de changer d'état. Donc, nous allons encore modifier la classe game state et y ajouter une fonction pour changer d'état.

Tant qu'on y est, nous allons en faire une classe de base abstraite, et faire de la plupart de ses méthodes des fonctions virtuelles pures. Cela garantit que les classes dérivées les implémenteront. Avec tous ces changements, la classe game state finale se présente ainsi :

class CGameState
 {
 public:
   virtual void Init() = 0;
   virtual void Cleanup() = 0;
 
   virtual void Pause() = 0;
   virtual void Resume() = 0;
 
   virtual void HandleEvents(CGameEngine* game) = 0;
   virtual void Update(CGameEngine* game) = 0;
   virtual void Draw(CGameEngine* game) = 0;
 
   void ChangeState(CGameEngine* game, CGameState* state)
   {
     game->ChangeState(state);
   }
 
 protected:
   CGameState() { }
 };


Désormais, pour ajouter des états à votre jeu, il suffira de dériver cette classe de base et de définir les sept fonctions virtuelles pures. Comme nous n'aurons jamais besoin de plus d'une instance de n'importe quel état, ce serait une bonne idée de les implémenter en tant que Singletons. Si vous ignorez ce qu'est le modèle Singleton, il s'agit simplement d'un moyen de garantir qu'il existe une et une seule instance d'un objet. Pour ce faire, le constructeur est défini en protected, et une fonction retournant un pointeur vers une instance statique de la classe est fournie.

Pour vous donner une idée de combien les Game States peuvent vous simplifier la vie, voici le code entier de main.cpp :

#include "gameengine.h"
 #include "introstate.h"
 
 int main ( int argc, char *argv[] )
 {
   CGameEngine game;
 
   // initialize the engine
   game.Init( "Engine Test v1.0" );
 
   // load the intro
   game.ChangeState( CIntroState::Instance() );
 
   // main loop
   while ( game.Running() )
   {
     game.HandleEvents();
     game.Update();
     game.Draw();
   }
 
   // cleanup the engine
   game.Cleanup();
 
   return 0;
 }


[modifier] Téléchargements

Cet exemple contient trois états différents - une phase d'introduction qui apparaît graduellement depuis un fond noir, un état de jeu, et un menu qui met le jeu en pause et le redémarre quand il se termine. Chaque état est représenté par une simple image de fond.

  • [stateman.zip] - Source, ressources et fichiers de projet pour Visual C++ 6
  • [stateman.tar.gz] - Source, ressources et Makefile pour Linux.
  • Fichiers téléchargeables ici

Cet exemple utilise SDL. Si vous ne savez pas ce que c'est, jetez un coup d'oeil à ce tutoriel (anglais). Si vous n'avez pas installé SDL sur votre machine, vous ne pourrez pas compiler et lancer cet exemple.

Note Cet exemple s'inspire du tutoriel State Pattern in C++ du site The Code Project. Malheureusement, l'auteur a implémenté les game states à l'intérieur d'un programme MFC que je trouve difficile à appréhender. De plus, il n'a pas implémenté de pile d'états.


Ce document a été publié sur la version 3 du G.C.N. par c@c.

  • Auteur Original : c@c
  • Date de publication : inconnue

(aucun commentaire actuellement)