Composants du jeu, gestionnaires de jeu et propriétés des objets

15

J'essaie de me familiariser avec la conception d'entités basée sur les composants.

Ma première étape a été de créer divers composants pouvant être ajoutés à un objet. Pour chaque type de composant, j'avais un gestionnaire, qui appelait la fonction de mise à jour de chaque composant, transmettant des choses comme l'état du clavier, etc., au besoin.

La prochaine chose que j'ai faite a été de supprimer l'objet et de simplement avoir chaque composant avec un ID. Un objet est donc défini par des composants ayant les mêmes identifiants.

Maintenant, je pense que je n'ai pas besoin d'un gestionnaire pour tous mes composants, par exemple j'ai un SizeComponent, qui a juste une Sizepropriété). Par conséquent, le SizeComponentn'a pas de méthode de mise à jour et la méthode de mise à jour du gestionnaire ne fait rien.

Ma première pensée a été d'avoir une ObjectPropertyclasse sur laquelle les composants pourraient interroger, au lieu de les avoir comme propriétés de composants. Un objet aurait donc un certain nombre de ObjectPropertyet ObjectComponent. Les composants auraient une logique de mise à jour qui interroge l'objet pour les propriétés. Le gestionnaire gérerait l'appel de la méthode de mise à jour du composant.

Cela me semble être une ingénierie excessive, mais je ne pense pas pouvoir me débarrasser des composants, car j'ai besoin d'un moyen pour les gestionnaires de savoir quels objets ont besoin de la logique du composant à exécuter (sinon je supprimerais simplement le composant complètement et pousser sa logique de mise à jour dans le gestionnaire).

  1. Est - ce (ayant ObjectProperty, ObjectComponentet ComponentManagercours) sur l' ingénierie?
  2. Quelle serait une bonne alternative?
George Duckett
la source
1
Vous avez la bonne idée en essayant d'apprendre le modèle de composant, mais vous devez mieux comprendre ce qu'il doit faire - et la seule façon de le faire est de terminer [principalement] un jeu sans l'utiliser. Je pense que faire un SizeComponentest exagéré - vous pouvez supposer que la plupart des objets ont une taille - ce sont des choses comme le rendu, l'IA et la physique où le modèle de composant est utilisé; La taille se comportera toujours de la même manière, vous pouvez donc partager ce code.
Jonathan Dickinson
@JonathanDickinson, @Den: Je pense que mon problème est alors de savoir où stocker les propriétés communes. Par exemple, un objet en tant que position, qui est utilisé par a RenderingComponentet a PhysicsComponent. Suis-je en train de réfléchir à la décision de mettre la propriété? Dois-je simplement le coller dans l'un ou l'autre, puis demander à l'autre de rechercher un objet pour le composant qui a la propriété requise?
George Duckett
Mon commentaire précédent, et le processus de réflexion derrière cela, c'est ce qui me pousse à avoir une classe distincte pour une propriété (ou un groupe de propriétés connexes peut-être) que les composants peuvent interroger.
George Duckett
1
J'aime vraiment cette idée - cela pourrait valoir la peine d'essayer; mais avoir un objet pour décrire chaque propriété individuelle coûte vraiment cher. Vous pouvez essayer PhysicalStateInstance(un par objet) à côté d'un GravityPhysicsShared(un par jeu); Cependant, je suis tenté de dire que cela s'aventure dans le domaine de l'euphorie des architectes, ne vous architectez pas dans un trou (exactement ce que j'ai fait avec mon premier système de composants). BAISER.
Jonathan Dickinson

Réponses:

6

La réponse simple à votre première question est Oui, vous êtes en train de trop concevoir la conception. Le "Jusqu'où dois-je ventiler les choses?" est très courante lorsque l'étape suivante est franchie et que l'objet central (généralement appelé entité) est supprimé.

Lorsque vous décomposez les objets à un niveau si détaillé qu'ils ont leur propre taille, le design est allé trop loin. Une valeur de données en soi n'est pas un composant. Il s'agit d'un type de données intégré et peut souvent être appelé exactement comme vous avez commencé à les appeler, une propriété. Une propriété n'est pas un composant, mais un composant contient des propriétés.

