Comment implémenter un système de buff / debuff flexible?


66

Vue d'ensemble:

Un grand nombre de jeux avec des statistiques de type RPG permettent des améliorations du personnage allant du simple "Infliger 25% de dégâts supplémentaires" à des choses plus complexes telles que "Infliger 15 points de dégâts aux attaquants lorsqu'ils sont touchés".

Les spécificités de chaque type de buff ne sont pas vraiment pertinentes. Je cherche un moyen (vraisemblablement orienté objet) pour gérer les buffs arbitraires.

Détails:

Dans mon cas particulier, j'ai plusieurs personnages dans un environnement de combat au tour par tour, alors j'ai imaginé que les buffs soient liés à des événements tels que "OnTurnStart", "OnReceiveDamage", etc. Chaque buff est peut-être une sous-classe d'une classe abstraite principale de Buff, où seuls les événements pertinents sont surchargés. Ensuite, chaque personnage pourrait avoir un vecteur de buffs actuellement appliqués.

Cette solution a-t-elle un sens? Je peux certes constater que des dizaines de types d’événements sont nécessaires, c’est comme si créer une nouvelle sous-classe pour chaque buff était exagéré, et cela ne semblait pas autoriser d’interactions. Autrement dit, si je voulais imposer un plafond sur les augmentations de dégâts de sorte que, même si vous aviez 10 améliorations différentes qui infligent toutes 25% de dégâts supplémentaires, vous ne payez que 100% de plus, au lieu de 250% de plus.

Et il y a des situations plus compliquées que je pourrais idéalement contrôler. Je suis sûr que tout le monde peut trouver des exemples de la façon dont des passionnés plus sophistiqués peuvent potentiellement interagir les uns avec les autres d'une manière que je ne souhaite peut-être pas en tant que développeur de jeux.

En tant que programmeur C ++ relativement inexpérimenté (j'ai généralement utilisé le C dans des systèmes embarqués), j'ai l'impression que ma solution est simpliste et ne tire probablement pas pleinement parti du langage orienté objet.

Pensées? Quelqu'un a-t-il déjà conçu un système de buff relativement robuste?

Edit: Concernant la réponse (s):

J'ai choisi une réponse basée principalement sur de bons détails et une réponse solide à la question que j'ai posée, mais la lecture des réponses m'a donné un peu plus de perspicacité.

Peut-être sans surprise, les différents systèmes ou systèmes modifiés semblent mieux s’appliquer à certaines situations. Le système qui fonctionne le mieux pour mon jeu dépend des types, de la variance et du nombre d'améliorations que je compte pouvoir appliquer.

Pour un jeu comme Diablo 3 (mentionné ci-dessous), où presque n'importe quel équipement peut changer la force d'un buff, les buffs ne sont que des statistiques sur les personnages . Il semble que ce soit une bonne idée chaque fois que cela est possible.

Dans la situation au tour par tour dans laquelle je me trouve, l'approche basée sur les événements peut être plus appropriée.

Dans tous les cas, j'espère toujours que quelqu'un me proposera une balle magique "OO" qui me permettra d'appliquer une amélioration de +2 à la distance de déplacement par tour , un montant équivalent à 50% des dégâts infligés à l'attaquant , et une téléportation automatique vers une tuile proche lorsqu’elle est attaquée à partir de 3 tuiles ou plus dans un même système sans transformer un buff de force de +5 en sa propre sous-classe.

Je pense que la chose la plus proche est la réponse que j'ai marquée, mais la parole est toujours ouverte. Merci à tous pour la contribution.


Je ne publie pas ceci comme réponse, je fais juste du brainstorming, mais qu'en est-il d'une liste d'améliorants? Chaque buff a une constante et un facteur. Constante serait +10 points de dégâts, le facteur serait 1.10 pour un boost de dégâts de + 10%. Dans vos calculs de dégâts, vous parcourez tous les buffs pour obtenir un modificateur total, puis vous imposez les limitations que vous souhaitez. Vous feriez cela pour tout type d'attribut modifiable. Vous auriez cependant besoin d'une méthode de cas spéciale pour les choses compliquées.
William Mariager

A propos, j'avais déjà implémenté quelque chose comme ça pour mon objet Stats lorsque je concevais un système pour des armes et des accessoires pouvant être équipés. Comme vous l'avez dit, c'est une solution assez décente pour les buffs qui ne modifient que les attributs existants, mais bien sûr, même dans ce cas, je souhaite que certains buffs expirent après que X se soit tourné, d'autres expirent lorsque l'effet se produise Y fois, etc. mentionnez-le dans la question principale car cela devenait déjà très long.
gkimsey

1
Si vous utilisez une méthode "onReceiveDamage" appelée par un système de messagerie, manuellement ou d'une autre manière, il devrait être assez facile d'inclure une référence à qui / à qui vous infligez des dégâts. Vous pourrez alors mettre cette information à la disposition de votre buff

D'accord, je m'attendais à ce que chaque modèle d'événement de la classe abstraite Buff contienne des paramètres pertinents comme celui-ci. Cela fonctionnerait certainement, mais j'hésite parce que j'ai l'impression que les choses ne vont pas bien. J'ai du mal à imaginer un MMORPG avec plusieurs centaines de buffs différents. Une classe distincte est définie pour chaque buff, en choisissant parmi une centaine d'événements différents. Non pas que je mette autant d'améliorations (probablement plus de 30), mais s'il existe un système plus simple, plus élégant ou plus flexible, j'aimerais l'utiliser. Système plus flexible = buffs / aptitudes plus intéressants.
gkimsey

4
Ce n'est pas une bonne réponse au problème d'interaction, mais il me semble que le motif de décorateur s'applique bien ici; appliquez simplement plus de buffs (décorateurs) les uns sur les autres. Peut-être avec un système permettant de gérer les interactions en "fusionnant" les buffs ensemble (par exemple, 10x25% sont fusionnés en un buff à 100%).
ashes999

Réponses:


32

C'est une question compliquée, parce que vous parlez de différentes choses qui (ces jours-ci) sont regroupées sous le nom de «buffs»:

  • des modificateurs aux attributs d'un joueur
  • effets spéciaux qui se produisent sur certains événements
  • combinaisons de ce qui précède.

J'implémente toujours le premier avec une liste d'effets actifs pour un personnage donné. Le retrait de la liste, qu'il soit basé sur la durée ou explicitement, est assez simple, donc je ne couvrirai pas cela ici. Chaque effet contient une liste de modificateurs d'attribut que vous pouvez appliquer à la valeur sous-jacente via une simple multiplication.

Ensuite, je l'enveloppe avec des fonctions pour accéder aux attributs modifiés. par exemple.:

def get_current_attribute_value(attribute_id, criteria):
    val = character.raw_attribute_value[attribute_id]
    # Accumulate the modifiers
    for effect in character.all_effects:
        val = effect.apply_attribute_modifier(attribute_id, val, criteria)
    # Make sure it doesn't exceed game design boundaries
    val = apply_capping_to_final_value(val)
    return val

class Effect():
    def apply_attribute_modifier(attribute_id, val, criteria):
        if attribute_id in self.modifier_list:
            modifier = self.modifier_list[attribute_id]
            # Does the modifier apply at this time?
            if modifier.criteria == criteria:
                # Apply multiplicative modifier
                return val * modifier.amount
        else:
            return val

class Modifier():
    amount = 1.0 # default that has no effect
    criteria = None # applies all of the time

Cela vous permet d'appliquer assez facilement des effets multiplicatifs. Si vous avez également besoin d'effets additifs, déterminez l'ordre dans lequel vous souhaitez les appliquer (probablement additif en dernier) et parcourez la liste deux fois. (J'aurais probablement des listes de modificateurs distinctes dans Effect, une pour les multiplicatifs et une pour les additifs).

