Un groupe d'amis et moi avons travaillé sur un projet ces derniers temps, et nous voulions inventer une manière agréable OOP de représenter un scénario spécifique à notre produit. Fondamentalement, nous sommes travailler sur un style Touhojeu d'enfer de balle , et nous voulions créer un système où nous pourrions facilement représenter tout comportement possible de balle que nous pourrions imaginer.
C'est exactement ce que nous avons fait; nous avons fait une architecture vraiment élégante qui nous a permis de séparer le comportement d'une balle en différents composants qui pourraient être attachés aux instances de balle à volonté, un peu comme nity's système de composants. Il fonctionnait bien, il était facilement extensible, il était flexible et couvrait toutes nos bases, mais il y avait un léger problème.
Notre application implique également une grande quantité de génération procédurale, à savoir que nous générons de manière procédurale les comportements des balles. Pourquoi c'est un problème? Eh bien, notre solution OOP pour représenter le comportement des balles, bien qu'élégante, est un peu compliquée à travailler sans être humain. Les humains sont assez intelligents pour penser à des solutions à des problèmes à la fois logiques et intelligents. Les algorithmes de génération procédurale ne sont pas encore aussi intelligents, et nous avons trouvé difficile d'implémenter une IA qui utilise notre architecture OOP à son plein potentiel. Certes, c'est un défaut de l'architecture est que ce n'est pas intuitif dans toutes les situations.
Donc, pour remédier à ce problème, nous avons essentiellement inséré tous les comportements offerts par différents composants dans la classe de puce, de sorte que tout ce que nous pouvions imaginer soit proposé directement dans chaque instance de puce, par opposition à d'autres instances de composants associées. Cela rend nos algorithmes de génération procédurale un peu plus faciles à utiliser, mais maintenant notre classe de puce est un énorme objet divin . C'est de loin la classe la plus importante du programme avec plus de cinq fois plus de code qu'autre chose. C'est aussi un peu pénible à entretenir.
Est-il normal qu'une de nos classes se transforme en objet divin, juste pour faciliter le travail avec un autre problème? En général, est-il acceptable d'avoir des odeurs de code dans votre code s'il admet une solution plus facile à un problème différent?
Lors de la création de programmes du monde réel, il y a souvent un compromis entre rester pragmatique d'une part et rester 100% propre de l'autre. Si rester propre vous interdit d'expédier votre produit à temps, alors vous feriez mieux avec un peu de ruban adhésif pour sortir la chose d *** d de la porte.
Dit que, votre description semble différente - il semble que vous n'allez pas ajouter un peu de ruban adhésif, il semble que vous allez ruiner votre toute l'architecture parce que vous n'avez pas cherché assez longtemps et durement pour une meilleure solution. Donc, au lieu de chercher quelqu'un ici sur PSE qui vous donne une bénédiction à ce sujet, vous feriez mieux de poser une question différente où vous décrivez en détail certains des problèmes que vous avez en profondeur, et regardez si quelqu'un vous propose une idée qui évite le dieu approche par classe.
Peut-être que la classe de balle peut être conçue pour être la façade d'un tas d'autres classes, de sorte que la classe de balle devient plus petite. Peut-être que le modèle de stratégie peut aider afin que la puce puisse déléguer différents comportements à différents objets de stratégie. Peut-être avez-vous juste besoin d'un adaptateur entre votre composant bullet et votre générateur de procédures. Mais honnêtement, sans connaître plus de détails sur votre système, on ne peut que deviner.
Question interessante. Je suis un peu biaisé cependant à cause de mes expériences précédentes, ce qui m'incite à répondre par non.
Réponse courte: Nous n'arrêtons jamais d'apprendre. Lorsque vous frappez un mur comme ça, c'est une chance d'améliorer vos compétences en architecture/conception, pas une excuse pour ajouter des odeurs de code.
La version plus longue est que l'on m'a souvent posé des questions similaires dans mes projets au travail, mais inévitablement, nous avons fini par discuter de la question de manière beaucoup plus approfondie, puis l'interrogateur d'origine l'a reconnu. Vous voulez inclure des mentalités comme l'analyse des causes profondes avec cela.
J'ai trouvé le 5 Whys particulièrement utile. Votre explication ici se lit comme le premier pourquoi:
Qu'est-ce qui est exactement simplifié? Et pourquoi en est-il ainsi? Continuez dans cette voie et ce qui se produit généralement, c'est que vous commencez à reconnaître des problèmes beaucoup plus fondamentaux, mais en même temps, ils vous permettent également des solutions plus fondamentales.
Pour faire court, après avoir réfléchi plus profondément au problème en question et en particulier à ses causes, d'après mon expérience, la question initiale a fini par être nulle et totalement inintéressante pour tous les participants. Si vous avez des doutes, défiez-les et soit ils disparaîtront, soit vous saurez ce que vous devez faire. Attention, c'est un bon signe à mon avis d'avoir ces doutes sur le code/design. D'autres appellent cela un sentiment d'intestin, mais pour contester ces problèmes, vous devez d'abord les reconnaître. Depuis que vous avez fait cette première étape, félicitations! Maintenant, descendez le terrier du lapin ...
Avoir une classe divine comme celle-ci n'est jamais souhaitable, car cela ne signifie pas seulement que vos balles sont maintenant des objets monolithiques, mais il en va de même pour votre algorithme de génération procédurale.
La première étape aurait été d'analyser, pourquoi exactement votre IA a-t-elle eu tant de mal à gérer la complexité de votre modèle?
Avez-vous, par hasard, essayé de transformer votre IA en un objet de classe divine, pleinement conscient de la sémantique de chaque propriété possible? Si c'est le cas, c'est à l'origine du problème.
La solution n'aurait alors pas été d'intégrer toutes les stratégies dans la classe à puces elle-même, mais de décharger au lieu de cela l'indication pour l'IA du noyau de l'IA aux implémentations de stratégie elles-mêmes.
Cela vous aurait donné la flexibilité que vous vouliez à l'origine, y compris la possibilité d'étendre le système par de nouvelles stratégies comme vous le souhaitez sans craindre de rencontrer des effets secondaires avec des comportements hérités.
Au lieu de cela, vous avez maintenant tous les problèmes associés aux objets divins: Non seulement votre classe divine elle-même est un monolithe difficile à comprendre, difficile à déboguer, mais il en va de même pour tous les autres composants accédant à une telle classe divine. En raison du manque d'abstraction, votre IA est maintenant appelée à se transformer en une abomination de complexité similaire car elle doit être consciente de toutes les propriétés individuelles redondantes maintenant.
Même en ce moment, vous rencontrez déjà des problèmes de maintenance. Ces problèmes s'aggravent, surtout dès que vous perdez les membres de l'équipe qui ont encore un modèle cognitif de la façon dont cette classe est censée fonctionner.
Jusqu'à présent, tous les projets que j'ai rencontrés qui utilisaient de telles classes de dieu ou fonctions de dieu ont été complètement réécrits à partir de zéro ou ont cessé, sans exception.
Tout mauvais code depuis la nuit des temps a une histoire derrière son évolution qui lui donne un aspect raisonnable étape par étape. Le vôtre ne fait pas exception. Les codeurs apprennent en codant. Il y a des aspects de votre problème que vous n'auriez pas pu prévoir qui semblent évidents maintenant. Il y a des décisions que vous avez prises qui étaient tout à fait raisonnables progressivement, mais qui ont conduit votre architecture dans la mauvaise direction dans l'ensemble. Vous devez identifier et réexaminer ces décisions.
Demandez à votre bureau si les gens ont des idées pour le réparer. Dans la plupart des endroits où j'ai travaillé, environ 10 à 20% des programmeurs ont un vrai talent pour ce genre de choses et attendent juste la chance. Découvrez qui sont ces personnes. Souvent, ce sont vos nouvelles recrues qui ne sont pas historiquement investies dans l'architecture actuelle qui peuvent le plus facilement voir des alternatives. Associez-les à l'un de vos anciens combattants et vous serez peut-être surpris de ce qu'ils trouveront ensemble.
Dans certains cas, cela est tout à fait acceptable. Cependant, j'ai du mal à croire qu'il n'y a pas de bonne solution en utilisant à la fois la génération procédurale et votre architecture comportementale Nice attachée/basée sur les composants. Si tous les comportements venaient d'être introduits dans la classe de balle, il n'y a pas de différence fonctionnelle entre l'objet divin et la version architecturée soignée. Qu'est-ce qui a rendu l'utilisation de vos algorithmes de génération procédurale si difficile?
Je pense à une nouvelle question (peut-être ici, ou sur gamedev.stackexchange.com?), Où vous décrivez les problèmes que vous avez rencontrés avec votre architecture en combinaison avec proc. gen., serait vraiment intéressant. Faites-le nous savoir ici si vous posez une nouvelle question!
Pour vous inspirer, vous voudrez peut-être jeter un œil à la programmation fonctionnelle ou, plus précisément, aux types sur mesure. L'idée étant que les choses ne fonctionnent pas est impossible à représenter dans le système de type dont vous disposez. Par exemple, disons que vous avez un pistolet qui a les composants "tirer des balles" et "tenir des balles". S'il s'agit de deux composants distincts, il y a des configurations qui n'ont aucun sens - vous pouvez avoir un pistolet qui tire des balles mais n'en a pas dans son stockage, ou un pistolet qui stocke des balles mais ne les tire pas.
À ce niveau de complexité, vous pensez "à droite, un humain comprendra que cela est inutile et évitera les combinaisons impossibles". Une meilleure façon pourrait être de rendre cela impossible. Par exemple, le composant pour "tirer des balles" pourrait exiger une référence au composant "tenir des balles" (ou simplement le maintenir comme une partie de lui-même, bien que cela ait ses propres problèmes).
Bien que vos exemples puissent être beaucoup plus compliqués, la clé limite toujours les combinaisons possibles, en ajoutant des contraintes. D'une certaine manière, si c'est trop compliqué pour que la génération procédurale se passe bien, c'est probablement trop compliqué de toute façon. Pensez à toutes les façons dont vous vous contraignez lors de la conception des armes - vous dites-vous "à droite, ce pistolet a déjà un lance-flammes, il est inutile d'ajouter un lance-grenades"? C'est quelque chose que vous pouvez intégrer à votre heuristique. Vous voudrez peut-être utiliser un mécanisme de probabilité un peu plus compliqué que de simplement donner une chance fixe d'avoir chaque composant - vous pourriez avoir des composants qui ont tendance à s'exclure les uns des autres, ou des composants qui ont tendance à bien fonctionner ensemble. Vous pouvez donc modifier les probabilités de sélection d'un nouveau composant en fonction des composants que vous possédez déjà.
Enfin, demandez-vous s'il s'agit en fait d'un problème suffisamment important. Est-ce que cela gâche le plaisir du jeu? Les pistolets résultants sont-ils inutiles ou trop puissants? Combien? Un sur 10 est-il insensé? Des jeux comme Borderlands (avec des effets d'armes générés de manière procédurale) ignorent souvent les combinaisons d'effets absurdes - quel est l'intérêt d'avoir un fusil de chasse avec une portée 16x? Et pourtant, cela se produit très souvent à Borderlands. C'est juste joué pour rire, plutôt que d'être considéré comme un échec des mécanismes de génération :)