Voici donc quelques lignes directrices que j'essaie de suivre lors du développement dans un système de composants:

  • Il n'y a pas de cuillère.
    • C'est l'étape que vous avez déjà franchie pour vous débarrasser de l'objet central. Cela supprime tout le débat sur ce qui entre dans l'objet Entity et ce qui entre dans un composant, car maintenant tout ce que vous avez, ce sont les composants.
  • Les composants ne sont pas des structures
    • Si vous décomposez quelque chose là où il ne contient que des données, ce n'est plus un composant, c'est juste une structure de données.
    • Un composant doit contenir toutes les fonctionnalités nécessaires pour accomplir une tâche très spécifique d'une manière spécifique.
    • L'interface IRenderable fournit la solution générique pour afficher visuellement n'importe quoi dans le jeu. CRenderableSprite et CRenderableModel est une implémentation de composant de cette interface qui fournit respectivement les spécificités à rendre en 2D et 3D.
    • IUseable est l'interface pour quelque chose avec laquelle un joueur peut interagir. CUseableItem serait le composant qui tire le pistolet actif ou boit la potion sélectionnée tandis que CUseableTrigger pourrait être l'endroit où un joueur va sauter dans une tourelle ou lancer un levier pour faire tomber le pont-levis.

Donc, la directive selon laquelle les composants ne sont pas des structures, le SizeComponent a été trop détaillé. Il ne contient que des données et ce qui définit la taille de quelque chose peut varier. Par exemple, dans un composant de rendu, il peut s'agir d'un scalaire 1d ou d'un vecteur 2 / 3d. Dans un composant physique, il pourrait s'agir du volume englobant de l'objet. Dans un élément d'inventaire, il peut s'agir de l'espace qu'il occupe sur une grille 2D.

Essayez de tracer une bonne ligne entre la théorie et l'aspect pratique.

J'espère que cela t'aides.

James
la source
N'oublions pas que sur certaines plates-formes, appeler une fonction depuis une interface est plus long que d'appeler une fonction depuis une classe parente (puisque votre réponse incluait des mentions d'interfaces et de classes)
ADB
Bon point à retenir, mais j'essayais de rester indépendant de la langue et de les utiliser simplement en termes de conception générale.
James
"Si vous décomposez quelque chose là où il ne contient que des données, ce n'est plus un composant, c'est juste une structure de données." -- Pourquoi? "Composant" est un mot si générique qu'il peut aussi signifier structure de données.
Paul Manta
@PaulManta Oui, c'est un terme générique, mais le point entier de cette question et réponse est où tracer la ligne. Ma réponse, comme vous l'avez citée, n'est que ma suggestion d'une règle de base pour faire exactement cela. Comme toujours, je préconise de ne jamais laisser la théorie ou les considérations de conception être le moteur du développement, c'est censé l'aider.
James
1
@James Ce fut une discussion intéressante. :) chat.stackexchange.com/rooms/2175 Mon plus grand reproche si votre implémentation est que les composants en savent trop sur les autres composants qui m'intéressent. J'aimerais continuer la discussion à un moment ultérieur.
Paul Manta
14

Vous avez déjà accepté une réponse, mais voici mon coup de couteau à un CBS. J'ai trouvé qu'une Componentclasse générique avait certaines limites, alors j'ai opté pour une conception décrite par Radical Entertainment à GDC 2009, qui a suggéré de séparer les composants en Attributeset Behaviors. (" Théorie et pratique de l'architecture des composants des objets de jeu ", Marcin Chady)

J'explique mes décisions de conception dans un document de deux pages. Je vais juste poster le lien car il est trop long pour tout coller ici. Il ne couvre actuellement que les composants logiques (pas les composants de rendu et de physique également), mais il devrait vous donner une idée de ce que j'ai essayé de faire:

http://www.pdf-archive.com/2012/01/08/08/entity-component-system/preview/page/1

Voici un extrait du document:

Attributs et comportements en bref