La valeur du critère est de vous permettre d'appliquer "+20% vs Undead" - définissez la valeur UNDEAD sur l'effet et ne transmettez la valeur UNDEAD que get_current_attribute_value()lorsque vous calculez un jet de dégâts contre un ennemi mort-vivant.

Incidemment, je ne serais pas tenté d'essayer d'écrire un système qui applique et n'applique pas directement les valeurs à la valeur d'attribut sous-jacent - le résultat final est que vos attributs risquent fort de s'éloigner de la valeur voulue en raison d'une erreur. (Par exemple, si vous multipliez quelque chose par 2, puis que vous le coiffiez, lorsque vous le divisez à nouveau par 2, il sera plus bas que celui avec lequel il a commencé.)

En ce qui concerne les effets basés sur des événements, tels que "Inflige 15 points de dégâts en retour aux attaquants lorsqu'ils sont touchés", vous pouvez ajouter des méthodes à cette classe. Mais si vous souhaitez un comportement distinct et arbitraire (par exemple, certains effets de l'événement ci-dessus peuvent refléter des dommages, certains pourraient vous soigner, ils pourraient vous téléporter au hasard, peu importe), vous aurez besoin de fonctions ou de classes personnalisées pour le gérer. Vous pouvez attribuer des fonctions aux gestionnaires d'événements sur l'effet, puis simplement appeler les gestionnaires d'événements sur tous les effets actifs.

# This is a method on a Character, called during combat
def on_receive_damage(damage_info):
    for effect in character.all_effects:
        effect.on_receive_damage(character, damage_info)

class Effect():
    self.on_receive_damage_handler = DoNothing # a default function that does nothing
    def on_receive_damage(character, damage_info):
        self.on_receive_damage_handler(character, damage_info)

def reflect_damage(character, damage_info):
    damage_info.attacker.receive_damage(15)

reflect_damage_effect = new Effect()
reflect_damage_effect.on_receive_damage_handler = reflect_damage
my_character.all_effects.add(reflect_damage_effect)

Il est évident que votre classe d'effets aura un gestionnaire d'événements pour chaque type d'événement et vous pouvez affecter des fonctions de gestionnaire à autant de personnes que vous le souhaitez dans chaque cas. Vous n'avez pas besoin de sous-classer Effect, car chacun est défini par la composition des modificateurs d'attribut et des gestionnaires d'événements qu'il contient. (Il contiendra probablement aussi un nom, une durée, etc.)


2
+1 pour un excellent détail. C'est la réponse la plus proche de répondre officiellement à ma question, comme je l'ai vu. La configuration de base ici semble permettre beaucoup de souplesse et une petite abstraction de ce qui pourrait autrement être une logique de jeu désordonnée. Comme vous l'avez dit, les effets les plus géniaux auraient encore besoin de leurs propres classes, mais cela gère l'essentiel des besoins d'un système "buff" typique, je pense.
gkimsey

+1 pour signaler les différences conceptuelles cachées ici. Tous ne fonctionneront pas avec la même logique de mise à jour basée sur les événements. Voir la réponse de @ Ross pour une application totalement différente. Les deux devront exister côte à côte.
ctietze

22

Dans un jeu sur lequel j'ai travaillé avec un ami pour une classe, nous avons créé un système de buff / debuff lorsque l'utilisateur est pris au piège dans les hautes herbes et les tuiles rapides, entre autres choses, et quelques petites choses comme les saignements et les poisons.

L'idée était simple et bien que nous l'appliquions en Python, elle était plutôt efficace.

Voici comment cela s'est passé:

  • L'utilisateur avait une liste des buffs et des debuffs appliqués (notez que les buffs et les debuff sont relativement identiques, c'est juste l'effet qui a un résultat différent)
  • Les buffs ont une variété d'attributs tels que la durée, le nom et le texte pour l'affichage des informations et l'heure vivante. Les plus importants sont le temps en vie, la durée et une référence à l'acteur auquel ce buff est appliqué.
  • Pour Buff, lorsqu'il est attaché au lecteur via player.apply (buff / debuff), il appelle une méthode start (), ce qui appliquerait les modifications critiques au lecteur, telles que l'augmentation de la vitesse ou le ralentissement.
  • Nous pourrions ensuite parcourir chaque buff dans une boucle de mise à jour et les buffs se mettraient à jour, ce qui augmenterait leur durée de vie. Les sous-classes implémenteraient des choses telles que l’empoisonnement du lecteur, le gain de HP au fil du temps, etc.
  • Lorsque le buff était terminé pour timeAlive> = duration, la logique de mise à jour le supprimait et appelait une méthode finish (), qui allait de la suppression des limitations de vitesse sur un joueur à la création d'un petit rayon (pensez à un effet de bombe après un DOT)

Maintenant, comment appliquer les améliorations du monde est une autre histoire. Voici ma matière à réflexion cependant.


1
Cela ressemble à une meilleure explication de ce que j’essayais de décrire ci-dessus. C'est relativement simple, certainement facile à comprendre. Vous avez essentiellement mentionné trois "événements" (OnApply, OnTimeTick, OnExpired) pour les associer davantage à ma pensée. Dans l'état actuel des choses, cela ne prendrait pas en charge des choses telles que le retour des dégâts après avoir été touché, mais cela va mieux pour beaucoup d'améliorants. Je préfère ne pas limiter ce que mes buffs peuvent faire (ce qui = limiter le nombre d'événements que je propose qui doivent être appelés par la logique de jeu principale), mais l'évolutivité des buffs peut être plus importante. Merci pour votre contribution!
gkimsey

