Utilisation du modèle de conception composite pour un système d'attributs RPG

Intelligence, volonté, charisme, sagesse: en plus d'être des qualités importantes que vous devriez posséder en tant que développeur de jeux, ces attributs sont également utilisés dans les jeux de rôle. Le calcul des valeurs de tels attributs - appliquer des bonus minutés et prendre en compte l'effet des objets équipés - peut être délicat. Dans ce tutoriel, je vais vous montrer comment utiliser un motif composite légèrement modifié pour gérer ceci, à la volée..

Remarque: Bien que ce tutoriel soit écrit en utilisant Flash et AS3, vous devriez pouvoir utiliser les mêmes techniques et concepts dans presque tous les environnements de développement de jeux..


introduction

Les systèmes d'attributs sont très couramment utilisés dans les RPG pour quantifier les forces, les faiblesses et les capacités des personnages. Si vous ne les connaissez pas, parcourez la page Wikipedia pour un bon aperçu..

Pour les rendre plus dynamiques et intéressants, les développeurs améliorent souvent ces systèmes en ajoutant des compétences, des éléments et d'autres éléments ayant une incidence sur les attributs. Si vous voulez le faire, vous aurez besoin d’un bon système capable de calculer les attributs finaux (en prenant en considération tous les autres effets) et de gérer l’ajout ou le retrait de différents types de bonus..

Dans ce didacticiel, nous allons explorer une solution à ce problème en utilisant une version légèrement modifiée du motif de conception Composite. Notre solution sera capable de gérer les bonus et fonctionnera sur n'importe quel ensemble d'attributs que vous définissez..


Quel est le motif composite?

Cette section présente un aperçu du modèle de conception Composite. Si vous le connaissez déjà, vous pouvez passer directement à Modéliser notre problème.

Le motif composite est un motif de conception (un modèle de conception général bien connu, réutilisable) permettant de subdiviser un objet volumineux en objets plus petits, afin de créer un groupe plus important en ne gérant que les objets de petite taille. Il est facile de diviser de gros morceaux d’informations en morceaux plus petits, plus faciles à traiter. Il s’agit essentiellement d’un modèle permettant d’utiliser un groupe d’un objet particulier comme s’il s’agissait d’un seul objet..

Nous allons utiliser un exemple largement utilisé pour illustrer ceci: pensez à une application de dessin simple. Vous voulez qu'il vous permette de dessiner des triangles, des carrés et des cercles et de les traiter différemment. Mais vous souhaitez également pouvoir gérer des groupes de dessins. Comment pouvons-nous facilement faire ça?

Le motif composite est le candidat idéal pour ce travail. En traitant un "groupe de dessins" comme un dessin, on pourrait facilement ajouter n'importe quel dessin à l'intérieur de ce groupe, et le groupe dans son ensemble serait toujours considéré comme un dessin unique..

En termes de programmation, nous aurions une classe de base, Dessin, qui a les comportements par défaut d'un dessin (vous pouvez le déplacer, changer les calques, le faire pivoter, etc.) et quatre sous-classes, Triangle, Carré, Cercle et Groupe.

Dans ce cas, les trois premières classes auront un comportement simple, nécessitant uniquement la saisie par l'utilisateur des attributs de base de chaque forme. le Groupe Cependant, la classe aura des méthodes pour ajouter et supprimer des formes, ainsi que pour effectuer une opération sur chacune d’elles (par exemple, changer la couleur de toutes les formes d’un groupe à la fois). Les quatre sous-classes seraient toujours traitées comme des Dessin, vous n'avez donc pas à vous soucier de l'ajout de code spécifique lorsque vous souhaitez opérer sur un groupe.

Pour mieux prendre cela en compte, nous pouvons voir chaque dessin comme un nœud dans un arbre. Chaque nœud est une feuille, à l'exception de Groupe nœuds, qui peuvent avoir des enfants - qui sont à leur tour des dessins à l'intérieur de ce groupe.


Une représentation visuelle du motif

Dans l'exemple de l'application de dessin, il s'agit d'une représentation visuelle de "l'application de dessin" à laquelle nous avons pensé. Notez qu'il y a trois dessins dans l'image: un triangle, un carré et un groupe constitué d'un cercle et d'un carré:

Et voici l'arborescence de la scène actuelle (la racine est l'étape de l'application de dessin):

