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?
la source
Réponses:
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.
la source
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;
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:
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.
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;
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.
la source
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:
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.
la source
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.
la source