Oui, nous n'avons rien mis en œuvre de ce genre. Cela semble vraiment chouette et un très bon concept (un peu comme un buff Thorns).
Ross

@gkimsey Pour des choses comme Thorns et d'autres buffs passifs, j'implémenterais la logique de votre classe Mob comme une statistique passive similaire aux dégâts ou aux points de vie et augmenterais cette statistique lors de l'application du buff. Cela simplifie beaucoup le cas lorsque vous avez plusieurs améliorations d'épines, tout en maintenant l'interface propre (10 améliorations afficheraient 1 dommage de retour au lieu de 10) et permettent au système d'optimisation de rester simple.
3Doubloons le

C’est une approche presque contre-intuitive, mais j’ai commencé à penser à moi-même lorsque je jouais à Diablo 3. J’ai remarqué que la vie volée, la vie touchée, les dommages infligés aux attaquants au corps-à-corps, etc., étaient toutes leurs propres statistiques dans la fenêtre du personnage. Certes, D3 n’a pas le système de buffing ni les interactions les plus compliqués au monde, mais ce n’est guère trivial. Cela fait beaucoup de sens. Néanmoins, il existe potentiellement 15 buffs différents avec 12 effets différents. Cela semble bizarre de remplir la feuille de statistiques du personnage ....
gkimsey

11

Je ne sais pas si vous lisez encore cela, mais cela fait longtemps que je lutte avec ce type de problème.

J'ai conçu de nombreux types de systèmes affectifs. Je vais les passer brièvement en revue maintenant. Tout est basé sur mon expérience. Je ne prétends pas connaître toutes les réponses.


Modificateurs statiques

Ce type de système repose principalement sur des entiers simples pour déterminer les modifications éventuelles. Par exemple, +100 à Max HP, +10 à attaquer et ainsi de suite. Ce système pourrait également gérer des pourcentages. Vous devez juste vous assurer que l'empilement ne devient pas incontrôlable.

Je n'ai jamais vraiment mis en cache les valeurs générées pour ce type de système. Par exemple, si je voulais afficher le maximum de santé de quelque chose, je générerais la valeur sur place. Cela a empêché les choses d'être sujettes aux erreurs et tout simplement plus faciles à comprendre pour toutes les personnes impliquées.

(Je travaille en Java, donc ce qui suit est basé sur Java, mais il devrait fonctionner avec quelques modifications pour d’autres langages). Ce système peut facilement être installé en utilisant des énumérations pour les types de modification, puis des entiers. Le résultat final peut être placé dans une sorte de collection qui a des paires clé / valeur ordonnées. Ce sera une recherche rapide et des calculs, donc la performance est très bonne.

Globalement, cela fonctionne très bien avec des modificateurs statiques à plat. Toutefois, le code doit exister aux emplacements appropriés pour que les modificateurs soient utilisés: getAttack, getMaxHP, getMeleeDamage, etc., etc.

Lorsque cette méthode échoue (pour moi), il existe une interaction très complexe entre les buffs. Il n’ya pas de moyen facile d’interagir autrement qu’en ghettant un peu. Il y a quelques possibilités d'interaction simples. Pour ce faire, vous devez modifier la façon dont vous stockez les modificateurs statiques. Au lieu d'utiliser une énumération comme clé, vous utilisez une chaîne. Cette chaîne serait le nom Enum + variable supplémentaire. 9 fois sur 10, la variable supplémentaire n'est pas utilisée, vous conservez donc le nom enum comme clé.

Prenons un exemple rapide: si vous voulez pouvoir modifier les dégâts infligés aux créatures morts-vivants, vous pourriez avoir une paire ordonnée comme ceci: (DAMAGE_Undead, 10) Le DAMAGE est l’énum et le Mort-vivant est la variable supplémentaire. Donc, pendant votre combat, vous pouvez faire quelque chose comme:

dam += attacker.getMod(Mod.DAMAGE + npc.getRaceFamily()); //in this case the race family would be undead

