Techniques de gestion de l'état du jeu?

24

Tout d'abord, je ne parle pas de gestion de scène; Je définis librement l'état du jeu comme toute sorte d'état dans un jeu qui a des implications sur l'activation ou non de l'entrée utilisateur, ou si certains acteurs doivent être temporairement désactivés, etc.

À titre d'exemple concret, disons que c'est un jeu du Battlechess classique. Après avoir fait un geste pour prendre la pièce d'un autre joueur, une courte séquence de bataille est jouée. Pendant cette séquence, le joueur ne devrait pas être autorisé à déplacer des pièces. Alors, comment pourriez-vous suivre ce type de transition d'état? Une machine à états finis? Une simple vérification booléenne? Il semble que ce dernier ne fonctionnerait bien que pour un jeu avec très peu de changements d'état de ce type.

Je peux penser à beaucoup de façons simples de gérer cela en utilisant des machines à états finis, mais je peux aussi les voir devenir rapidement incontrôlables. Je suis simplement curieux de savoir s'il existe un moyen plus élégant de suivre les états / transitions du jeu.

vargonian
la source
Avez-vous vérifié gamedev.stackexchange.com/questions/1783/game-state-stack et gamedev.stackexchange.com/questions/2423/… ? C'est un peu tout autour du même concept, mais je ne peux penser à rien de mieux qu'une machine d'état pour l'état du jeu.
michael.bartnett
Copie
Tetrad

Réponses:

18

Une fois, je suis tombé sur un article qui résout votre problème avec élégance. Il s'agit d'une implémentation FSM de base, appelée dans votre boucle principale. J'ai décrit le résumé de base de l'article dans le reste de cette réponse.

Votre état de jeu de base ressemble à ceci:

class CGameState
{
    public:
        // Setup and destroy the state
        void Init();
        void Cleanup();

        // Used when temporarily transitioning to another state
        void Pause();
        void Resume();

        // The three important actions within a game loop
        void HandleEvents();
        void Update();
        void Draw();
};

Chaque état de jeu est représenté par une implémentation de cette interface. Pour votre exemple Battlechess, cela pourrait signifier ces états:

  • animation d'introduction
  • menu principal
  • animation de configuration de l'échiquier
  • entrée de déplacement du joueur
  • animation de déplacement du joueur
  • animation de mouvement de l'adversaire
  • menu pause
  • écran de fin de partie

Les états sont gérés dans votre moteur d'état:

class CGameEngine
{
    public:
        // Creating and destroying the state machine
        void Init();
        void Cleanup();

        // Transit between states
        void ChangeState(CGameState* state);
        void PushState(CGameState* state);
        void PopState();

        // The three important actions within a game loop
        // (these will be handled by the top state in the stack)
        void HandleEvents();
        void Update();
        void Draw();

        // ...
};

Notez que chaque état a besoin d'un pointeur vers le CGameEngine à un moment donné, afin que l'état lui-même puisse décider si un nouvel état doit être entré. L'article suggère de passer le CGameEngine en tant que paramètre pour HandleEvents, Update et Draw.

Au final, votre boucle principale ne traite que du moteur d'état:

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;
}
fantôme
la source
17
C pour la classe? Ew. Cependant, c'est un bon article - +1.
The Communist Duck
D'après ce que je peux comprendre, c'est le genre de chose sur laquelle la question ne se pose pas explicitement. Cela ne veut pas dire que vous ne pouviez pas le gérer de cette façon, comme vous le pouviez certainement, mais si tout ce que vous vouliez faire était de désactiver temporairement l'entrée, je pense que c'est à la fois exagéré et mauvais pour la maintenance de dériver une nouvelle sous-classe de CGameState qui va être identique à 99% à une autre sous-classe.
Kylotan
Je pense que cela dépend grandement de la façon dont le code est couplé. Je peux imaginer une séparation nette entre la sélection d'une pièce et une destination (principalement les indicateurs d'interface utilisateur et la gestion des entrées), et une animation de la pièce d'échecs vers cette destination (une animation de plateau entière où d'autres pièces se déplacent, interagissent avec le mouvement) pièce, etc.), ce qui rend les états loin d'être identiques. Cela sépare la responsabilité, ce qui permet une maintenance facile et même une réutilisation (démonstration d'intro, mode de relecture). Je pense que cela répond également à la question en montrant que l'utilisation d'un FSM n'a pas besoin d'être un problème.
fantôme
C'est vraiment super, merci. Un point clé que vous avez fait ressortir de votre dernier commentaire: "l'utilisation d'un FSM n'a pas besoin d'être un problème." J'avais imaginé à tort que l'utilisation d'un FSM impliquerait l'utilisation d'instructions switch, ce qui n'est pas nécessairement vrai. Une autre confirmation clé est que chaque état a besoin d'une référence au moteur de jeu; Je me demandais comment cela fonctionnerait autrement.
vargonian
2

