Boucle de jeu, comment vérifier les conditions une fois, faire quelque chose, puis ne plus le faire

13

Par exemple, j'ai une classe de jeu et elle en garde une intqui suit la vie du joueur. J'ai un conditionnel

if ( mLives < 1 ) {
    // Do some work.
}

Cependant, cette condition continue de se déclencher et le travail est effectué à plusieurs reprises. Par exemple, je veux régler une minuterie pour quitter le jeu en 5 secondes. Actuellement, il continuera de le régler à 5 secondes à chaque image et le jeu ne se termine jamais.

Ce n'est qu'un exemple, et j'ai le même problème dans plusieurs domaines de mon jeu. Je veux vérifier une condition, puis faire quelque chose une fois et une seule fois, puis ne pas vérifier ou refaire le code dans l'instruction if. Certaines solutions possibles qui viennent à l'esprit sont d'avoir un boolpour chaque condition et de définir le boolmoment où la condition se déclenche. Cependant, dans la pratique, cela devient très compliqué à gérer bools, car ils doivent être stockés en tant que champs de classe ou statiquement dans la méthode elle-même.

Quelle est la bonne solution à cela (pas pour mon exemple, mais pour le domaine problématique)? Comment feriez-vous cela dans un jeu?

EddieV223
la source
Merci pour les réponses, ma solution est maintenant une combinaison de réponses ktodisco et crancran.
EddieV223

Réponses:

13

Je pense que vous pouvez résoudre ce problème simplement en exerçant un contrôle plus attentif sur vos chemins de code possibles. Par exemple, dans le cas où vous vérifiez si le nombre de vies du joueur est tombé en dessous de un, pourquoi ne pas vérifier uniquement lorsque le joueur perd une vie, au lieu de chaque image?

void subtractPlayerLife() {
    // Generic life losing stuff, such as mLives -= 1
    if (mLives < 1) {
        // Do specific stuff.
    }
}

Ceci, à son tour, suppose que subtractPlayerLifecela ne serait appelé qu'à des occasions spécifiques, ce qui résulterait peut-être de conditions que vous devez vérifier chaque trame (collisions, peut-être).

En contrôlant soigneusement la façon dont votre code s'exécute, vous pouvez éviter les solutions compliquées comme les booléens statiques et également réduire bit à bit la quantité de code exécutée dans une seule trame.

S'il y a quelque chose qui semble impossible à refactoriser, et que vous n'avez vraiment besoin de vérifier qu'une seule fois, alors enumdes booléens statiques (comme un ) ou statiques sont le chemin à parcourir. Pour éviter de déclarer vous-même la statique, vous pouvez utiliser l'astuce suivante:

#define ONE_TIME_IF(condition, statement) \
    static bool once##__LINE__##__FILE__; \
    if(!once##__LINE__##__FILE__ && condition) \
    { \
        once##__LINE__##__FILE__ = true; \
        statement \
    }

ce qui rendrait le code suivant:

while (true) {
    ONE_TIME_IF(1 > 0,
        printf("something\n");
        printf("something else\n");
    )
}

imprimer somethinget something elseune seule fois. Il ne donne pas le meilleur code, mais cela fonctionne. Je suis également sûr qu'il existe des moyens de l'améliorer, peut-être en garantissant mieux les noms de variables uniques. Cela #definene fonctionnera cependant qu'une seule fois pendant l'exécution. Il n'y a aucun moyen de le réinitialiser et il n'y a aucun moyen de l'enregistrer.

Malgré l'astuce, je recommande fortement de mieux contrôler votre flux de code en premier, et de l'utiliser #defineen dernier recours, ou à des fins de débogage uniquement.

kevintodisco
la source
+1 agréable une fois si la solution. Désordonné, mais cool.
kender
1
il y a un gros problème avec votre ONE_TIME_IF, vous ne pouvez pas le refaire. Par exemple, le joueur retourne au menu principal et revient plus tard à la scène du jeu. Cette déclaration ne se déclenchera plus. et il existe des conditions que vous devrez peut-être conserver entre les exécutions d'application, par exemple la liste des trophées déjà acquis. votre méthode récompensera le trophée deux fois si le joueur le remporte dans deux cycles d'application différents.
Ali1S232
1
@Gajoo C'est vrai, c'est un problème si la condition doit être réinitialisée ou enregistrée. J'ai édité pour clarifier cela. C'est pourquoi je l'ai recommandé en tout dernier recours, ou tout simplement pour le débogage.
kevintodisco
Puis-je suggérer l'idiome do {} while (0)? Je sais que je bousillerais personnellement quelque chose et mettrais un point-virgule là où je ne devrais pas. Cool macro quand même, bravo.
michael.bartnett
5

Cela ressemble au cas classique de l'utilisation du modèle d'état. Dans un état, vous vérifiez votre condition pour des vies <1. Si cette condition devient vraie, vous passez à un état de retard. Cet état de délai attend la durée spécifiée, puis passe à votre état de sortie.

Dans l'approche la plus triviale:

switch(mCurrentState) {
  case LIVES_GREATER_THAN_0:
    if(lives < 1) mCurrentState = LIVES_LESS_THAN_1;
    break;
  case LIVES_LESS_THAN_1:
    timer.expires(5000); // expire in 5 seconds
    mCurrentState = TIMER_WAIT;
    break;
  case TIMER_WAIT:
    if(timer.expired()) mCurrentState = EXIT;
    break;
  case EXIT:
    mQuit = true;
    break;
}

Vous pouvez envisager d'utiliser une classe State avec un StateManager / StateMachine pour gérer la modification / poussée / transition entre les états plutôt qu'une instruction switch.

Vous pouvez rendre votre solution de gestion des états aussi sophistiquée que vous le souhaitez, y compris plusieurs états actifs, un seul état parent actif avec de nombreux états enfants actifs en utilisant une certaine hiérarchie, etc. La solution de modèle d'état rendra votre code beaucoup plus réutilisable et plus facile à maintenir et à suivre.

Naros
la source
1

Si vous êtes préoccupé par les performances, les performances de vérification plusieurs fois ne sont généralement pas importantes; idem pour avoir des conditions booléennes.

Si vous êtes intéressé par autre chose, vous pouvez utiliser quelque chose de similaire au modèle de conception nul pour résoudre ce problème.

Dans ce cas, supposons que vous souhaitiez appeler une méthode foos'il ne reste plus de vie au joueur. Vous implémenteriez cela en deux classes, une qui appelle fooet une qui ne fait rien. Appelons-les DoNothinget CallFooainsi:

class SomeLevel {
  int numLives = 3;
  FooClass behaviour = new DoNothing();
  ...
  void tick() { 
    if (numLives < 1) {
      behaviour = new CallFoo();
    }

    behaviour.execute();
  }
}

Ceci est un peu un exemple "brut"; les points clés sont:

  • Gardez une trace de certains cas FooClassqui peuvent être DoNothingou CallFoo. Il peut s'agir d'une classe de base, d'une classe abstraite ou d'une interface.
  • Appelez toujours la executeméthode de cette classe.
  • Par défaut, la classe ne fait rien ( DoNothinginstance).
  • Lorsque des vies sont épuisées, l'instance est échangée contre une instance qui fait réellement quelque chose ( CallFooinstance).

C'est essentiellement comme un mélange de stratégie et de modèles nuls.

cendres999
la source
Mais il continuera à créer de nouveaux CallFoo () pour chaque trame, que vous devez supprimer avant de nouveau, et continue de se produire, ce qui ne résout pas le problème. Par exemple, si j'avais CallFoo () appeler une méthode qui définit une minuterie à exécuter en quelques secondes, cette minuterie serait réglée à la même heure chaque trame. Autant que je sache, votre solution est la même que si (numLives <1) {// faire des trucs} else {// empty}
EddieV223
Hmm, je vois ce que tu dis. La solution pourrait être de déplacer le ifchèque dans l' FooClassinstance elle-même, il n'est donc exécuté qu'une seule fois, et idem pour l'instanciation CallFoo.
ashes999