Quoi qu'il en soit, cela fonctionne assez bien et est rapide. Mais cela échoue aux interactions complexes et à un code «spécial» partout. Par exemple, considérons la situation de «25% de chances de se téléporter à la mort». Ceci est un "assez" complexe. Le système ci-dessus peut le gérer, mais pas facilement, car vous avez besoin des éléments suivants:

  1. Déterminez si le joueur a ce mod.
  2. Quelque part, avoir un code pour exécuter la téléportation, si elle réussit. L'emplacement de ce code est une discussion en soi!
  3. Obtenez les bonnes données de la carte Mod. Que signifie la valeur? Est-ce la pièce où ils se téléportent aussi? Que faire si un joueur a deux mods de téléportation sur eux ?? Les montants ne seront-ils pas cumulés ?????? ÉCHEC!

Donc cela m'amène à mon prochain:


Le système ultime de buff complexe

Une fois, j'ai essayé d'écrire moi-même un MMORPG 2D. C'était une terrible erreur mais j'ai beaucoup appris!

J'ai réécrit le système affect 3 fois. Le premier utilisait une variante moins puissante de ce qui précède. Le second était ce que je vais parler.

Ce système avait une série de classes pour chaque modification. Des choses comme: ChangeHP, ChangeMaxHP, ChangeHPByPercent, ChangeMaxByPercent. J'ai eu un million de ces gars - même des choses comme TeleportOnDeath.

Mes cours avaient des choses qui feraient ce qui suit:

  • applyAffect
  • removeAffect
  • checkForInteraction <--- important