Je commence par gérer ce genre de chose de la manière la plus simple possible.

bool isPieceMoving;

Ensuite, j'ajouterai les contrôles par rapport à ce drapeau booléen aux endroits appropriés.

Si je découvre par la suite que j'ai besoin de plus de cas particuliers que cela - et seulement cela - je me réintègre dans quelque chose de mieux. Il y a généralement 3 approches que je prendrai:

  • Refactorisez tous les drapeaux exclusifs représentant des sous-états en énumérations. par exemple. enum { PRE_MOVE, MOVE, POST_MOVE }et ajoutez les transitions là où vous en avez besoin. Ensuite, je peux vérifier cette énumération où je vérifiais le drapeau booléen. Il s'agit d'un changement simple, mais qui réduit le nombre d'éléments à vérifier, vous permet d'utiliser des instructions switch pour gérer efficacement le comportement, etc.
  • Désactivez les sous-systèmes individuels selon vos besoins. Si la seule différence pendant la séquence de bataille est que vous ne pouvez pas déplacer de pièces, vous pouvez appeler pieceSelectionManager->disable()ou similaire au début de la séquence, et pieceSelectionManager->enable(). Vous avez toujours essentiellement des drapeaux, mais maintenant ils sont stockés plus près de l'objet qu'ils contrôlent, et vous n'avez plus besoin de conserver d'état supplémentaire dans votre code de jeu.
  • La partie précédente implique l'existence d'un PieceSelectionManager: plus généralement, vous pouvez factoriser des parties de votre état de jeu et de votre comportement en objets plus petits qui gèrent un sous-ensemble de l'état global de manière cohérente. Chacun de ces objets aura son propre état qui détermine son comportement mais il est facile à gérer car il est isolé des autres objets. Résistez à l'envie de permettre à votre objet gamestate ou à votre boucle principale de devenir un dépotoir pour les pseudo-globaux et factorisez cela!

D'une manière générale, je n'ai jamais besoin d'aller plus loin en ce qui concerne les sous-états de cas spéciaux, donc je ne pense pas qu'il y ait un risque de «perdre rapidement le contrôle».

Kylotan
la source
1
Oui, j'imagine qu'il y a une ligne entre aller à fond avec les états et simplement utiliser un bool / enums lorsque cela est approprié. Mais connaissant mes tendances pédantes, je vais probablement finir par faire de presque chaque état sa propre classe.
vargonian
Vous donnez l'impression qu'une classe est plus correcte que les alternatives, mais rappelez-vous que c'est subjectif. Si vous commencez à créer trop de petites classes pour des choses qui peuvent être représentées plus facilement par d'autres constructions de langage, cela peut obscurcir l'intention du code.
Kylotan
1

http://www.ai-junkie.com/architecture/state_driven/tut_state1.html est un joli tutoriel pour la gestion de l'état du jeu! Vous pouvez l'utiliser soit pour des entités de jeu, soit pour un système de menus comme ci-dessus.

Il commence à enseigner le modèle de conception d'état , puis continue à implémenter un State Machine, et l'étend successivement de plus en plus. C'est une très bonne lecture! Vous donnera une solide compréhension du fonctionnement de l'ensemble du concept et de son application à de nouveaux types de problèmes!

Zolomon
la source
1

J'essaie de ne pas utiliser la machine d'état et les booléens à cet effet, car les deux ne sont pas évolutifs. Les deux se transforment en désordre lorsque le nombre d'États augmente.

Je conçois généralement le gameplay comme une séquence d'actions et de conséquences, tout état de jeu vient naturellement sans qu'il soit nécessaire de le définir séparément.

Par exemple, dans votre cas avec la désactivation de l'entrée du lecteur: vous avez un gestionnaire d'entrée utilisateur et une indication visuelle dans le jeu que l'entrée est désactivée, vous devez en faire un seul objet ou composant, donc pour désactiver l'entrée, vous désactivez simplement l'objet entier, pas besoin de synchronisez-les dans une machine d'état ou réagissez à un indicateur booléen.

Filipp Keks
la source