Pourquoi est-ce une mauvaise idée de stocker des méthodes dans des entités et des composants? (Avec d'autres questions sur le système d'entité.)

16

Il s'agit d'une suite à cette question, à laquelle j'ai répondu, mais celle-ci aborde un sujet beaucoup plus spécifique.

Cette réponse m'a aidé à mieux comprendre Entity Systems que l'article.

J'ai lu l'article (oui, le) sur Entity Systems, et il m'a dit ce qui suit:

Les entités ne sont qu'un identifiant et un tableau de composants (les articles indiquent que le stockage d'entités dans des composants n'est pas une bonne façon de faire les choses, mais ne fournit pas d'alternative).
Les composants sont des éléments de données qui indiquent ce qui peut être fait avec une certaine entité.
Les systèmes sont les "méthodes", ils effectuent des manipulations de données sur des entités.

Cela semble vraiment pratique dans de nombreuses situations, mais la partie sur les composants qui ne sont que des classes de données me dérange. Par exemple, comment pourrais-je implémenter ma classe Vector2D (Position) dans un système d'entité?

La classe Vector2D contient des données: les coordonnées x et y, mais elle a également des méthodes , qui sont cruciales pour son utilité et distinguent la classe d'un simple tableau à deux éléments. Méthodes sont par exemple: add(), rotate(point, r, angle), substract(), normalize()et toutes les autres normes, méthodes utiles et absolument nécessaires que les positions (qui sont des instances de la classe Vector2D) devrait avoir.

Si le composant n'était qu'un détenteur de données, il ne pourrait pas avoir ces méthodes!

Une solution qui pourrait probablement apparaître serait de les implémenter dans des systèmes, mais cela semble très contre-intuitif. Ces méthodes sont des choses que je veux effectuer maintenant , qu'elles soient complètes et prêtes à l'emploi. Je ne veux pas attendre la MovementSystemlecture d'un ensemble coûteux de messages qui lui demandent d'effectuer un calcul sur la position d'une entité!

Et, l'article indique très clairement que seuls les systèmes devraient avoir une fonctionnalité, et la seule explication à cela, que j'ai pu trouver, était "d'éviter la POO". Tout d'abord, je ne comprends pas pourquoi devrais-je m'abstenir d'utiliser des méthodes dans les entités et les composants. La surcharge de mémoire est pratiquement la même, et lorsqu'ils sont couplés à des systèmes, ils devraient être très faciles à mettre en œuvre et à combiner de manière intéressante. Les systèmes, par exemple, ne pouvaient fournir une logique de base qu'aux entités / composants qui connaissent eux-mêmes la mise en œuvre. Si vous me demandez - c'est essentiellement en prenant les goodies à la fois de ES et OOP, quelque chose qui ne peut pas être fait selon l'auteur de l'article, mais cela me semble être une bonne pratique.

Pensez-y de cette façon; il existe de nombreux types d'objets à dessiner dans un jeu. Images pures et simples, des animations ( update(), getCurrentFrame(), etc.), des combinaisons de ces types primitifs, et tous pourraient simplement fournir une draw()méthode pour le système de rendu, qui ne puis pas besoin de se soucier de la façon dont l'image - objet d'une entité est mis en œuvre, seulement sur l'interface (dessiner) et la position. Et puis, je n'aurais besoin que d'un système d'animation qui appellerait des méthodes spécifiques à l'animation qui n'ont rien à voir avec le rendu.

Et juste une autre chose ... Existe-t-il vraiment une alternative aux tableaux quand il s'agit de stocker des composants? Je ne vois aucun autre endroit pour les composants à stocker autre que des tableaux à l'intérieur d'une classe d'entité ...

C'est peut-être une meilleure approche: stocker les composants comme de simples propriétés d'entités. Par exemple, un composant de position serait collé entity.position.

La seule autre façon serait d'avoir une sorte de table de recherche étrange à l' intérieur des systèmes, qui fait référence à différentes entités. Mais cela semble très inefficace et plus compliqué à développer que de simplement stocker des composants dans l'entité.

jcora
la source
Alexandre tu fais juste beaucoup de retouches pour obtenir un autre badge? Parce que c'est vilain vilain, il continue de cogner une tonne de fils anciens.
jhocking du

Réponses:

25

Je pense qu'il est tout à fait correct d'avoir des méthodes simples pour accéder, mettre à jour ou manipuler les données dans les composants. Je pense que la fonctionnalité qui devrait rester en dehors des composants est une fonctionnalité logique. Les fonctions utilitaires sont très bien. N'oubliez pas que le système entité-composant n'est qu'un guide, et non des règles strictes que vous devez suivre. Ne sortez pas de votre chemin pour les suivre. Si vous pensez qu'il est plus logique de le faire dans un sens, faites-le de cette façon :)

ÉDITER

Pour clarifier, votre objectif n'est pas d'éviter la POO . Ce serait assez difficile dans la plupart des langues courantes utilisées de nos jours. Vous essayez de minimiser l' héritage , qui est un aspect important de la POO, mais pas obligatoire. Vous voulez vous débarrasser de l'héritage Object-> MobileObject-> Creature-> Bipedal-> Human type.