Attributesgérer une catégorie de données et leur logique est de portée limitée. Par exemple, Healthpeut s'assurer que sa valeur actuelle n'est jamais supérieure à sa valeur maximale, et il peut même avertir d'autres composants lorsque la valeur actuelle tombe en dessous d'un certain niveau critique, mais elle ne contient pas de logique plus complexe. Attributesne dépendent d'aucun autre Attributesou Behaviors.

Behaviorscontrôler la façon dont l'entité réagit aux événements du jeu, prendre des décisions et modifier les valeurs de Attributesselon les besoins. Behaviorsdépendent de certains des Attributes, mais ils ne peuvent pas interagir directement les uns avec les autres - ils ne réagissent que sur la façon dont les Attributes’valeurs sont modifiées par l'autre Behaviorset aux événements auxquels ils sont envoyés.


Edit: Et voici un diagramme de relations qui montre comment les composants communiquent entre eux:

Diagramme de communication entre les attributs et les comportements

Un détail d'implémentation: le niveau entité EventManagern'est créé que s'il est utilisé. La Entityclasse stocke juste un pointeur sur un EventManagerqui n'est initialisé que si un composant le demande.


Edit: Sur une autre question, j'ai donné une réponse similaire à celle-ci. Vous pouvez le trouver ici pour une explication peut-être meilleure du système:
/gamedev//a/23759/6188

Paul Manta
la source
2

Cela dépend vraiment des propriétés dont vous avez besoin et de l'endroit où vous en avez besoin. La quantité de mémoire dont vous disposerez et la puissance de traitement / type que vous utiliserez. J'ai vu et j'essaye de faire ce qui suit:

  • Les propriétés utilisées par plusieurs composants mais modifiées par un seul sont stockées dans ce composant. La forme est un bon exemple dans un jeu où le système AI, le système physique ainsi que le système de rendu ont besoin d'accéder à la forme de base, c'est une propriété lourde et elle ne devrait rester que dans un seul endroit si possible.
  • Des propriétés telles que la position doivent parfois être dupliquées. Par exemple, si vous exécutez plusieurs systèmes en parallèle, vous voulez éviter de jeter un coup d'œil entre les systèmes et préférez synchroniser la position (copier à partir du composant maître ou synchroniser via des deltas ou avec une passe de collision si nécessaire).
  • Les propriétés provenant des contrôles ou des «intentions» de l'IA peuvent être stockées dans un système dédié car elles peuvent être appliquées aux autres systèmes sans être visibles de l'extérieur.
  • Les propriétés simples peuvent devenir compliquées. Parfois, votre position nécessitera un système dédié si vous avez besoin de partager beaucoup de données (position, orientation, delta de trame, mouvement courant delta total, mouvement delta pour la trame actuelle et pour la trame précédente, rotation ...). Dans ce cas, vous devrez aller avec le système et accéder aux dernières données du composant dédié et vous devrez peut-être les modifier via des accumulateurs (deltas).
  • Parfois, vos propriétés peuvent être stockées dans un tableau brut (double *) et vos composants auront simplement des pointeurs vers les tableaux contenant les différentes propriétés. L'exemple le plus évident est celui où vous avez besoin de calculs parallèles massifs (CUDA, OpenCL). Donc, avoir un seul système pour gérer correctement les pointeurs pourrait être utile.

Ces principes ont leurs limites. Bien sûr, vous devrez pousser la géométrie vers le moteur de rendu, mais vous ne voudrez probablement pas la récupérer à partir de là. La géométrie principale sera stockée dans le moteur physique dans le cas où des déformations s'y produisent et synchronisée avec le rendu (de temps en temps en fonction de la distance des objets). Donc, d'une certaine manière, vous le dupliquerez de toute façon.

Il n'y a pas de systèmes parfaits. Et certains jeux seront mieux lotis avec un système plus simple tandis que d'autres nécessiteront des synchronisations plus complexes entre les systèmes.

Dans un premier temps, assurez-vous que toutes les propriétés sont accessibles de manière simple à partir de vos composants afin que vous puissiez changer la façon dont vous stockez les propriétés de manière transparente une fois que vous avez commencé à affiner vos systèmes.

Il n'y a aucune honte à copier certaines propriétés. Si quelques composants doivent contenir une copie locale, il est parfois plus efficace de copier et de synchroniser plutôt que d'accéder à une valeur "externe".

