web-dev-qa-db-fra.com

Un système à composants d'entité n'est-il pas terrible pour le découplage / la dissimulation d'informations?

Le titre est intentionnellement hyperbolique et c'est peut-être juste mon inexpérience avec le motif mais voici mon raisonnement:

La manière "habituelle" ou sans doute simple d'implémenter des entités est de les implémenter en tant qu'objets et de sous-classer les comportements communs. Cela conduit au problème classique de "est un EvilTree une sous-classe de Tree ou Enemy?". Si nous autorisons l'héritage multiple, le problème du diamant se pose. Nous pourrions plutôt tirer la fonctionnalité combinée de Tree et Enemy plus haut dans la hiérarchie qui mène aux classes de Dieu, ou nous pouvons intentionnellement laisser de côté le comportement dans nos Tree et Entity (ce qui en fait des interfaces dans le cas extrême) afin que le EvilTree puisse l'implémenter lui-même - ce qui conduit à la duplication de code si nous avons un SomewhatEvilTree.

Les systèmes à composants d'entité tentent de résoudre ce problème en divisant l'objet Tree et Enemy en différents composants - disons Position, Health et AI - et implémentez des systèmes, tels qu'un AISystem qui modifie la position d'une entité en fonction des décisions de l'IA. Jusqu'ici tout va bien mais que faire si EvilTree peut ramasser une mise sous tension et infliger des dégâts? Nous avons d'abord besoin d'un CollisionSystem et d'un DamageSystem (nous en avons probablement déjà). Le CollisionSystem doit communiquer avec le DamageSystem: Chaque fois que deux choses entrent en collision, le CollisionSystem envoie un message au DamageSystem pour qu'il puisse soustraire la santé. Les dégâts sont également influencés par les bonus, nous devons donc les stocker quelque part. Créons-nous un nouveau PowerupComponent que nous attachons aux entités? Mais alors, le DamageSystem a besoin de savoir quelque chose dont il préfère ne rien savoir - après tout, il y a aussi des choses qui infligent des dégâts qui ne peuvent pas capter les bonus (par exemple un Spike). Autorisons-nous le PowerupSystem à modifier un StatComponent qui est également utilisé pour des calculs de dommages similaires à cette réponse ? Mais maintenant, deux systèmes accèdent aux mêmes données. À mesure que notre jeu devient plus complexe, il deviendrait un graphe de dépendance intangible où les composants sont partagés entre de nombreux systèmes. À ce stade, nous pouvons simplement utiliser des variables statiques globales et nous débarrasser de tout le passe-partout.

Existe-t-il un moyen efficace de résoudre ce problème? Une idée que j'avais était de laisser les composants avoir certaines fonctions, par exemple donnez la fonction StatComponentattack() qui retourne juste un entier par défaut mais qui peut être composée lors d'une mise sous tension:

attack = getAttack compose powerupBy(20) compose powerdownBy(40)

Cela ne résout pas le problème selon lequel attack doit être enregistré dans un composant accessible par plusieurs systèmes mais au moins je pourrais taper correctement les fonctions si j'ai un langage qui le supporte suffisamment:

// In StatComponent
type Strength = PrePowerup | PostPowerup
type Damage = Int
type PrePowerup = Int
type PostPowerup = Int
attack: Strength = getAttack //default value, can be changed by systems
getAttack: PrePowerup

// these functions can be defined in other components or in PowerupSystems
powerupBy: Strength -> PostPowerup
powerdownBy: Strength -> PostPowerup
subtractArmor: Strength -> Damage

// in DamageSystem
dealDamage: Damage -> () = attack compose subtractArmor compose hurtSomeEntity

De cette façon, je garantis au moins un ordre correct des différentes fonctions ajoutées par les systèmes. Quoi qu'il en soit, il semble que j'approche rapidement de la programmation réactive fonctionnelle ici, donc je me demande si je n'aurais pas dû utiliser cela depuis le début (je viens juste de regarder FRP, donc je peux me tromper ici). Je vois que ECS est une amélioration par rapport aux hiérarchies de classes complexes, mais je ne suis pas convaincu que ce soit idéal.