Cependant, c'est OK d'avoir un héritage! Vous avez affaire à une langue fortement influencée par l'héritage, il est très difficile de ne pas l'utiliser. Par exemple, vous pouvez avoir une Componentclasse ou une interface que tous vos autres composants étendent ou implémentent. Même chose avec votre Systemclasse. Cela rend les choses beaucoup plus faciles. Je vous recommande fortement de jeter un œil au framework Artemis . C'est open source et il a quelques exemples de projets. Ouvrez ces choses et voyez comment cela fonctionne.

Pour Artemis, les entités sont stockées dans un tableau, simple. Cependant, leurs composants sont stockés dans un ou plusieurs tableaux (séparés des entités). Le tableau de niveau supérieur regroupe le tableau de niveau inférieur par type de composant. Ainsi, chaque type de composant a son propre tableau. Le tableau de niveau inférieur est indexé par ID d'entité. (Maintenant, je ne sais pas si je le ferais de cette façon, mais c'est comme ça que ça se passe ici). Artemis réutilise les ID d'entité, de sorte que l'ID d'entité max ne dépasse pas votre nombre actuel d'entités, mais vous pouvez toujours avoir des tableaux clairsemés si le composant n'est pas un composant fréquemment utilisé. De toute façon, je ne vais pas trop séparer cela. Cette méthode de stockage des entités et de leurs composants semble fonctionner. Je pense que ce serait une excellente première étape pour mettre en œuvre votre propre système.

Les entités et les composants sont stockés dans un gestionnaire distinct.

La stratégie que vous mentionnez, faire en sorte que les entités stockent leurs propres composants ( entity.position), est en quelque sorte contraire au thème des composants d'entité, mais elle est totalement acceptable si vous pensez que cela a le plus de sens.

MichaelHouse
la source
1
Hmm, cela simplifie grandement la situation, merci! Je pensais qu'il y avait quelque chose de magique "tu vas le regretter plus tard", et je ne pouvais pas le voir!
jcora
1
Na, je les utilise totalement dans mon système de composants d'entité. J'ai même quelques composants qui héritent d'un parent commun, le souffle coupé . Je pense que le seul regret que vous feriez serait que vous essayiez de contourner le fait de ne pas utiliser de telles méthodes. Il s'agit de faire ce qui a le plus de sens pour vous. S'il est logique d'utiliser l'héritage ou de mettre certaines méthodes dans un composant, allez-y.
MichaelHouse
2
J'ai appris de ma dernière réponse à ce sujet. Avertissement: je ne dis pas que c'est la façon de le faire. :)
MichaelHouse
1
Oui, je sais à quel point il peut être intimidant d'apprendre un nouveau paradigme. Heureusement, vous pouvez utiliser des aspects de l'ancien paradigme pour faciliter les choses! J'ai mis à jour ma réponse avec des informations de stockage. Si vous regardez Artemis, consultez le EntityManagercomme c'est où les choses sont stockées.
MichaelHouse
1
Agréable! Ça va être un moteur assez doux quand c'est fait. Bonne chance! Merci d'avoir posé des questions intéressantes.
MichaelHouse
10

«Cet» article n'est pas celui avec lequel je suis particulièrement d'accord, donc ma réponse sera quelque peu critique, je pense.

Cela semble vraiment pratique dans de nombreuses situations, mais la partie sur les composants qui ne sont que des classes de données me dérange. Par exemple, comment pourrais-je implémenter ma classe Vector2D (Position) dans un système d'entité?

L'idée n'est pas de s'assurer que rien dans votre programme n'est autre qu'un ID d'entité, un composant ou un système - c'est de s'assurer que les données et le comportement de l'entité sont créés en composant des objets plutôt qu'en utilisant un arbre d'héritage complexe ou pire en essayant de mettre toutes les fonctionnalités possibles dans un seul objet. Pour implémenter ces composants et systèmes, vous aurez certainement des données normales comme des vecteurs qui sont, dans la plupart des langues, mieux représentées en tant que classe.

Ignorez la partie de l'article qui suggère que ce n'est pas la POO - c'est tout aussi la POO que toute autre approche. Lorsque la plupart des compilateurs ou des exécutions de langage implémentent des méthodes objet, c'est essentiellement comme n'importe quelle autre fonction, sauf qu'il existe un argument caché appelé thisor self, qui est un pointeur vers un endroit en mémoire où les données de cet objet sont stockées. Dans un système basé sur des composants, l'ID d'entité peut être utilisé pour trouver où se trouvent les composants pertinents (et donc les données) pour une entité donnée. Ainsi, l'ID d'entité équivaut à un pointeur this / self, et les concepts sont fondamentalement la même chose, juste un peu réarrangés.

Et, l'article indique très clairement que seuls les systèmes devraient avoir une fonctionnalité, et la seule explication à cela, que j'ai pu trouver, était "d'éviter la POO". Tout d'abord, je ne comprends pas pourquoi devrais-je m'abstenir d'utiliser des méthodes dans les entités et les composants.