Appliquer et supprimer expliquer eux-mêmes (bien que pour des pourcentages tels que, par exemple, l’effet garderait une trace de son augmentation de HP pour assurer que lorsque l’effet disparaîtrait, il ne supprime que le montant ajouté. Il m'a fallu beaucoup de temps pour m'assurer que tout allait bien. Je n'ai toujours pas eu un bon pressentiment à ce sujet.).

La méthode checkForInteraction était un morceau de code incroyablement complexe. Dans chacune des classes d’effets (c.-à-d. ChangeHP), il y aurait un code pour déterminer s’il doit être modifié par l’effet d’entrée. Donc, par exemple, si vous aviez quelque chose comme ...

  • Buff 1: Inflige 10 points de dégâts de Feu en attaque.
  • Buff 2: Augmente tous les dégâts de feu de 25%.
  • Buff 3: Augmente tous les dégâts de feu de 15.

La méthode checkForInteraction traiterait tous ces effets. Pour ce faire, chacun des effets sur TOUS les joueurs à proximité devait être vérifié! En effet, le type d’effets que j’avais eu avec plusieurs joueurs sur une zone donnée. Cela signifie que le code N'A JAMAIS eu de déclaration spéciale comme ci-dessus - "si nous venons de mourir, nous devrions vérifier le téléport au décès". Ce système le traiterait automatiquement correctement au bon moment.

Essayer d'écrire ce système m'a pris environ deux mois et m'a fait exploser à plusieurs reprises. CEPENDANT, il était VRAIMENT puissant et pouvait faire une quantité incroyable de choses - en particulier si vous tenez compte des deux faits suivants pour les capacités de mon jeu: 1. Ils avaient des domaines cibles (c.-à-d. Unique, seul, groupe uniquement, PB AE). , Cible PB AE, AE ciblée, etc.). 2. Les capacités pourraient avoir plus d'un effet sur elles.

Comme je l'ai mentionné ci-dessus, il s'agissait du deuxième système de troisième affect pour ce jeu. Pourquoi je me suis éloigné de ça?

Ce système a eu la pire performance que j'ai jamais vue! C'était terriblement lent, car il devait faire beaucoup de vérifications pour chaque chose qui se passait. J'ai essayé de l'améliorer, mais j'ai considéré que c'était un échec.

Nous arrivons donc à ma troisième version (et à un autre type de système de buff):


Classe d'affect complexe avec gestionnaires

Il s’agit donc en fait d’une combinaison des deux premières: nous pouvons avoir des variables statiques dans une classe Affect contenant de nombreuses fonctionnalités et des données supplémentaires. Ensuite, il suffit d'appeler des gestionnaires (pour moi, des méthodes utilitaires statiques plutôt que des sous-classes pour des actions spécifiques. Mais je suis sûr que vous pouvez utiliser des sous-classes pour des actions si vous le souhaitez également) lorsque nous voulons faire quelque chose.

La classe Affect aurait toutes les bonnes choses juteuses, comme les types de cibles, la durée, le nombre d'utilisations, les chances d'exécution et ainsi de suite.

Il faudrait encore ajouter des codes spéciaux pour gérer les situations, par exemple, le téléport à la mort. Nous devrions toujours vérifier cela dans le code de combat manuellement, et si cela existait, nous aurions une liste des affects. Cette liste d’effets contient tous les effets actuellement appliqués sur le joueur qui s’est déjà téléporté à sa mort. Ensuite, nous examinerions chacun d’entre eux et vérifierions s’il s’exécutait et réussissait (nous nous arrêterions au premier essai réussi). Si cela réussissait, nous appelions simplement le gestionnaire pour s’occuper de cela.

L'interaction peut être faite, si vous voulez aussi. Il suffirait d’écrire le code pour rechercher des améliorations spécifiques sur les lecteurs / etc. Parce qu'il a de bonnes performances (voir ci-dessous), il devrait être assez efficace pour le faire. Il aurait simplement besoin de gestionnaires plus complexes, etc.

Donc, il a beaucoup de performances du premier système et toujours beaucoup de complexité comme le second (mais pas autant). Au moins en Java, vous pouvez faire certaines choses difficiles pour obtenir les performances de presque la première dans la plupart des cas (par exemple, avoir une carte enum ( http://docs.oracle.com/javase/6/docs/api/java). /util/EnumMap.html ) avec Enums comme clés et ArrayList des affects en tant que valeurs, ce qui vous permet de voir si vous avez rapidement des affects [puisque la liste serait 0 ou que la carte n'aurait pas l'énum] et ne pas avoir parcourir sans cesse les listes d’effets des joueurs sans aucune raison (cela ne me dérange pas d’itérer plus d’affect si nous en avons besoin à ce moment-là. J'optimiserai plus tard si cela devient un problème).

Je suis actuellement en train de ré-ouvrir (réécrire le jeu en Java au lieu du code de base FastROM dans lequel il se trouvait à l'origine) mon MUD qui s'est terminé en 2005 et j'ai récemment compris comment je veux implémenter mon système de buff. Je vais utiliser ce système car il a bien fonctionné dans mon jeu précédent échoué.

Espérons que quelqu'un, quelque part, trouvera certaines de ces idées utiles.


6

Une classe (ou fonction adressable) différente pour chaque buff n'est pas excessive si le comportement de ces buff est différent les uns des autres. Une chose serait d'avoir des améliorations de + 10% ou de + 20% (qui, bien sûr, seraient mieux représentées comme deux objets de la même classe), une autre implémenterait des effets extrêmement différents qui nécessiteraient de toute façon un code personnalisé. Cependant, je pense qu'il vaut mieux avoir moyens standard de personnalisation de la logique de jeu plutôt que de laisser chaque buff faire ce qu'il veut (et d'interférer les uns avec les autres de manière imprévue, en perturbant l'équilibre du jeu).

Je suggérerais de diviser chaque "cycle d'attaque" en étapes, chaque étape ayant une valeur de base, une liste ordonnée de modifications pouvant être appliquées à cette valeur (éventuellement limitée) et une limite finale. Chaque modification a une transformation d'identité par défaut et peut être influencée par zéro ou plus buffs / debuffs. Les détails de chaque modification dépendraient de l’étape appliquée. La manière dont le cycle est mis en œuvre dépend de vous (y compris l'option d'une architecture basée sur les événements, comme vous en avez parlé).

Un exemple de cycle d'attaque pourrait être:

  • calculer l'attaque du joueur (base + mods);
  • calculer la défense de l'adversaire (base + mods);
  • faites la différence (et appliquez des mods) et déterminez les dégâts de base;
  • calculer tous les effets de parade / armure (mods sur les dégâts de base) et appliquer les dégâts;
  • calculez tout effet de recul (mods sur les dégâts de base) et appliquez-le à l'attaquant.

Il est important de noter que plus un buff est appliqué tôt dans le cycle, plus il aura d’effet sur le résultat . Donc, si vous voulez un combat plus "tactique" (où la compétence du joueur est plus importante que le niveau du personnage), créez beaucoup de buffs / debuffs sur les statistiques de base. Si vous souhaitez un combat plus "équilibré" (où le niveau importe plus - il est important dans les MMOG de limiter le taux de progression), utilisez uniquement les buffs / debuffs plus tard dans le cycle.

La distinction entre "Modifications" et "Améliorations" dont j'ai parlé plus tôt a un but: les décisions concernant les règles et l'équilibre peuvent être appliquées à la première, de sorte que toute modification apportée à celles-ci n'a pas besoin de refléter les modifications apportées à chaque classe de la dernière. OTOH, le nombre et le type de buffs ne sont limités que par votre imagination, chacun pouvant exprimer le comportement souhaité sans avoir à prendre en compte une éventuelle interaction entre lui et les autres (ni même l'existence des autres).

Donc, pour répondre à la question: ne créez pas de classe pour chaque buff, mais une classe pour chaque modification (type de), et liez la modification au cycle d'attaque, pas au personnage. Les buffs peuvent être simplement une liste de tuples (Modification, clé, valeur), et vous pouvez appliquer un buff à un personnage en ajoutant / supprimant simplement ce dernier dans son ensemble de buffs. Cela réduit également la fenêtre d'erreur, car les statistiques du personnage n'ont pas besoin d'être modifiées du tout lorsque les buffs sont appliqués (il y a donc moins de risque de restaurer une statistique à une valeur incorrecte après l'expiration d'un buff).


C’est une approche intéressante car elle se situe quelque part entre les deux mises en œuvre que j’avais envisagées - c’est-à-dire, restreindre simplement les améliorations à des modificateurs de statistiques et de résultats assez simples, ou créer un système très robuste mais avec beaucoup de temps système pouvant supporter à peu près tout. Il s’agit en quelque sorte d’une extension de l’ancien pour permettre aux "épines" tout en maintenant une interface simple. Bien que je ne pense pas que ce soit la solution miracle pour ce dont j'ai besoin, cela semble certainement rendre l'équilibrage beaucoup plus facile que les autres approches, alors c'est peut-être la voie à suivre. Merci pour votre contribution!
gkimsey

3

Je ne sais pas si vous le lisez encore, mais voici comment je le fais maintenant (le code est basé sur UE4 et C ++). Après avoir réfléchi au problème pendant plus de deux semaines (!!), j'ai finalement trouvé ceci:

http://gamedevelopment.tutsplus.com/tutorials/using-the-composite-design-pattern-for-an-rpg-attributes-system--gamedev-243

Et je me suis dit que bien, encapsuler un seul attribut au sein de la classe / structure n’est pas une si mauvaise idée après tout. Gardez cependant à l'esprit que je tire un très grand avantage du système de réflexion de code intégré à UE4. Par conséquent, sans quelques retouches, cela pourrait ne pas convenir partout.

De toute façon, j'ai commencé à encapsuler l'attribut dans une seule structure:

USTRUCT(BlueprintType)
struct GAMEATTRIBUTES_API FGAAttributeBase
{
    GENERATED_USTRUCT_BODY()
public:
    UPROPERTY()
        FName AttributeName;
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Value")
        float BaseValue;
    /*
        This is maxmum value of this attribute.
    */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Value")
        float ClampValue;
protected:
    float BonusValue;
    //float OldCurrentValue;
    float CurrentValue;
    float ChangedValue;

    //map of modifiers.
    //It could be TArray, but map seems easier to use in this case
    //we need to keep track of added/removed effects, and see 
    //if this effect affected this attribute.
    TMap<FGAEffectHandle, FGAModifier> Modifiers;

public:

    inline float GetFinalValue(){ return BaseValue + BonusValue; };
    inline float GetCurrentValue(){ return CurrentValue; };
    void UpdateAttribute();

    void Add(float ValueIn);
    void Subtract(float ValueIn);

    //inline float GetCurrentValue()
    //{
    //  return FMath::Clamp<float>(BaseValue + BonusValue + AccumulatedBonus, 0, GetFinalValue());;
    //}

    void AddBonus(const FGAModifier& ModifiersIn, const FGAEffectHandle& Handle);
    void RemoveBonus(const FGAEffectHandle& Handle);

    void InitializeAttribute();

    void CalculateBonus();

    inline bool operator== (const FGAAttributeBase& OtherAttribute) const
    {
        return (OtherAttribute.AttributeName == AttributeName);
    }

    inline bool operator!= (const FGAAttributeBase& OtherAttribute) const
    {
        return (OtherAttribute.AttributeName != AttributeName);
    }

    inline bool IsValid() const
    {
        return !AttributeName.IsNone();
    }
    friend uint32 GetTypeHash(const FGAAttributeBase& AttributeIn)
    {
        return AttributeIn.AttributeName.GetComparisonIndex();
    }
};

Ce n'est toujours pas fini mais l'idée de base est que cette structure garde une trace de son état interne. Les attributs ne peuvent être modifiés que par effets. Essayer de les modifier directement est dangereux et n’est pas exposé aux concepteurs. Je suppose que tout ce qui peut interagir avec les attributs est un effet. Y compris les bonus plats des articles. Lorsqu'un nouvel élément est équipé, un nouvel effet (avec la poignée) est créé et ajouté à la carte dédiée, qui gère les bonus de durée infinie (ceux qui doivent être supprimés manuellement par le joueur). Lorsqu'un nouvel effet est appliqué, un nouveau descripteur est créé (le descripteur est juste int, enveloppé avec une structure), puis ce descripteur est transmis de manière généralisée afin d'interagir avec cet effet, ainsi que le suivi de l'effet. toujours actif. Lorsque l'effet est supprimé, son traitement est diffusé à tous les objets intéressés,

La vraie partie importante de ceci est TMap (TMap est une carte hachée). FGAModifier est une structure très simple:

struct FGAModifier
{
    EGAAttributeOp AttributeMod;
    float Value;
};

Il contient le type de modification:

UENUM()
enum class EGAAttributeOp : uint8
{
    Add,
    Subtract,
    Multiply,
    Divide,
    Set,
    Precentage,

    Invalid
};

Et Valeur qui est la valeur finale calculée que nous allons appliquer à l’attribut.

Nous ajoutons un nouvel effet en utilisant une fonction simple, puis appelons:

void FGAAttributeBase::CalculateBonus()
{
    float AdditiveBonus = 0;
    auto ModIt = Modifiers.CreateConstIterator();
    for (ModIt; ModIt; ++ModIt)
    {
        switch (ModIt->Value.AttributeMod)
        {
        case EGAAttributeOp::Add:
            AdditiveBonus += ModIt->Value.Value;
                break;
            default:
                break;
        }
    }
    float OldBonus = BonusValue;
    //calculate final bonus from modifiers values.
    //we don't handle stacking here. It's checked and handled before effect is added.
    BonusValue = AdditiveBonus; 
    //this is absolute maximum (not clamped right now).
    float addValue = BonusValue - OldBonus;
    //reset to max = 200
    CurrentValue = CurrentValue + addValue;
}

Cette fonction est supposée recalculer toute la pile de bonus, chaque fois qu'un effet est ajouté ou supprimé. La fonction n'est toujours pas terminée (comme vous pouvez le voir), mais vous pouvez en avoir une idée générale.

Mon plus gros problème en ce moment est la gestion de l'attribut Damaging / Healing (sans impliquer de recalculer toute la pile), je pense avoir résolu ce problème, mais cela nécessite toujours plus de tests pour être à 100%.

Dans tous les cas, les attributs sont définis comme suit (+ macros Unreal, omis ici):

FGAAttributeBase Health;
FGAAttributeBase Energy;

etc.

De plus, je ne suis pas sûr à 100% de la gestion de CurrentValue d'attribut, mais cela devrait fonctionner. Ils sont comme ça maintenant.

Quoi qu’il en soit, j’espère que cela sauvera certaines personnes de la mémoire cache, sans savoir si c’est la meilleure solution, ni même une bonne solution, mais je l’aime plus que le suivi des effets indépendamment des attributs. Rendre chaque attribut dépistant son propre état est beaucoup plus facile dans ce cas, et devrait être moins sujet aux erreurs. Il n'y a essentiellement qu'un seul point d'échec, qui est une classe assez courte et simple.


Merci pour le lien et l'explication de votre travail! Je pense que vous vous dirigez essentiellement vers ce que je demandais. Quelques choses qui me viennent à l’esprit sont l’ordre des opérations (par exemple, 3 effets «ajouter» et 2 effets «multiplier» sur le même attribut, ce qui devrait se produire en premier?) Et c’est un support purement attribut. Il existe également la notion de déclencheurs (type d'effets du type "perdre 1 PA lorsque touché"), mais il s'agira probablement d'une enquête distincte.
gkimsey

L'ordre de fonctionnement, dans le cas où le calcul du bonus d'attribut est simple, est facile à faire. Vous pouvez voir ici que j'ai là pour et basculer. Pour itérer sur tous les bonus actuels (qui peuvent être ajoutés, soustraits, multipliés, divisés, etc.), puis simplement les accumuler. Le vous faites quelque chose comme BonusValue = (BonusValue * MultiplyBonus + AddBonus-SubtractBonus) / DivideBonus, Ou comme vous voulez regarder cette équation. En raison du point d'entrée unique, il est facile de l'expérimenter. Pour ce qui est des déclencheurs, je n’ai pas écrit à ce sujet, car c’est un autre problème sur lequel je réfléchis et j’ai déjà essayé la version 3-4 (limite)
Łukasz Baran le

solutions, aucune d’elles n’a fonctionné comme je le souhaitais (mon objectif principal, c’est d’être convivial pour les concepteurs). Mon idée générale est d'utiliser les balises et de vérifier les effets entrants par rapport aux balises. Si l'étiquette correspond, l'effet peut déclencher un autre effet. (la balise est un simple nom humain lisible, comme Damage.Fire, Attack.Physical etc.). Le concept de base est très simple, il s’agit d’organiser les données, d’être facilement accessible (rapide pour la recherche) et d’ajouter de nouveaux effets. Vous pouvez vérifier le code ici github.com/iniside/ActionRPGGame (GameAttributes est le module qui vous intéressera)
Łukasz Baran

2

J'ai travaillé sur un petit MMO et tous les objets, pouvoirs, buffs, etc. avaient des "effets". Un effet était une classe ayant des variables pour 'AddDefense', 'InstantDamage', 'HealHP', etc. Les pouvoirs, les éléments, etc. gèrent la durée de cet effet.

Lorsque vous lancez un pouvoir ou placez un objet, il applique l'effet au personnage pour la durée spécifiée. Ensuite, l’attaque principale, les calculs, etc. prendraient en compte les effets appliqués.

Par exemple, vous avez un buff qui ajoute une défense. Il y aurait au minimum un EffectID et une Durée pour ce buff. Lors de sa conversion, l’effet EffectID s’appliquerait au caractère pour la durée spécifiée.

Un autre exemple pour un article aurait les mêmes champs. Mais la durée serait infinie ou jusqu'à ce que l'effet soit supprimé en enlevant l'élément du personnage.

Cette méthode vous permet de parcourir une liste d’effets actuellement appliqués.

J'espère que j'ai expliqué cette méthode assez clairement.


Si je comprends bien mon expérience minimale, c’est la manière traditionnelle d’implémenter les mods statistiques dans les jeux de rôle. Cela fonctionne bien et est facile à comprendre et à mettre en œuvre. L'inconvénient est qu'il ne semble pas me laisser de place pour faire des choses comme le buff "d'épines", ou quelque chose de plus avancé ou de situation. Cela a aussi été historiquement la cause de certains exploits dans les RPG, bien qu'ils soient assez rares, et puisque je fais un jeu à un seul joueur, si quelqu'un trouve un exploit, je ne suis pas vraiment inquiet. Merci pour la contribution.
gkimsey

2
  1. Si vous êtes un utilisateur d’unité, voici quelque chose à commencer: http://www.stevegargolinski.com/armory-a-free-and-unfinished-stat-inventory-and-buffdebuff-framework-for-unity/

J'utilise ScriptableOjects en tant que buffs / sorts / talents

public class Spell : ScriptableObject 
{
    public SpellType SpellType = SpellType.Ability;
    public SpellTargetType SpellTargetType = SpellTargetType.SingleTarget;
    public SpellCategory SpellCategory = SpellCategory.Ability;
    public MagicSchools MagicSchool = MagicSchools.Physical;
    public CharacterClass CharacterClass = CharacterClass.None;
    public string Description = "no description available";
    public SpellDragType DragType = SpellDragType.Active; 
    public bool Active = false;
    public int TargetCount = 1;
    public float CastTime = 0;
    public uint EffectRange = 3;
    public int RequiredLevel = 1;
    public virtual void OnGUI()
    {
    }
}

using UnityEngine; using System.Collections.Generic;

public enum BuffType {Buff, Debuff} [System.Serializable] classe publique BuffStat {public Stat Stat = Stat.Strength; flottant public ModValueInPercent = 0.1f; }

public class Buff : Spell
{
    public BuffType BuffType = BuffType.Buff;
    public BuffStat[] ModStats;
    public bool PersistsThroughDeath = false;
    public int AmountPerTick = 3;
    public bool UseTickTimer = false;
    public float TickTime = 1.5f;
    [HideInInspector]
    public float Ticktimer = 0;
    public float Duration = 360; // in seconds
    public float ModifierPerStack = 1.1f;
    [HideInInspector]
    public float Timer = 0;
    public int Stack = 1;
    public int MaxStack = 1;
}

BuffModul:

using System;
using RPGCore;
using UnityEngine;

public class Buff_Modul : MonoBehaviour
{
    private Unit _unit;

    // Use this for initialization
    private void Awake()
    {
        _unit = GetComponent<Unit>();
    }

    #region BUFF MODUL

    public virtual void RUN_BUFF_MODUL()
    {
        try
        {
            foreach (var buff in _unit.Attr.Buffs)
            {
                CeckBuff(buff);
            }
        }
        catch(Exception e) {throw new Exception(e.ToString());}
    }

    #endregion BUFF MODUL

    public void ClearBuffs()
    {
        _unit.Attr.Buffs.Clear();
    }

    public void AddBuff(string buffName)
    {
        var buff = Instantiate(Resources.Load("Scriptable/Buff/" + buffName, typeof(Buff))) as Buff;
        if (buff == null) return;
        buff.name = buffName;
        buff.Timer = buff.Duration;
        _unit.Attr.Buffs.Add(buff);
        foreach (var buffStat in buff.ModStats)
        {
            switch (buff.BuffType)
            {
                case BuffType.Buff:
                    _unit.Attr.AddBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat] + _unit.Attr.StatsItem[buffStat.Stat]) * buffStat.ModValueInPercent));
                    break;
                case BuffType.Debuff:
                    _unit.Attr.RemoveBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat] /*+ unit.character.StatsItem[_stat.stat]*/) * buffStat.ModValueInPercent));
                    break;
            }
            Core.StatController(_unit.Attr, buffStat.Stat);
        }
    }

    public void RemoveBuff(Buff buff)
    {
        foreach (var buffStat in buff.ModStats)
        {
            switch (buff.BuffType)
            {
                case BuffType.Buff:
                    _unit.Attr.RemoveBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat] + _unit.Attr.StatsItem[buffStat.Stat]) * buffStat.ModValueInPercent));
                    break;
                case BuffType.Debuff:
                    _unit.Attr.AddBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat]  /*+ unit.character.StatsItem[_stat.stat]*/) * buffStat.ModValueInPercent));
                    break;
            }
            Core.StatController(_unit.Attr, buffStat.Stat);
        }
        _unit.Attr.Buffs.Remove(buff);
    }

    void CeckBuff(Buff buff)
    {
        buff.Timer -= Time.deltaTime;
        if (!_unit.IsAlive && !buff.PersistsThroughDeath)
        {
            if (buff.ModStats != null)
                foreach (var stat in buff.ModStats)
                {
                    _unit.Attr.StatsBuff[stat.Stat] = 0;
                }

            RemoveBuff(buff);
        }
        if (_unit.IsAlive && buff.Timer <= 0)
        {
            RemoveBuff(buff);
        }
    }
}

