Techniques de gestion des entrées dans les grands jeux

16

Existe-t-il une technique standard pour gérer les entrées dans les grands jeux? Actuellement, dans mon projet, toute la gestion des entrées se fait dans la boucle de jeu, comme ceci:

while(SDL_PollEvent(&event)){
            switch(event.type){
                case SDL_QUIT:
                    exit = 1;
                    break;
                case SDL_KEYDOWN:
                    switch(event.key.keysym.sym){
                        case SDLK_c:
                            //do stuff
                            break;
                    }
                    break;
                case SDL_MOUSEBUTTONDOWN:
                    switch(event.button.button){
                        case SDL_BUTTON_MIDDLE:
                                //do stuff
                                break;
                            }
                    }
                    break;
            }

(J'utilise SDL, mais je m'attends à ce que la pratique principale s'applique également aux bibliothèques et aux frameworks). Pour un grand projet, cela ne semble pas être la meilleure solution. Je peux avoir plusieurs objets voulant tous savoir sur quoi l'utilisateur a appuyé, il serait donc plus logique que ces objets gèrent les entrées. Cependant, ils ne peuvent pas tous gérer l'entrée, car une fois qu'un événement a été obtenu, il sera poussé hors du tampon d'événements, de sorte qu'un autre objet ne recevra pas cette entrée. Quelle méthode est la plus couramment utilisée pour contrer cela?

w4etwetewtwet
la source
Avec un gestionnaire d'événements, vous pouvez déclencher un événement en entrée et laisser toutes les autres parties de votre jeu s'enregistrer.
danijar
@danijar qu'entendez-vous exactement par un gestionnaire d'événements, est-il possible si vous pouviez fournir un pseudo-code squelette pour montrer de quel genre de chose vous parlez?
w4etwetewtwet du
1
J'ai écrit une réponse pour élaborer sur les gestionnaires d'événements, qui sont la voie à suivre pour la gestion des entrées pour moi.
danijar

Réponses:

12

Depuis demandé par le starter, je développe sur les gestionnaires d'événements. Je pense que c'est un bon moyen de gérer les entrées dans un jeu.

Un gestionnaire d'événements est une classe globale qui permet à la fois d'enregistrer des fonctions de rappel sur des touches et de déclencher ces rappels. Le gestionnaire d'événements stocke les fonctions enregistrées dans une liste privée regroupée par leur clé. Chaque fois qu'une touche est déclenchée, tous les rappels enregistrés sont exécutés.

Les rappels peuvent être des std::functionobjets pouvant contenir des lambdas. Les clés peuvent être des chaînes. Le gestionnaire étant global, les composants de votre application peuvent s'enregistrer sur des clés déclenchées à partir d'autres composants.

// in character controller
// at initialization time
Events->Register("Jump", [=]{
    // perform the movement
});

// in input controller
// inside the game loop
// note that I took the code structure from the question
case SDL_KEYDOWN:
    switch(event.key.keysym.sym) {
    case SDLK_c:
        Events->Fire("Jump");
        break;
    }
    break;

Vous pouvez même étendre ce gestionnaire d'événements pour autoriser le passage de valeurs comme arguments supplémentaires. Les modèles C ++ sont parfaits pour cela. Vous pouvez utiliser un tel système pour, par exemple, qu'un "WindowResize"événement passe la nouvelle taille de fenêtre, afin que les composants d'écoute n'aient pas besoin de le récupérer eux-mêmes. Cela peut réduire considérablement les dépendances du code.

Events->Register<int>("LevelUp", [=](int NewLevel){ ... });

J'ai implémenté un tel gestionnaire d'événements pour mon jeu. Si vous êtes intéressé, je posterai le lien vers le code ici.

À l'aide d'un gestionnaire d'événements, vous pouvez facilement diffuser des informations d'entrée dans votre application. De plus, cela permet une belle façon de laisser l'utilisateur personnaliser les raccourcis clavier. Les composants écoutent les événements sémantiques au lieu des clés directement ( "PlayerJump"au lieu de "KeyPressedSpace"). Ensuite, vous pouvez avoir un composant de mappage d'entrée qui écoute "KeyPressedSpace"et déclenche toute action que l'utilisateur a liée à cette clé.

danijar
la source
4
Excellente réponse, merci. Bien que j'aimerais voir le code, je ne veux pas le copier, donc je ne vous demanderai pas de le poster avant d'avoir implémenté le mien, car j'apprendrai plus de cette façon.
w4etwetewtwet
Je viens de penser à quelque chose, puis-je transmettre n'importe quelle fonction membre comme celle-ci, ou la fonction register n'aura-t-elle pas à prendre AClass :: func, en la limitant à une seule classe fonctions membres
w4etwetewtwet
C'est la grande chose à propos des expressions lambda en C ++, vous pouvez spécifier une clause de capture [=]et les références à toutes les variables locales accessibles à partir du lambda seront copiées. Vous n'avez donc pas à passer un pointeur this ou quelque chose comme ça. Mais notez que vous ne pouvez pas stocker lambdas avec clause de capture dans les anciens pointeurs de fonction C . Cependant, le C ++ std::functionfonctionne bien.
danijar
std :: la fonction est très lente
TheStatehz
22

Divisez cela en plusieurs couches.

Au niveau le plus bas, vous avez des événements d'entrée bruts du système d'exploitation. Entrée de clavier SDL, entrée de souris, entrée de joystick, etc. Vous pouvez avoir plusieurs plates-formes (SDL est un dénominateur le moins commun sans plusieurs formes d'entrée, par exemple, dont vous pourriez vous soucier plus tard).

Vous pouvez les résumer avec un type d'événement personnalisé de très bas niveau, comme "bouton du clavier enfoncé" ou similaire. Lorsque votre couche de plateforme (boucle de jeu SDL) reçoit des entrées, elle doit créer ces événements de bas niveau, puis les transmettre à un gestionnaire d'entrées. Il peut le faire avec des appels de méthode simples, des fonctions de rappel, un système d'événements compliqué, tout ce que vous préférez.

Le système d'entrée a désormais pour tâche de traduire les entrées de bas niveau en événements logiques de haut niveau. La logique du jeu ne se soucie pas du tout de la pression sur SPACE. Il importe que JUMP ait été pressé. Le travail du gestionnaire d'entrées consiste à collecter ces événements d'entrée de bas niveau et à générer des événements d'entrée de haut niveau. Il est responsable de savoir que la barre d'espace et le bouton de la manette de jeu «A» correspondent tous deux à la commande logique Jump. Il traite des commandes de look gamepad vs mouse et ainsi de suite. Il émet des événements logiques de haut niveau qui sont aussi abstraits que possible des contrôles de bas niveau (il y a quelques limitations ici, mais vous pouvez complètement supprimer les choses dans le cas commun).

Votre contrôleur de personnage reçoit ensuite ces événements et traite ces événements d'entrée de haut niveau pour répondre réellement. La couche de plate-forme a envoyé l'événement "Barre d'espace enfoncée". Le système d'entrée a reçu cela, examine ses tables / logiques de mappage, puis envoie l'événement "Saut pressé". La logique de jeu / contrôleur de personnage reçoit cet événement, vérifie que le joueur est réellement autorisé à sauter, puis émet l'événement "Player jumped" (ou provoque directement un saut), que le reste de la logique de jeu utilise pour faire tout .

Tout ce qui dépend de la logique du jeu va dans le contrôleur du joueur. Tout ce qui dépend du système d'exploitation va dans la couche plate-forme. Tout le reste va dans la couche de gestion des entrées.

Voici un art ASCII amateur pour décrire cela:

-----------------------------------------------------------------------
Platform Abstraction | Collect and forward OS input events
-----------------------------------------------------------------------
                          | |
                          | |
                         \   /
                          \_/
-----------------------------------------------------------------------
    Input Manager    | Translate OS input events into logical events
-----------------------------------------------------------------------
                          | |
                          | |
                         \   /
                          \_/
-----------------------------------------------------------------------
Character Controller | React to logical events and affect game play
-----------------------------------------------------------------------
                          | |
                          | |
                         \   /
                          \_/
-----------------------------------------------------------------------
      Game Logic     | React to player actions and provides feedback
-----------------------------------------------------------------------
Sean Middleditch
la source
Art ASCII sympa, mais pas nécessaire, je suis désolé. Je suggère d'utiliser une liste numérotée à la place. Quoi qu'il en soit, une bonne réponse!
danijar
1
@danijar: Eh, j'expérimentais, je n'avais jamais essayé de tirer une réponse auparavant. Plus de travail qu'il n'en valait la peine, mais beaucoup moins de travail que de gérer un programme de peinture. :)
Sean Middleditch
D'accord, compréhensible :-)
danijar
8
Personnellement, je préfère plus l'art ASCII qu'une liste numérotée ennuyeuse.
Jesse Emond
@JesseEmond Hé, ici pour l'art?
danijar