Bien. Les méthodes sont un moyen efficace d'organiser votre code. La chose importante à retirer de l'idée "éviter la POO" est d'éviter d'utiliser l'héritage partout pour étendre les fonctionnalités. Au lieu de cela, divisez la fonctionnalité en composants qui peuvent être combinés pour faire la même chose.

Pensez-y de cette façon; il existe de nombreux types d'objets à dessiner dans un jeu. De vieilles images simples, des animations (update (), getCurrentFrame (), etc.), des combinaisons de ces types primitifs, et toutes pourraient simplement fournir une méthode draw () au système de rendu [...]

L'idée d'un système basé sur les composants est que vous n'auriez pas de classes séparées pour celles-ci, mais que vous auriez une seule classe Objet / Entité, et l'image serait un Objet / Entité qui a un ImageRenderer, les Animations seraient un Objet / Entité qui a un AnimationRenderer, etc. Les systèmes concernés sauraient comment rendre ces composants et il n'y aurait donc pas besoin d'avoir de classe de base avec une méthode Draw ().

[...] qui n'a alors pas besoin de se soucier de la façon dont le sprite d'une entité est implémenté, seulement de l'interface (draw) et de la position. Et puis, je n'aurais besoin que d'un système d'animation qui appellerait des méthodes spécifiques à l'animation qui n'ont rien à voir avec le rendu.

Bien sûr, mais cela ne fonctionne pas bien avec les composants. Vous avez 3 choix:

  • Chaque composant implémente cette interface et possède une méthode Draw (), même si rien n'est dessiné. Si vous faisiez cela pour chaque fonctionnalité, les composants auraient l'air plutôt moche.
  • Seuls les composants qui ont quelque chose à dessiner implémentent l'interface - mais qui décide sur quels composants appeler Draw ()? Un système doit-il en quelque sorte interroger chaque composant pour voir quelle interface est prise en charge? Ce serait source d'erreurs et potentiellement délicat à implémenter dans certaines langues.
  • Les composants sont uniquement gérés par leur propre système (ce qui est l'idée dans l'article lié). Dans ce cas, l'interface n'est pas pertinente car un système sait exactement avec quelle classe ou type d'objet il travaille.

Et juste une autre chose ... Existe-t-il vraiment une alternative aux tableaux quand il s'agit de stocker des composants? Je ne vois aucun autre endroit pour les composants à stocker autre que des tableaux à l'intérieur d'une classe d'entité ...

Vous pouvez stocker les composants dans le système. Le tableau n'est pas le problème, mais l'emplacement de stockage du composant.

Kylotan
la source
+1 Merci pour un autre point de vue. C'est bien d'en avoir quelques-uns lorsque l'on traite un sujet aussi ambigu! Si vous stockez des composants dans le système, cela signifie-t-il que les composants ne peuvent être modifiés que par un seul système? Par exemple, le système de dessin et le système de mouvement accèdent tous deux au composant de position. Où le stockez-vous?
MichaelHouse
Eh bien, ils ne stockent qu'un pointeur sur ces composants, ce qui peut, tant que je suis concerné, quelque part ... Aussi, pourquoi stockeriez-vous des composants dans des systèmes? Y a-t-il un avantage à cela?
jcora
Ai-je raison, @Kylotan? C'est comme ça que je le ferais, cela semble logique ...
jcora
Dans l'exemple d'Adam / T-Machine, l'intention est qu'il y ait 1 système par composant, mais un système pourrait certainement accéder et modifier d'autres composants. (Cela entrave les avantages du multithreading des composants, mais c'est une autre affaire.)
Kylotan
1
Le stockage des composants dans le système permet une meilleure localité de référence pour ce système - ce système ne fonctionne (généralement) qu'avec ces données, alors pourquoi parcourir toute la mémoire de votre ordinateur d'entité en entité pour les obtenir? Cela aide également à la concurrence, car vous pouvez placer un système entier et ses données sur un cœur ou un processeur (ou même un ordinateur séparé, dans les MMO). Encore une fois, ces avantages diminuent lorsqu'un système accède à plus d'un type de composant, ce qui doit être pris en compte lors du choix de la répartition des responsabilités composant / système.
Kylotan
2

Un vecteur est une donnée. Les fonctions sont plus comme des fonctions utilitaires - elles ne sont pas spécifiques à cette instance des données, elles peuvent être appliquées à tous les vecteurs indépendamment. Une bonne façon de penser est la suivante: ces fonctions peuvent-elles être réécrites en tant que méthodes statiques? Si c'est le cas, c'est juste une utilité.

Matt Kemp
la source
Je le sais, mais le problème est que les méthodes d'appel sont plus rapides et peuvent être effectuées sur place par un système, ou toute autre chose qui pourrait avoir besoin de manipuler la position d'une entité. J'ai expliqué que, vérifiez-le aussi, la question a bien plus que cela, je crois.
jcora