0

C'était une question réelle pour moi. J'ai une idée à ce sujet.

  1. Comme mentionné précédemment, nous devons implémenter une Buffliste et un programme de mise à jour logique pour les buffs.
  2. Nous devons ensuite modifier tous les paramètres de lecteur spécifiques à chaque image des sous-classes de la Buffclasse.
  3. Nous obtenons ensuite les paramètres actuels du lecteur à partir du champ des paramètres modifiables.

class Player {
  settings: AllPlayerStats;

  private buffs: Array<Buff> = [];
  private baseSettings: AllPlayerStats;

  constructor(settings: AllPlayerStats) {
    this.baseSettings = settings;
    this.resetSettings();
  }

  addBuff(buff: Buff): void {
    this.buffs.push(buff);
    buff.start(this);
  }

  findBuff(predcate(buff: Buff) => boolean): Buff {...}

  removeBuff(buff: Buff): void {...}

  update(dt: number): void {
    this.resetSettings();
    this.buffs.forEach((item) => item.update(dt));
  }

  private resetSettings(): void {
    //some way to copy base to settings
    this.settings = this.baseSettings.copy();
  }
}

class Buff {
    private owner: Player;        

    start(owner: Player) { this.owner = owner; }

    update(dt: number): void {
      //here we change anything we want in subclasses like
      this.owner.settings.hp += 15;
      //if we need base value, just make owner.baseSettings public but don't change it! only read

      //also here logic for removal buff by time or something
    }
}