Et si nous voulions ajouter un autre dessin, qui est un groupe d'un triangle et d'un cercle, à l'intérieur du groupe que nous avons actuellement? Nous l'ajouterions simplement comme nous ajouterions n'importe quel dessin à l'intérieur d'un groupe. Voici à quoi ressemblerait la représentation visuelle:

Et voici ce que l’arbre deviendrait:

Maintenant, imaginons que nous construisions une solution au problème des attributs que nous avons. Évidemment, nous n'allons pas avoir de représentation visuelle directe (nous ne pouvons voir que le résultat final, qui est l'attribut calculé compte tenu des valeurs brutes et des bonus), nous allons donc commencer à penser dans le motif composite avec la représentation arborescente..


Modéliser notre problème

Afin de pouvoir modéliser nos attributs dans un arbre, nous devons décomposer chaque attribut en parties les plus petites possibles..

Nous savons que nous avons des bonus, qui peuvent soit ajouter une valeur brute à l'attribut, soit l'augmenter d'un pourcentage. Certains bonus s'ajoutent à l'attribut et d'autres sont calculés après l'application de tous les premiers bonus (bonus de compétences, par exemple)..

Donc, on peut avoir:

  • Bonus bruts (ajoutés à la valeur brute de l'attribut)
  • Bonus finaux (ajoutés à l'attribut après que tout le reste a été calculé)

Vous avez peut-être remarqué que nous ne séparons pas les bonus qui ajoutent une valeur à l'attribut des bonus qui augmentent l'attribut d'un pourcentage. C'est parce que nous modélisons chaque bonus pour pouvoir changer en même temps. Cela signifie que nous pourrions avoir un bonus qui ajoute 5 à la valeur et augmente l'attribut de 10%. Tout cela sera traité dans le code.

Ces deux types de bonus ne sont que les feuilles de notre arbre. Ils sont à peu près comme le Triangle, Carré et Cercle classes dans notre exemple d'avant.

Nous n'avons toujours pas créé d'entité qui serve de groupe. Ces entités seront les attributs eux-mêmes! le Groupe La classe dans notre exemple sera simplement l'attribut lui-même. Donc nous aurons un Attribut classe qui se comportera comme tout attribut.

Voici à quoi pourrait ressembler une arborescence d'attributs:

Maintenant que tout est décidé, allons-nous commencer notre code?


Création des classes de base

Nous utiliserons ActionScript 3.0 comme langage de code dans ce didacticiel, mais ne vous inquiétez pas! Le code sera entièrement commenté par la suite, et tout ce qui est unique à la langue (et à la plate-forme Flash) sera expliqué et des alternatives seront fournies - donc si vous êtes familier avec une langue de POO, vous pourrez la suivre. tutoriel sans problèmes.

La première classe que nous devons créer est la classe de base pour tout attribut et bonus. Le fichier sera appelé BaseAttribute.as, et le créer est très simple. Voici le code, avec des commentaires après:

 package classe publique BaseAttribute private var _baseValue: int; private var _baseMultiplier: Number; fonction publique BaseAttribute (valeur: int, multiplicateur: Nombre = 0) _baseValue = valeur; _baseMultiplier = multiplicateur;  fonction publique get baseValue (): int return _baseValue;  fonction publique get baseMultiplier (): Number return _baseMultiplier; 

Comme vous pouvez le constater, les choses sont très simples dans cette classe de base. Nous venons de créer le _valeur et _multiplicateur champs, les assigner dans le constructeur et créer deux méthodes de lecture, une pour chaque champ.

Maintenant, nous devons créer le RawBonus et FinalBonus Des classes. Ce sont simplement des sous-classes de BaseAttribute, avec rien ajouté. Vous pouvez développer autant que vous voulez, mais pour le moment, nous ne ferons que ces deux sous-classes vierges de BaseAttribute:

RawBonus.as:

 package classe publique RawBonus étend BaseAttribute fonction publique RawBonus (valeur: int = 0, multiplicateur: Nombre = 0) super (valeur, multiplicateur); 

FinalBonus.as:

 package classe publique FinalBonus étend BaseAttribute fonction publique FinalBonus (valeur: int = 0, multiplicateur: Nombre = 0) super (valeur, multiplicateur); 

Comme vous pouvez le constater, ces classes ne contiennent qu'un constructeur..


La classe d'attribut

le Attribut La classe sera l'équivalent d'un groupe dans le motif composite. Il peut contenir tous les bonus bruts ou finaux, et aura une méthode pour calculer la valeur finale de l'attribut. Puisqu'il s'agit d'une sous-classe de BaseAttribute, la _baseValue le champ de la classe sera la valeur de départ de l'attribut.

Lors de la création de la classe, nous aurons un problème lors du calcul de la valeur finale de l'attribut: étant donné que nous ne séparons pas les bonus bruts des bonus finaux, il est impossible de calculer la valeur finale, car nous ne savons pas quand appliquer chaque bonus.

Cela peut être résolu en apportant une légère modification au motif composite de base. Au lieu d'ajouter n'importe quel enfant au même "conteneur" dans le groupe, nous allons créer deux "conteneurs", l'un pour les bonus bruts et l'autre pour les bonus finaux. Chaque bonus sera toujours un enfant de Attribut, mais seront à des endroits différents pour permettre le calcul de la valeur finale de l'attribut.

Avec cela expliqué, passons au code!

 package public class Attribut étend BaseAttribute private var _rawBonuses: Array; private var _finalBonuses: Array; private var _finalValue: int; fonction publique Attribute (startupValue: int) super (startupValue); _rawBbonuses = []; _finalBonuses = []; _finalValue = baseValue;  fonction publique addRawBonus (bonus: RawBonus): void _rawBonuses.push (bonus);  fonction publique addFinalBonus (bonus: FinalBonus): void _finalBonuses.push (bonus);  fonction publique removeRawBonus (bonus: RawBonus): void if (_rawBonuses.indexOf (bonus)> = 0) _rawBonuses.splice (_rawBonuses.indexOf (bonus), 1);  fonction publique removeFinalBonus (bonus: FinalBonus): void if (_finalBonuses.indexOf (bonus)> = 0) _finalBonuses.splice (_finalBonuses.indexOf (bonus), 1);  public function CalculateValue (): int _finalValue = baseValue; // Ajout de valeur à partir de raw var rawBonusValue: int = 0; var rawBonusMultiplier: Number = 0; pour chaque (bonus var: RawBonus dans _rawBonuses) rawBonusValue + = bonus.baseValue; rawBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = rawBonusValue; _finalValue * = (1 + rawBonusMultiplier); // Ajout de la valeur de la variable finale var finalBonusValue: int = 0; var finalBonusMultiplier: Number = 0; pour chaque (bonus var: FinalBonus dans _finalBonuses) finalBonusValue + = bonus.baseValue; finalBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = finalBonusValue; _finalValue * = (1 + finalBonusMultiplier); return _finalValue;  fonction publique get finalValue (): int return CalculateValue (); 

Les méthodes addRawBonus (), addFinalBonus (), removeRawBonus () et removeFinalBonus () sont très clairs. Tout ce qu’ils font est d’ajouter ou de supprimer leur type de bonus spécifique au tableau contenant tous les bonus de ce type..

La partie délicate est la CalculateValue () méthode. Premièrement, il résume toutes les valeurs que les bonus bruts ajoutent à l'attribut, ainsi que tous les multiplicateurs. Après cela, il ajoute la somme de toutes les valeurs de bonus brutes à l'attribut de départ, puis applique le multiplicateur. Plus tard, il en va de même pour les bonus finaux, mais cette fois en appliquant les valeurs et les multiplicateurs à la valeur d'attribut final semi-calculée.

Et nous en avons fini avec la structure! Vérifiez les prochaines étapes pour voir comment vous utiliseriez et étendez-le.


Comportement supplémentaire: bonus chronométrés

Dans notre structure actuelle, nous ne disposons que de simples bonus bruts et finaux, qui ne présentent actuellement aucune différence. Dans cette étape, nous allons ajouter un comportement supplémentaire à la FinalBonus classe, afin de le faire ressembler plus à des bonus qui seraient appliqués à travers actif compétences dans un jeu.

Comme, comme leur nom l’indique, de telles compétences ne sont actives que pendant un certain temps, nous ajouterons un comportement de chronométrage aux bonus finaux. Les bonus bruts peuvent être utilisés, par exemple, pour des bonus ajoutés via des équipements.

Pour ce faire, nous utiliserons le Minuteur classe. Cette classe est native d'ActionScript 3.0. Elle se comporte comme un minuteur, commençant à 0 seconde, puis appelant une fonction spécifiée après un laps de temps donné, réinitialisant à 0 et recommençant le compte jusqu'à ce qu'elle atteigne la valeur spécifiée. nombre de comptes encore. Si vous ne les spécifiez pas, le Minuteur continuera à fonctionner jusqu'à ce que vous l'arrêtez. Vous pouvez choisir quand le minuteur démarre et quand il s’arrête. Vous pouvez reproduire son comportement simplement en utilisant les systèmes de synchronisation de votre langue avec du code supplémentaire approprié, si nécessaire..

Passons au code!

 package import flash.events.TimerEvent; import flash.utils.Timer; Classe publique FinalBonus étend BaseAttribute private var _timer: Timer; private var _parent: attribut; fonction publique FinalBonus (heure: int, valeur: int = 0, multiplicateur: Nombre = 0) super (valeur, multiplicateur); _timer = new Timer (heure); _timer.addEventListener (TimerEvent.TIMER, onTimerEnd);  fonction publique startTimer (parent: attribut): void _parent = parent; _timer.start ();  fonction privée onTimerEnd (e: TimerEvent): void _timer.stop (); _parent.removeFinalBonus (this); 

Dans le constructeur, la première différence est que les bonus finaux nécessitent maintenant un temps paramètre, qui montrera pendant combien de temps ils durent. À l'intérieur du constructeur, nous créons un Minuteur pendant ce laps de temps (en supposant que le temps est en millisecondes) et ajoutez-lui un écouteur d'événement.

(Les écouteurs d’événement sont essentiellement ce qui fera que la minuterie appellera la bonne fonction quand elle atteindra cette certaine période de temps - dans ce cas, la fonction à appeler est onTimerEnd ().)

Notez que nous n'avons pas encore démarré le chronomètre. Ceci est fait dans le startTimer () méthode, qui nécessite également un paramètre, parent, qui doit être un Attribut. Cette fonction nécessite l'attribut qui ajoute le bonus pour appeler cette fonction afin de l'activer. à son tour, cela démarre le chronomètre et indique au bonus quelle instance demander de supprimer le bonus lorsque le chronomètre a atteint sa limite.

La partie enlèvement se fait dans le onTimerEnd () méthode, qui demandera simplement au parent du groupe de le supprimer et d’arrêter le chronomètre.

Maintenant, nous pouvons utiliser les bonus finaux en tant que bonus chronométrés, indiquant qu'ils ne dureront qu'un certain temps..


Comportement supplémentaire: attributs dépendants

Une chose que l’on voit souvent dans les jeux de rôle est un attribut qui dépend des autres. Prenons, par exemple, l'attribut "vitesse d'attaque". Cela dépend non seulement du type d'arme que vous utilisez, mais aussi presque toujours de la dextérité du personnage..

Dans notre système actuel, nous autorisons uniquement les bonus à être des enfants de Attribut les instances. Mais dans notre exemple, nous devons laisser un attribut devenir l'enfant d'un autre attribut. Comment peut-on faire ça? Nous pouvons créer une sous-classe de Attribut, appelé DépendantAttribut, et donner à cette sous-classe tout le comportement dont nous avons besoin.

L'ajout d'attributs en tant qu'enfants est très simple: il suffit de créer un autre tableau pour contenir les attributs et d'ajouter un code spécifique pour le calcul de l'attribut final. Puisque nous ne savons pas si chaque attribut sera calculé de la même manière (vous pouvez utiliser d’abord la dextérité pour modifier la vitesse d’attaque, puis vérifiez les bonus, mais utilisez d’abord les bonus pour modifier l’attaque magique, puis utilisez par exemple: intelligence), nous devrons également séparer le calcul de l'attribut final dans le Attribut classe dans différentes fonctions. Faisons-le d'abord.

Dans Attribut.as:

 package public class Attribut étend BaseAttribute private var _rawBonuses: Array; private var _finalBonuses: Array; protected var _finalValue: int; fonction publique Attribute (startupValue: int) super (startupValue); _rawBbonuses = []; _finalBonuses = []; _finalValue = baseValue;  fonction publique addRawBonus (bonus: RawBonus): void _rawBonuses.push (bonus);  fonction publique addFinalBonus (bonus: FinalBonus): void _finalBonuses.push (bonus);  fonction publique removeRawBonus (bonus: RawBonus): void if (_rawBonuses.indexOf (bonus)> = 0) _rawBonuses.splice (_rawBonuses.indexOf (bonus), 1);  fonction publique removeFinalBonus (bonus: RawBonus): void if (_finalBonuses.indexOf (bonus)> = 0) _finalBonuses.splice (_finalBonuses.indexOf (bonus), 1);  fonction protégée applyRawBonuses (): void // Ajout d'une valeur à partir de raw var rawBonusValue: int = 0; var rawBonusMultiplier: Number = 0; pour chaque (bonus var: RawBonus dans _rawBonuses) rawBonusValue + = bonus.baseValue; rawBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = rawBonusValue; _finalValue * = (1 + rawBonusMultiplier);  fonction protégée applyFinalBonuses (): void // Ajout d'une valeur à partir de la variable finale var finalBonusValue: int = 0; var finalBonusMultiplier: Number = 0; pour chaque (bonus var: RawBonus dans _finalBonuses) finalBonusValue + = bonus.baseValue; finalBonusMultiplier + = bonus.baseMultiplier;  _finalValue + = finalBonusValue; _finalValue * = (1 + finalBonusMultiplier);  fonction publique CalculateValue (): int _finalValue = baseValue; applyRawBonuses (); applyFinalBonuses (); return _finalValue;  fonction publique get finalValue (): int return CalculateValue (); 

Comme vous pouvez le voir avec les lignes en surbrillance, tout ce que nous avons fait a été de créer applyRawBonuses () et applyFinalBonuses () et appelez-les lors du calcul de l'attribut final dans CalculateValue (). Nous avons également fait _finalValue protégé, afin que nous puissions le changer dans les sous-classes.

Maintenant, tout est mis pour que nous puissions créer le DépendantAttribut classe! Voici le code:

 package classe publique DependantAttribute extend Attribute protected var _otherAttributes: Array; fonction publique DependantAttribute (startupValue: int) super (startupValue); _otherAttributes = [];  fonction publique addAttribute (attr: Attribute): void _otherAttributes.push (attr);  fonction publique removeAttribute (attr: Attribute): void if (_otherAttributes.indexOf (attr)> = 0) _otherAttributes.splice (_otherAttributes.indexOf (attr), 1);  public override function CalculateValue (): int // Le code de l'attribut spécifique va quelque part ici _finalValue = baseValue; applyRawBonuses (); applyFinalBonuses (); return _finalValue; 

Dans cette classe, le ajouter un attribut() et removeAttribute () les fonctions doivent vous être familières. Vous devez faire attention à la décision CalculateValue () une fonction. Ici, nous n'utilisons pas les attributs pour calculer la valeur finale - vous devez le faire pour chaque attribut dépendant!

Voici un exemple de la façon dont vous feriez cela pour calculer la vitesse d'attaque:

 package public class AttackSpeed ​​extend DependantAttribute public function AttackSpeed ​​(startingValue: int) super (startingValue);  public override function CalculateValue (): int _finalValue = baseValue; // Tous les 5 points de dextérité ajoute 1 à la vitesse d'attaque. Var dextérité: int = _otherAttributes [0] .calculateValue (); _finalValue + = int (dextérité / 5); applyRawBonuses (); applyFinalBonuses (); return _finalValue; 

Dans cette classe, nous supposons que vous avez déjà ajouté l'attribut de dextérité en tant qu'enfant de Vitesse d'attaque, et que c'est le premier dans le _autresAttributs tableau (c’est beaucoup d’hypothèses à faire; vérifiez la conclusion pour plus d’informations). Après avoir récupéré la dextérité, nous l’utilisons simplement pour ajouter plus à la valeur finale de la vitesse d’attaque.


Conclusion

Avec tout terminé, comment utiliseriez-vous cette structure dans un jeu? C’est très simple: il suffit de créer différents attributs et d’affecter à chacun d’eux un Attribut exemple. Après cela, il suffit d'ajouter et de supprimer les bonus grâce aux méthodes déjà créées..

Lorsqu'un objet est équipé ou utilisé et qu'il ajoute un bonus à un attribut, vous devez créer une instance de bonus du type correspondant, puis l'ajouter à l'attribut du personnage. Après cela, il suffit de recalculer la valeur d'attribut finale.

Vous pouvez également développer les différents types de bonus disponibles. Par exemple, vous pourriez avoir un bonus qui change la valeur ajoutée ou le multiplicateur au fil du temps. Vous pouvez également utiliser des bonus négatifs (que le code actuel peut déjà gérer).

Avec n'importe quel système, il y a toujours plus à ajouter. Voici quelques suggestions d'améliorations à apporter:

  • Identifier les attributs par leurs noms
  • Faire un système "centralisé" pour gérer les attributs
  • Optimiser les performances (indice: vous ne devez pas toujours calculer entièrement la valeur finale)
  • Permettre à certains bonus d'atténuer ou de renforcer d'autres bonus

Merci d'avoir lu!