Comment puis-je concevoir de nombreux types d'attaque différents qui peuvent être combinés?

17

Je fais un jeu 2D descendant et je veux avoir beaucoup de types d'attaque différents. Je voudrais rendre les attaques très flexibles et combinables comme le fonctionne The Binding of Isaac. Voici une liste de tous les objets de collection du jeu . Pour trouver un bon exemple, regardons l' élément Spoon Bender .

Spoon Bender donne à Isaac la possibilité de tirer des larmes à tête chercheuse.

Si vous regardez la section "synergies", vous verrez qu'elle peut être combinée avec d'autres objets de collection pour des effets intéressants mais intuitifs. Par exemple, s'il se combine avec The Inner Eye , il "permettra à Isaac de tirer plusieurs coups de repérage à la fois". Cela a du sens, car The Inner Eye

Donne à Isaac un triple coup

Quelle est une bonne architecture pour concevoir des choses comme ça? Voici une solution de force brute:

if not spoon bender and not the inner eye then ...
if spoon bender and not the inner eye then ...
if not spoon bender and the inner eye then ...
if spoon bender and the inner eye then ...

Mais cela deviendra incontrôlable très rapidement. Quelle est la meilleure façon de concevoir un système comme celui-ci?

Daniel Kaplan
la source
Personnellement, je voudrais simplement conserver tous les éléments équipés dans une liste et les faire implémenter une sorte d'interface commune qui prend un objet que vous mutez d'un objet à l'autre. "pour chaque élément, modifiez l'attaque planifiée" afin qu'un élément puisse dupliquer la quantité de projectiles, on pourrait ajouter sa teinte et changer les dégâts (donc si un élément a fait des boulons rouges et un autre a jaune, vous auriez une attaque orange après avoir tous deux modifié l'attaque) . Vous pouvez également avoir une seule classe d'élément générique qui a des paramètres pour décider comment elle modifie l'attaque planifiée.
Benjamin Danger Johnson
2
Je voudrais souligner que Kirby 64 a adopté l'approche de la force brute et des effets différents programmés en dur pour toutes les combinaisons possibles de capacités, donc c'est faisable.
Kevin

Réponses:

16

Vous n'avez absolument pas besoin de coder manuellement les combinaisons. Vous pouvez plutôt vous concentrer sur les propriétés que chaque élément vous donne. Par exemple, l' article A définit Projectile=Fireball,Targetting=Homing. Élément B définit FireMode=ArcShot,Count=3. La ArcShotlogique est responsable de l'envoi du Countnombre d' Projectilearticles dans un arc.

Ces deux éléments peuvent être combinés avec tout autre élément qui modifie librement ces propriétés (ou d'autres). Si vous ajoutez un nouveau type de projectile, il fonctionnera automatiquement avec ArcShot, et si vous ajoutez un nouveau mode de tir, il fonctionnera automatiquement avec les Fireballprojectiles. De même, Targettingest une propriété qui définit le contrôleur pour les projectiles tout en FireModecréant les projectiles, afin qu'ils puissent être facilement et trivialement combinés dans n'importe quelle combinaison comme il est logique.

Vous pouvez également définir des dépendances de propriété, etc. Par exemple, ArcShotnécessite que vous ayez un fournisseur de Projectile(qui pourrait être juste la valeur par défaut). Vous pouvez définir des priorités de sorte que si vous avez deux éléments actifs qui fournissent tous les deux Projectilele code, vous savez lequel utiliser. Ou vous pouvez fournir une interface utilisateur pour permettre à l'utilisateur de sélectionner le type de projectile à utiliser, ou simplement demander au joueur d'équiper les éléments de haute priorité dont il ne veut pas, ou d'utiliser l'élément le plus récent, etc. Vous pouvez en outre autoriser un système d'incompatibilités , par exemple de telle sorte que deux éléments qui ne font que les modifier Projectilene peuvent pas être équipés simultanément.

En général, lorsque cela est possible, préférez tout type d' approche basée sur les données (ou déclarative ) aux approches procédurales (les gros gâchis si-sinon) en ce qui concerne les objets et autres dans votre jeu. Une logique générique de niveau supérieur configurable par des données simples est de loin préférable aux listes codées en dur de règles spéciales.

Sean Middleditch
la source
Petit nit, mais vous ne semblez pas avoir les bonnes propriétés pour les exemples que vous utilisez. "Spoon Bender" ajoute un homing et "Inner Eye" ajoute un triple coup. Ni ajouter un arc et les deux sont des larmes. Si vous utilisez des propriétés arbitraires dans votre exemple pour abstraire la conception, il serait plus facile à lire si elles n'étaient pas nommées de manière trompeuse. Je préfère "Article A" et "Article B" à cela.
Daniel Kaplan
1
@tieTYT: J'ai généralisé les noms et développé l'exemple pour inclure les modes de ciblage en plus des types de projectiles et des modes de tir. Je n'ai jamais joué à BoI pendant plus de quelques minutes, donc je n'ai pas les noms aussi intériorisés que les autres. :)
Sean Middleditch
Il me semble que la partie délicate est d'identifier les catégories de propriétés. Par exemple: le ciblage en est un, FireMode en est un autre, Count peut être un 3ème ... ou pas. Peut-être que 3 projectiles pourraient être une boule de feu et 5 pourraient être des grenades même si c'est une arme.
Daniel Kaplan
@tieTYT: absolument. Vous pouvez étendre davantage le système pour permettre des combinaisons ou une logique spéciales, certes, mais cela devrait être l'exception plutôt que la règle. Optimiser pour le cas commun, pas le cas du coin.
Sean Middleditch
Voici une excellente vidéo expliquant pourquoi vous vous trompez en matière de programmation procédurale youtu.be/QM1iUe6IofM , ainsi que la façon dont vous décrivez les cartes mappe tous les blocs if / else dans des ensembles de données individuels, ne faisant essentiellement rien pour réduire le nombre de scénarios de cas spéciaux que vous devez programme, car vous devez tous les programmer ...
RenaissanceProgrammer
8

