Que puis-je faire pour éviter les indicateurs et vérifications ponctuels dans tout mon code?

18

Considérez un jeu de cartes, comme Hearthstone .

Il y a des centaines de cartes qui font une grande variété de choses, dont certaines sont uniques, même pour une seule carte! Par exemple, il existe une carte (appelée Nozdormu) qui réduit le nombre de tours de joueur à seulement 15 secondes!

Lorsque vous avez une si grande variété d'effets potentiels, comment évitez-vous les nombres magiques et les contrôles ponctuels dans tout votre code? Comment éviter une méthode "Check_Nozdormu_In_Play" dans la classe PlayerTurnTime? Et comment organiser le code de telle sorte que lorsque vous ajoutez encore plus d' effets, vous n'avez pas besoin de refactoriser les systèmes de base pour prendre en charge des choses qu'ils n'ont jamais eu à prendre en charge auparavant?

Sable Dreamer
la source
Est-ce vraiment un problème de performances? Je veux dire, vous pouvez faire beaucoup de choses folles avec des processeurs modernes en un rien de temps ..
Jari Komppa
11
Qui a parlé des problèmes de performances? Le principal problème que je verrais est de devoir constamment ajuster tout votre code à chaque fois que vous créez une nouvelle carte.
jhocking
2
alors ajoutez un langage de script et scriptez chaque carte.
Jari Komppa
1
Pas le temps de faire une bonne réponse, mais au lieu d'avoir le contrôle Nozdormu par exemple et un ajustement de 15 secondes à l'intérieur du code de classe "PlayerTurnTime" qui gère les tours de joueur, vous pouvez coder la classe "PlayerTurnTime" pour appeler une [classe-, si vous le souhaitez ] fonction fournie de l'extérieur à des points spécifiques. Ensuite, le code de la carte Nozdormu (et toutes les autres cartes qui doivent affecter la même apparence) peut implémenter une fonction pour cet ajustement et injecter cette fonction dans la classe PlayerTurnTime si nécessaire. Il pourrait être utile de lire sur le modèle de stratégie et l'injection de dépendances dans le livre classique Design Patterns
Peteris
2
À un moment donné, je dois me demander si l'ajout de vérifications ad hoc aux bits de code pertinents est la solution la plus simple.
user253751

Réponses:

12

Avez-vous étudié les systèmes de composants d'entité et les stratégies de messagerie d'événements?

Les effets de statut doivent être des composants d'une sorte qui peuvent appliquer leurs effets persistants dans une méthode OnCreate (), expirer leurs effets dans OnRemoved () et s'abonner aux messages d'événements de jeu pour appliquer des effets qui se produisent en réaction à quelque chose qui se passe.

Si l'effet est constamment conditionnel (dure pendant X tours, mais ne s'applique que dans certaines circonstances), vous devrez peut-être vérifier ces conditions à différentes phases.

Ensuite, assurez-vous simplement que votre jeu n'a pas de numéros magiques par défaut. Assurez-vous que tout ce qui peut être changé est une variable pilotée par les données plutôt que des valeurs par défaut codées en dur avec des variables utilisées pour toutes les exceptions.

De cette façon, vous ne supposez jamais quelle sera la longueur du tour. C'est toujours une variable constamment vérifiée qui peut être modifiée par n'importe quel effet et éventuellement annulée plus tard par l'effet à son expiration. Vous ne vérifiez jamais les exceptions avant de revenir par défaut à votre numéro magique.

RobStone
la source
2
"Assurez-vous que tout ce qui peut être changé est une variable pilotée par les données plutôt que des valeurs par défaut codées en dur avec des variables utilisées pour toutes les exceptions." - Ooh, j'aime bien ça. Ça aide beaucoup, je pense!
Sable Dreamer du
Pourriez-vous élaborer sur "appliquer leurs effets persistants"? L'abonnement à turnStarted et la modification de la valeur de longueur ne rendraient-ils pas le code indéfinissable et, ou pire encore, produiraient des résultats incohérents (lors de l'interaction entre des effets similaires)?
wondra
Uniquement pour les abonnés qui assumeraient un laps de temps donné. Vous devez modéliser avec soin. Il peut être bon que le temps de tour actuel soit différent du temps de tour du joueur. PTT serait vérifié pour créer un nouveau tour. CTT pourrait être vérifié par cartes. Si un effet devait augmenter la durée actuelle, l'interface utilisateur du minuteur devrait naturellement emboîter le pas s'il est apatride.
RobStone
Pour mieux répondre à la question. Rien d'autre ne stocke le temps de virage ou quoi que ce soit basé sur cela. Vérifiez toujours.
RobStone
11

RobStone est sur la bonne voie, mais je voulais élaborer car c'est exactement ce que j'ai fait quand j'ai écrit Dungeon Ho !, un Roguelike qui avait un système d'effets très complexe pour les armes et les sorts.

Chaque carte doit avoir un ensemble d'effets attachés, définis de telle manière qu'elle puisse indiquer quel est l'effet, ce qu'elle cible, comment et pour combien de temps. Par exemple, un effet "endommager l'adversaire" pourrait ressembler à ceci;

Effect type: deal damage (enumeration, string, what-have-you)
Effect amount: 20
Source: my weapon
Target: opponent
Effect Cost: 20
Cost Type: Mana

Ensuite, lorsque l'effet se déclenche, demandez à une routine générique de gérer le traitement de l'effet. Comme un idiot, j'ai utilisé une énorme déclaration case / switch:

switch (effect_type)
{
     case DAMAGE:

     break;
}

Mais une manière bien meilleure et plus modulaire de le faire est via le polymorphisme. Créez une classe d'effet qui encapsule toutes ces données, créez une sous-classe pour chaque type d'effet, puis demandez à cette classe de remplacer une méthode onExecute () spécifique à la classe.