De cette manière, il peut être facile d’ajouter de nouvelles statistiques de joueur, sans aucun changement dans la logique des Buffsous - classes.


0

Je sais que c’est assez vieux, mais c’est lié à un post plus récent et j’aimerais y réfléchir. Malheureusement, je n'ai pas mes notes avec moi pour le moment, je vais donc essayer de vous donner un aperçu général de ce dont je parle et de modifier les détails ainsi qu'un exemple de code lorsque je l'aurai devant vous. moi.

Premièrement, je pense que du point de vue de la conception, la plupart des gens sont trop pris au piège des types de buffs pouvant être créés et de la façon dont ils sont appliqués, en oubliant les principes de base de la programmation orientée objet.

Qu'est ce que je veux dire? Peu importe qu’il s’agisse d’un buff ou d’un debuff, ce sont deux modificateurs qui affectent quelque chose de manière positive ou négative. Le code ne tient pas compte de qui est qui. D'ailleurs, peu importe si quelque chose ajoute des statistiques ou les multiplie, ce ne sont que des opérateurs différents et, là encore, le code n'a pas d'importance.

Alors, où vais-je avec ça? Il n’est pas si difficile de concevoir une bonne classe de buff / debuff (lire: simple et élégante). Ce qui est difficile, c’est de concevoir les systèmes qui calculent et maintiennent l’état du jeu.