Si vous utilisez un langage POO, cela semble être un bon endroit pour utiliser le motif de décoration . Lorsque vous souhaitez modifier la façon dont une attaque se produit, décorez-la simplement avec l'augmentation appropriée.

Exemple c ++ brut:

class AttackBehaviour
{
    /* other code */
    virtual void Attack(double angle);
};

class TearAttack: public AttackBehaviour
{
    /* other code */
    void Attack(double angle);
};

class TripleAttack: public AttackBehaviour
{
    /* other code */
    AttackBehaviour* baseAttackBehaviour;
    void Attack(double angle);
};

void TripleAttack::Attack(angle)
{
    baseAttackBehaviour->Attack(angle-30);
    baseAttackBehaviour->Attack(angle);
    baseAttackBehaviour->Attack(angle+30);
}

Cette méthode serait préférable si vous avez un très grand nombre d'attaques et que vous devez toutes les faire se comporter plus ou moins de la même manière. Si vous voulez changer substantiellement la façon dont l'attaque se produit avec le modificateur (par exemple nouvelle animation avec modificateur), cette méthode n'est pas pour vous.

Mile nautique
la source
Si je veux changer l'animation, pourquoi ce n'est pas pour moi? Et si je travaille dans un langage plus fonctionnel? Connaissez-vous un modèle de conception qui leur convient?
Daniel Kaplan
C'est plus rigide car le modificateur finira toujours par appeler la Attackméthode de l'objet qu'il agrège. La TripleAttackclasse ne devrait pas connaître la TearAttackclasse. Si cela était vrai, cela conduirait à autant de maux de tête que le else-ifbloc. Cela signifie que toutes les animations de larmes doivent résider à l'intérieur de l' TearAttackBehaviourobjet. Cet objet ne sait pas (et ne devrait pas) savoir qu'il a été décoré par un TripleAttackobjet. Le résultat est que les 3 animations de déchirure se déroulent indépendamment, car elles sont indépendantes.
NauticalMile
J'ai du mal à expliquer cela avec des mots, si quelqu'un d'autre veut essayer, soyez mon invité.
NauticalMile
Quant à l'implémentation dans un langage plus fonctionnel, j'y penserai un moment et modifierai ma réponse quand je serai prêt.
NauticalMile
1

En tant que fan de Binding of Isaac, je me suis aussi demandé comment faire quelque chose comme ça. Le système du jeu est suffisamment robuste pour que les comportements émergents résultent de la combinaison d'effets (celui qui me vient à l'esprit est d'obtenir un miroir, une cuillère à cintrer et certains amplificateurs de gamme entraînent un mur de larmes tourbillonnant et homing autour d'Isaac, style Magneto ). Le nombre considérable d'entre eux rendrait impossible un bloc "si".

Ma conclusion est qu'Isaac et ses larmes sont deux entités au centre d'un cadre de composant-entité massif . Les entités ont des statistiques de base (vitesse de déplacement, durée de vie, portée, dégâts, sprite, etc.) et chaque composant apporterait un modificateur de statistiques et un verbe.

Dans le code, Isaac et ses larmes auraient chacun une liste qui contiendrait des éléments d'une interface. Isaac aurait une liste de choses qui s'abonnent à l'interface IsaacMutator, et ses larmes tearMutator. IsaacMutator aurait des fonctions pour modifier la santé, la vitesse, la portée, l'apparence et certains verbes spéciaux d'Isaac. TearMutator serait similaire. Une fois par boucle de jeu, Isaac parcourrait tous les IsaacMutators qu'il possède, et toutes les larmes vivantes le feraient également. Pour reprendre votre exemple anglais, cela se lirait comme suit:

Isaac has IsaacMutators:
--spoonbender which gives no stat change and: Tears are made homing
--MeatEater which give +1 health, +1 damage and: nothing
--MagicMirror which gives no stat change and: Tears are made reflecting

Tears have tearMutators:
--(depends on MeatEater) +1 damage and: nothing
--(depends on MagicMirror) no stat change and: +1 vector towards isaac
--(depnds on spoonbender) no stat change and: +1 vector towards enemytype

etc. Parce que les types sont additifs, vous pouvez empiler et ajouter et supprimer le contenu de votre cœur.

Kirbinator
la source
-5

Je pense que votre chemin fonctionne le mieux. Ces types d'articles donnent chacun une condition, s'ils sont utilisés ensemble, ils produisent une condition différente, alors vous aurez effectivement besoin des 3 conditions possibles définies.

Vous pouvez également vous y prendre en créant un nouveau type de définition lorsque les deux éléments sont présents, mais cela ajoute en fait à la convolution:

if spoon bender and the inner eye then new spoon bender inner eye

if spoon bender inner eye then ...
Programmeur Renaissance
la source