class Effect
{
    Object source;
    int amount;

    public void onExecute(Object target)
    {
          // Do nothing
    }
}

class DamageEffect extends Effect
{
    public void onExecute(Object target)
    {
          target.health -= amount;
    }
}

Nous aurions donc une classe d'effet de base, puis une classe DamageEffect avec une méthode onExecute (), donc dans notre code de traitement, nous irions simplement;

Effect effect = card.getActiveEffect();

effect.onExecute();

La manière de gérer ce qui est en jeu est de créer un vecteur / tableau / liste liée / etc. des effets actifs (de type Effet, la classe de base) attachés à n'importe quel objet (y compris le champ de jeu / "jeu"), donc plutôt que d'avoir à vérifier si un effet particulier est en jeu, il vous suffit de parcourir tous les effets attachés à les objets et laissez-les s'exécuter. Si un effet n'est pas attaché à un objet, il n'est pas en jeu.

Effect effect;

for (int o = 0; o < objects.length; o++)
{
    for (int e = 0; e < objects[o].effects.length; e++)
    {
         effect = objects[o].effects[e];

         effect.onExecute();
    }
}
Sandalfoot
la source
C'est exactement comme ça que je l'ai fait. La beauté ici est que vous avez essentiellement un système piloté par les données, et vous pouvez ajuster la logique assez facilement par effet. Habituellement, vous devrez faire quelques vérifications de condition dans la logique d'exécution de l'effet, mais c'est toujours beaucoup plus cohérent puisque ces vérifications sont juste pour l'effet en question.
manabreak
1

Je proposerai quelques suggestions. Certains d'entre eux se contredisent. Mais peut-être que certains sont utiles.

Considérez les listes par rapport aux indicateurs

Vous pouvez parcourir le monde et vérifier un drapeau sur chaque élément pour décider de faire le drapeau. Ou vous pouvez garder une liste des seuls éléments qui devraient faire le drapeau.

Tenez compte des listes et des énumérations

Vous pouvez continuer à ajouter des champs booléens à votre classe d'élément, isAThis et isAThat. Ou vous pouvez avoir une liste de chaînes ou d'éléments d'énumération, comme {"isAThis", "isAThat"} ou {IS_A_THIS, IS_A_THAT}. De cette façon, vous pouvez en ajouter de nouveaux dans l'énumération (ou les chaînes de caractères) sans ajouter de champs. Pas qu'il y ait vraiment quelque chose de mal à ajouter des champs ...

Considérez les pointeurs de fonction

Au lieu d'une liste d'indicateurs ou d'énumérations, pourrait avoir une liste d'actions à exécuter pour cet élément dans différents contextes. (Entité-ish…)

Considérez les objets

Certaines personnes préfèrent les approches basées sur les données, les scripts ou les entités de composants. Mais les hiérarchies d'objets à l'ancienne méritent également d'être prises en compte. La classe de base doit accepter les actions, comme «jouer cette carte pour la phase de tour B» ou autre chose. Ensuite, chaque type de carte peut remplacer et répondre comme il convient. Il y a probablement aussi un objet joueur et un objet jeu, donc le jeu peut faire des choses comme, si (player-> isAllowedToPlay ()) {joue le jeu…}.

Envisagez la capacité de débogage

Une fois une bonne chose à propos d'une pile de champs de drapeau, c'est que vous pouvez examiner et imprimer l'état de chaque élément de la même manière. Si l'état est représenté par différents types, ou des sacs de composants, ou des pointeurs de fonction, ou étant dans des listes différentes, il ne suffit peut-être pas de simplement regarder les champs de l'élément. Ce sont tous des compromis.

Finalement, refactoring: envisager des tests unitaires

Peu importe combien vous généralisez votre architecture, vous pourrez imaginer des choses qu'elle ne couvre pas. Ensuite, vous devrez refactoriser. Peut-être un peu, peut-être beaucoup.

Un moyen de rendre cela plus sûr est avec un ensemble de tests unitaires. De cette façon, vous pouvez être sûr que même si vous avez réorganisé les choses en dessous (peut-être beaucoup!), La fonctionnalité existante fonctionne toujours. Chaque test unitaire ressemble généralement à ceci:

void test1()
{
   Game game;
   game.addThis();
   game.setupThat(); // use primary or backdoor API to get game to known state

   game.playCard(something something).

   int x = game.getSomeInternalState;
   assertEquals(“did it do what we wanted?”, x, 23); // fail if x isn’t 23
}

Comme vous pouvez le voir, la stabilité de ces appels d'API de niveau supérieur sur le jeu (ou le joueur, la carte, etc.) est la clé de la stratégie de test unitaire.

david van brink
la source
0

Au lieu de penser à chaque carte individuellement, commencez à penser en termes de catégories d'effets, et les cartes contiennent une ou plusieurs de ces catégories. Par exemple, pour calculer la durée d'un tour, vous pouvez parcourir toutes les cartes en jeu et vérifier la catégorie "manipuler la durée du tour" de chaque carte qui contient cette catégorie. Chaque carte incrémente ou écrase ensuite la durée du tour en fonction des règles que vous avez décidées.

Il s'agit essentiellement d'un système de mini-composants, où chaque objet "carte" est simplement un conteneur pour un tas de composants d'effet.

jhocking
la source
Étant donné que les cartes - et les futures cartes aussi - peuvent faire à peu près n'importe quoi, je m'attends à ce que chaque carte porte un script. Je suis quand même sûr que ce n'est pas un vrai problème de performances ..
Jari Komppa
4
selon les commentaires principaux: Personne (sauf vous) n'a dit quoi que ce soit sur les problèmes de performances. En ce qui concerne le script complet comme alternative, développez-le dans une réponse.
jhocking