Suis-je sur la bonne voie avec cette architecture de composants?

9

J'ai récemment décidé de réorganiser mon architecture de jeu pour se débarrasser des hiérarchies de classes profondes et les remplacer par des composants configurables. La première hiérarchie que je remplace est la hiérarchie des articles et j'aimerais avoir des conseils pour savoir si je suis sur la bonne voie.

Auparavant, j'avais une hiérarchie qui ressemblait à ceci:

Item -> Equipment -> Weapon
                  -> Armor
                  -> Accessory
     -> SyntehsisItem
     -> BattleUseItem -> HealingItem
                      -> ThrowingItem -> ThrowsAsAttackItem

Inutile de dire que cela commençait à devenir désordonné et que ce n'était pas une solution facile aux objets qui devaient être de types multiples (c'est-à-dire que certains équipements sont utilisés dans la synthèse des objets, certains équipements sont jetables, etc.)

J'ai ensuite tenté de refactoriser et de placer des fonctionnalités dans la classe d'objet de base. Mais alors je notais que l'article avait beaucoup de données inutilisées / superflues. Maintenant, j'essaie de faire un composant comme l'architecture, au moins pour mes objets avant d'essayer de le faire pour mes autres classes de jeu.

Voici ce que je pense actuellement pour la configuration des composants:

J'ai une classe d'objet de base qui a des emplacements pour divers composants (c'est-à-dire un emplacement de composant d'équipement, un emplacement de composant de guérison, etc. ainsi qu'une carte pour des composants arbitraires) donc quelque chose comme ceci:

class Item
{
    //Basic item properties (name, ID, etc.) excluded
    EquipmentComponent* equipmentComponent;
    HealingComponent* healingComponent;
    SynthesisComponent* synthesisComponent;
    ThrowComponent* throwComponent;
    boost::unordered_map<std::string, std::pair<bool, ItemComponent*> > AdditionalComponents;
} 

Tous les composants d'élément hériteraient d'une classe ItemComponent de base, et chaque type de composant est chargé d'indiquer au moteur comment implémenter cette fonctionnalité. c'est-à-dire que le HealingComponent indique aux mécaniciens de bataille comment consommer l'objet en tant qu'objet de guérison, tandis que le ThrowComponent indique au moteur de combat comment traiter l'objet en tant qu'objet jetable.

La carte est utilisée pour stocker des composants arbitraires qui ne sont pas des composants d'élément de base. Je l'associe à un booléen pour indiquer si le conteneur d'articles doit gérer le composant d'élément ou s'il est géré par une source externe.

Mon idée ici était de définir les composants de base utilisés par mon moteur de jeu à l'avance, et ma fabrique d'articles assignerait les composants que l'élément a réellement, sinon ils sont nuls. La carte contiendrait des composants arbitraires qui seraient généralement ajoutés / consommés par les fichiers de script.

Ma question est, est-ce un bon design? Sinon, comment peut-il être amélioré? J'ai envisagé de regrouper tous les composants dans la carte, mais l'utilisation de l'indexation des chaînes semblait inutile pour les composants principaux de l'élément

user127817
la source

Réponses:

8

Cela semble être une première étape très raisonnable.

Vous optez pour une combinaison de généralité (la carte des "composants supplémentaires") et de performances de recherche (les membres codés en dur), ce qui peut être un peu une pré-optimisation - votre point concernant l'inefficacité générale des chaînes de caractères la recherche est bien faite, mais vous pouvez atténuer cela en choisissant d'indexer les composants par quelque chose de plus rapide à hacher. Une approche pourrait être de donner à chaque type de composant un ID de type unique (essentiellement vous implémentez un RTTI personnalisé léger ) et un index basé sur cela.

Quoi qu'il en soit, je vous conseille d'exposer une API publique pour l'objet Item qui vous permet de demander n'importe quel composant - ceux codés en dur et les autres - de manière uniforme. Cela faciliterait la modification de la représentation ou de l'équilibre sous-jacent des composants codés en dur / non codés en dur sans avoir à refactoriser tous les clients de composants d'article.

Vous pouvez également envisager de fournir des versions sans opération «factices» de chacun des composants codés en dur et de vous assurer qu'ils sont toujours attribués - vous pouvez ensuite utiliser des membres de référence au lieu de pointeurs, et vous n'aurez jamais besoin de rechercher un pointeur NULL avant d'interagir avec l'une des classes de composants codés en dur. Vous encourrez toujours le coût de la répartition dynamique pour interagir avec les membres de ce composant, mais cela se produirait même avec les membres du pointeur. Il s'agit davantage d'un problème de propreté du code car l'impact sur les performances sera très probablement négligeable.

Je ne pense pas que ce soit une bonne idée d'avoir deux types différents d'étendues à vie (en d'autres termes, je ne pense pas que le bool que vous avez dans la carte des composants supplémentaires soit une excellente idée). Cela complique le système et implique que la destruction et la libération des ressources ne seront pas terriblement déterministes. L'API de vos composants serait beaucoup plus claire si vous optez pour une stratégie de gestion de la vie ou l'autre - soit l'entité gère la durée de vie du composant, soit le sous-système qui réalise les composants le fait (je préfère ce dernier car il se marie mieux avec le composant externe approche, dont je parlerai ensuite).

Le gros inconvénient que je vois avec votre approche est que vous regroupez tous les composants dans l'objet "entité", ce qui n'est pas toujours le meilleur design. De ma réponse connexe à une autre question basée sur les composants:

Votre approche consistant à utiliser une grande carte de composants et un appel update () dans l'objet de jeu est tout à fait sous-optimale (et un piège courant pour ceux qui construisent d'abord ce type de systèmes). Cela rend la cohérence du cache très médiocre pendant la mise à jour et ne vous permet pas de profiter de la simultanéité et de la tendance vers un processus de style SIMD de grands lots de données ou de comportement à la fois. Il est souvent préférable d'utiliser une conception où l'objet de jeu ne met pas à jour ses composants, mais plutôt le sous-système responsable du composant lui-même les met à jour en même temps.

Vous adoptez essentiellement la même approche en stockant les composants dans l'entité article (ce qui est, encore une fois, une première étape entièrement acceptable). Ce que vous découvrirez peut-être, c'est que l'essentiel de l'accès aux composants dont vous vous souciez des performances consiste simplement à les mettre à jour, et si vous choisissez d'utiliser une approche plus externe de l'organisation des composants, où les composants sont conservés dans un cache cohérent , structure de données efficace (pour leur domaine) par un sous-système qui comprend le mieux leurs besoins, vous pouvez obtenir des performances de mise à jour bien meilleures et plus parallélisables.

Mais je le signale seulement comme quelque chose à considérer comme une direction future - vous ne voulez certainement pas aller trop loin pour trop concevoir cela; vous pouvez effectuer une transition progressive grâce à une refactorisation constante ou vous pouvez découvrir que votre implémentation actuelle répond parfaitement à vos besoins et qu'il n'est pas nécessaire de l'itérer.

Communauté
la source
1
+1 pour avoir suggéré de se débarrasser de l'objet Item. Bien que ce soit plus de travail à l'avance, cela finira par produire un meilleur système de composants.
James
J'avais quelques questions supplémentaires, je ne sais pas si je devrais commencer un nouveau topiuc, donc j'essaierai d'abord ici: Pour ma classe d'élément, il n'y a aucune méthode que j'appellerai Evert Frame (ou même close). Pour mes sous-systèmes graphiques, je prendrai vos conseils et garderai tous les objets à mettre à jour sous le système. L'autre question que j'avais était de savoir comment gérer les vérifications des composants? Comme par exemple, je veux voir si je peux utiliser un élément comme X, donc naturellement je vérifierais si l'élément a le composant nécessaire pour effectuer X. Est-ce la bonne façon de le faire? Merci encore pour la réponse
user127817