Si je concevais un système buff / debuff, voici quelques points à considérer:

  • Une classe buff / debuff pour représenter l'effet lui-même.
  • Une classe de type buff / debuff pour contenir les informations sur ce que le buff affecte et comment.
  • Les personnages, les éléments et peut-être les emplacements devraient tous avoir une propriété list ou collection pour contenir les buffs et les debuffs.

Quelques détails sur les types de buff / debuff qui doivent contenir:

  • À qui / à quoi il peut être appliqué, IE: joueur, monstre, emplacement, objet, etc.
  • De quel type d’effet il s’agit (positif, négatif), qu’il soit multiplicatif ou additif, et quel type de statistiques il influe, par exemple: attaque, défense, mouvement, etc.
  • Quand doit-il être vérifié (combat, heure du jour, etc.).
  • S'il peut être supprimé et, le cas échéant, comment il peut être supprimé.

Ce n'est qu'un début, mais à partir de là, vous définissez simplement ce que vous voulez et agissez en fonction de votre état de jeu normal. Par exemple, supposons que vous vouliez créer un objet maudit qui réduise la vitesse de déplacement ...

Tant que j'ai mis les types appropriés en place, il est simple de créer un enregistrement de buff qui dit:

  • Type: Malédiction
  • ObjectType: Item
  • StatCategory: Utilitaire
  • StatAffected: MovementSpeed
  • Durée: infinie
  • Déclencheur: OnEquip

Et ainsi de suite, et quand je crée un buff, je lui attribue simplement le BuffType of Curse et tout le reste appartient au moteur ...

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.