De plus, la synchronisation ne doit pas nécessairement se produire à chaque image. Certains composants peuvent être synchronisés moins fréquemment que d'autres. Les composants de rendu sont souvent un bon exemple. Ceux qui n'interagissent pas avec les joueurs peuvent être synchronisés moins fréquemment, tout comme ceux qui sont éloignés. Ceux qui se trouvent loin et en dehors du champ de la caméra peuvent être synchronisés encore moins fréquemment.


En ce qui concerne votre composant de taille, il pourrait probablement être intégré à votre composant de position:

  • toutes les entités ayant une taille n'ont pas de composant physique, des zones par exemple, donc le regrouper avec la physique n'est pas dans votre intérêt.
  • la taille n'aura probablement pas d'importance sans position
  • tous les objets avec une position auront probablement une taille (utilisable pour les scripts, la physique, l'IA, le rendu ...).
  • la taille n'est probablement pas mise à jour à chaque cycle mais la position peut l'être.

Au lieu de la taille, vous pouvez même utiliser un modificateur de taille, cela pourrait être plus pratique.

Quant au stockage de toutes les propriétés dans un système de stockage de propriété générique ... Je ne suis pas sûr que vous alliez dans la bonne direction ... Concentrez-vous sur les propriétés qui sont au cœur de votre jeu et créez des composants qui regroupent autant de propriétés connexes que possible. Tant que vous résumez correctement l'accès à ces propriétés (via des getters dans les composants qui en ont besoin par exemple), vous devriez pouvoir les déplacer, les copier et les synchroniser plus tard, sans casser trop de logiques.

Coyote
la source
2
BTW +1 ou -1 moi parce que mon représentant actuel est 666 depuis le 9 novembre ... C'est flippant.
Coyote
1

Si des composants peuvent être ajoutés arbitrairement à des entités, vous avez besoin d'un moyen de rechercher si un composant donné existe dans une entité et d'obtenir une référence à celui-ci. Vous pouvez donc parcourir une liste d'objets dérivés d'ObjectComponent jusqu'à ce que vous trouviez celui que vous voulez et le renvoyiez. Mais vous renverriez un objet du type correct.

En C ++ ou C #, cela signifie généralement que vous auriez une méthode de modèle sur l'entité comme T GetComponent<T>() . Et une fois que vous avez cette référence, vous savez exactement quelles données de membre elle contient, alors accédez-y directement.

Dans quelque chose comme Lua ou Python, vous n'avez pas nécessairement un type explicite de cet objet, et ne vous en souciez probablement pas non plus. Mais encore une fois, vous pouvez simplement accéder à la variable membre et gérer toute exception qui survient en tentant d'accéder à quelque chose qui n'est pas là.

L'interrogation des propriétés d'objet ressemble explicitement à la duplication du travail que la langue peut faire pour vous, soit au moment de la compilation pour les langues à typage statique, soit au moment de l'exécution pour les langues à typage dynamique.

Kylotan
la source
Je comprends comment obtenir des composants fortement typés d'une entité (en utilisant des génériques ou autres), ma question est plus sur où ces propriétés devraient aller, en particulier lorsqu'une propriété est utilisée par plusieurs composants et qu'aucun composant ne peut en être propriétaire. Voir mes 3ème et 4ème commentaires sur la question.
George Duckett
Choisissez simplement un existant s'il correspond, ou factorisez la propriété dans un nouveau composant si ce n'est pas le cas. Par exemple, Unity a un composant 'Transform' qui est juste la position, et si quelque chose d'autre doit changer la position de l'objet, ils le font via ce composant.
Kylotan
1

"Je pense, alors mon problème est où dois-je stocker les propriétés communes. Par exemple, un objet en tant que position, qui est utilisé par un composant de rendu et un composant de physique. Suis-je en train de trop réfléchir à la décision de mettre la propriété? Dois-je simplement dans l'un ou l'autre, puis demander à l'autre un objet pour le composant qui a la propriété nécessaire? "

Le fait est que RenderingComponent utilise la position, mais le PhysicsComponent la fournit . Vous avez juste besoin d'un moyen de dire à chaque composant utilisateur quel fournisseur utiliser. Idéalement de manière agnostique, sinon il y aura une dépendance.

"... ma question concerne davantage la destination de ces propriétés, en particulier lorsqu'une propriété est utilisée par plusieurs composants et qu'aucun composant ne peut en être propriétaire. Voir mes 3ème et 4ème commentaires sur la question."

Il n'y a pas de règle commune. Dépend d'une propriété spécifique (voir ci-dessus).

Créez un jeu avec une architecture laide mais basée sur des composants, puis refactorisez-le.

Tanière
la source
Je ne pense pas que je comprenne bien ce qu'il PhysicsComponentfaut faire alors. Je le vois comme gérer la simulation de l'objet dans un environnement physique, ce qui m'amène à cette confusion: toutes les choses qui doivent être rendues ne doivent pas être simulées, il me semble donc mal d'ajouter PhysicsComponentquand j'ajoute RenderingComponentcar il contient une position qui RenderingComponentutilise. Je pouvais facilement me voir me retrouver avec un réseau de composants interconnectés, ce qui signifie que tous / la plupart doivent être ajoutés à chaque entité.
George Duckett
J'ai eu une situation similaire en fait :). J'ai un PhysicsBodyComponent et un SimpleSpatialComponent. Les deux fournissent la position, l'angle et la taille. Mais le premier participe à la simulation physique et possède des propriétés pertinentes supplémentaires, et le second ne contient que ces données spatiales. Si vous avez votre propre moteur physique, vous pouvez même hériter le premier du second.
Den
"Je pouvais facilement me voir me retrouver avec un réseau de composants interconnectés, ce qui signifie que tous / la plupart doivent être ajoutés à chaque entité." C'est parce que vous n'avez pas de prototype de jeu réel. Nous parlons ici de quelques composants de base. Pas étonnant qu'ils soient utilisés partout.
Den
1

Votre intestin vous dit que d' avoir la ThingProperty, ThingComponentet ThingManagerpour chaque Thingtype d'un composant peu exagéré. Je pense que c'est vrai.

Mais, vous avez besoin d'un moyen de garder une trace des composants connexes en termes de quels systèmes les utilisent, à quelle entité ils appartiennent, etc.

TransformPropertyva être assez courant. Mais qui en est responsable, le système de rendu? Le système physique? Le système de son? Pourquoi un Transformcomposant aurait-il même besoin de se mettre à jour?

La solution consiste à supprimer tout type de code de vos propriétés en dehors des getters, setters et initializers. Les composants sont des données qui sont utilisées par les systèmes du jeu pour effectuer diverses tâches telles que le rendu, l'IA, la lecture du son, le mouvement, etc.

Lisez à propos d'Artemis: http://piemaster.net/2011/07/entity-component-artemis/

Regardez son code et vous verrez qu'il est basé sur des systèmes qui déclarent leurs dépendances comme des listes de ComponentTypes. Vous écrivez chacun de vosSystem classes et dans la méthode constructeur / init déclarez de quels types dépend le système.

Lors de la configuration des niveaux ou ainsi de suite, vous créez vos entités et leur ajoutez des composants. Ensuite, vous dites à cette entité de faire rapport à Artemis, et Artemis détermine ensuite en fonction de la composition de cette entité quels systèmes seraient intéressés à connaître cette entité.

Ensuite, pendant la phase de mise à jour de votre boucle, vos Systems ont maintenant une liste des entités à mettre à jour. Maintenant , vous pouvez avoir granularité des composants afin que vous puissiez concevoir des systèmes fous, construire des entités d'un ModelComponent, TransformComponent, FliesLikeSupermanComponentetSocketInfoComponent , et faire quelque chose de bizarre comme faire une soucoupe volante qui vole entre clients connectés à un jeu multijoueur. D'accord, peut-être pas ça, mais l'idée est que cela garde les choses découplées et flexibles.

Artemis n'est pas parfait, et les exemples sur le site sont un peu basiques, mais la séparation du code et des données est puissante. C'est aussi bon pour votre cache si vous le faites correctement. Artemis ne le fait probablement pas sur ce front, mais c'est bon d'apprendre.

michael.bartnett
la source