Y a-t-il une solution à cela? Y a-t-il une fonctionnalité/un modèle qui me manque pour découpler ECS plus proprement? Le FRP est-il strictement mieux adapté à ce problème? Ces problèmes découlent-ils simplement de la complexité inhérente à ce que j'essaie de programmer; c'est-à-dire que FRP aurait des problèmes similaires?

11
PawkyPenguin

ECS ruine complètement la dissimulation des données. Il s'agit d'un compromis du modèle.

ECS est excellent au découplage. Un bon ECS permet à un système de déplacement de déclarer qu'il fonctionne sur n'importe quelle entité qui a une vitesse et un composant de position, sans avoir à se soucier des types d'entités existants ou des autres systèmes qui accèdent à ces composants. Ceci est au moins équivalent en découplant la puissance à avoir des objets de jeu implémentant certaines interfaces.

Deux systèmes accédant aux mêmes composants est une fonctionnalité, pas un problème. Il est tout à fait attendu et ne couple en aucun cas les systèmes. Il est vrai que les systèmes auront un graphe de dépendances implicites, mais ces dépendances sont inhérentes au monde modélisé. Dire que le système de dommages ne devrait pas avoir la dépendance implicite du système de mise sous tension revient à affirmer que les mises sous tension n'affectent pas les dommages, et c'est probablement faux. Cependant, bien que la dépendance existe, les systèmes ne sont pas couplés - vous pouvez supprimer le système de mise sous tension du jeu sans affecter le système de dégâts, car la communication s'est produite via le composant stat et était complètement implicite.

La résolution de ces dépendances et des systèmes de commande peut être effectuée dans un seul emplacement central, de la même manière que la résolution des dépendances dans un système DI. Oui, un jeu complexe aura un graphe complexe de systèmes, mais cette complexité est inhérente, et au moins elle est contenue.

21
Sebastian Redl

Il n'y a presque aucun moyen de contourner le fait qu'un système doit accéder à plusieurs composants. Pour que quelque chose comme un VelocitySystem fonctionne, il aurait probablement besoin d'accéder à un VelocityComponent et un PositionComponent. Pendant ce temps, le RenderingSystem doit également accéder à ces données. Peu importe ce que vous faites, à un moment donné, le système de rendu doit savoir où rendre l'objet et VelocitySystem doit savoir où déplacer l'objet.

Ce dont vous avez besoin pour cela est le explicitness des dépendances. Chaque système doit être explicite sur les données qu'il va lire et sur quelles données il va écrire. Lorsqu'un système veut récupérer un composant particulier, il doit pouvoir le faire explicitement uniquement. Dans sa forme la plus simple, il a simplement les composants pour chaque type dont il a besoin (par exemple, le RenderSystem a besoin des RenderComponents et PositionComponents) comme arguments et renvoie tout ce qu'il a changé (par exemple, les RenderComponents uniquement).

De cette façon, je garantis au moins un ordre correct des différentes fonctions ajoutées par les systèmes

Vous pouvez avoir la commande dans une telle conception. Rien ne dit que pour ECS, vos systèmes doivent être indépendants de l'ordre ou de toute autre chose.

Le FRP est-il strictement mieux adapté à ce problème? Ces problèmes découlent-ils simplement de la complexité inhérente à ce que j'essaie de programmer; c'est-à-dire que FRP aurait des problèmes similaires?

L'utilisation de cette conception de système de composants d'entité et de FRP n'est pas mutuellement exclusive. En fait, les systèmes peuvent être vus comme rien d'autre comme n'ayant aucun état, effectuant simplement des transformations de données (les composants).

FRP ne résoudrait pas le problème d'avoir à utiliser les informations dont vous avez besoin pour effectuer certaines opérations.

7
Athos vk