Quand / où mettre à jour les composants

10

Au lieu de mes moteurs de jeu lourds d'héritage habituels, je joue avec une approche davantage basée sur les composants. Cependant, j'ai du mal à justifier où laisser les composants faire leur travail.

Disons que j'ai une entité simple qui a une liste de composants. Bien entendu, l'entité ne sait pas quels sont ces composants. Il peut y avoir un composant présent qui donne à l'entité une position à l'écran, un autre peut être là pour dessiner l'entité à l'écran.

Pour que ces composants fonctionnent, ils doivent mettre à jour chaque image, la façon la plus simple de le faire est de parcourir l'arborescence de la scène, puis de mettre à jour chaque composant pour chaque entité. Mais certains composants peuvent nécessiter un peu plus de gestion. Par exemple, un composant qui rend une entité collidable doit être géré par quelque chose qui peut superviser tous les composants collidables. Un composant qui rend une entité dessinable a besoin de quelqu'un pour superviser tous les autres composants dessinables afin de déterminer l'ordre de dessin, etc.

Donc ma question est, où dois-je mettre à jour les composants, quelle est une façon propre de les faire parvenir aux gestionnaires?

J'ai pensé à utiliser un objet gestionnaire singleton pour chacun des types de composants, mais qui présente les inconvénients habituels de l'utilisation d'un singleton, un moyen de remédier à cela est un peu d'utiliser l'injection de dépendances, mais cela semble exagéré pour ce problème. Je pourrais également parcourir l'arborescence de la scène, puis rassembler les différents composants dans des listes à l'aide d'une sorte de modèle d'observation, mais cela semble un peu inutile de faire chaque image.

Roy T.
la source
1
Utilisez-vous les systèmes d'une manière ou d'une autre?
Asakeron
Les systèmes de composants sont la manière habituelle de le faire. Personnellement, j'appelle simplement la mise à jour sur toutes les entités, ce qui appelle la mise à jour sur tous les composants, et j'ai quelques cas "spéciaux" (comme le gestionnaire spatial pour la détection de collision, qui est statique).
ashes999
Systèmes de composants? Je n'en ai jamais entendu parler auparavant. Je vais commencer Google, mais j'accueillerais volontiers tous les liens recommandés.
Roy T.
1
Entity Systems sont l'avenir du développement MMOG est une grande ressource. Et, pour être honnête, je suis toujours confus par ces noms d'architecture. La différence avec l'approche suggérée est que les composants ne contiennent que des données et que les systèmes les traitent. Cette réponse est également très pertinente.
Asakeron
1
J'ai écrit un article de blog sinistre
AlexFoxGill

Réponses:

15

Je suggère de commencer par lire les 3 gros mensonges de Mike Acton, car vous en violez deux. Je suis sérieux, cela va changer la façon dont vous concevez votre code: http://cellperformance.beyond3d.com/articles/2008/03/three-big-lies.html

Alors, que violez-vous?

Mensonge n ° 3 - Le code est plus important que les données

Vous parlez d'injection de dépendance, qui peut être utile dans certains (et seulement certains) cas, mais devrait toujours sonner l'alarme si vous l'utilisez, en particulier dans le développement de jeux! Pourquoi? Parce que c'est une abstraction souvent inutile. Et les abstractions aux mauvais endroits sont horribles. Vous avez donc un jeu. Le jeu a des gestionnaires pour différents composants. Les composants sont tous définis. Faites donc une classe quelque part dans votre code de boucle de jeu principal qui "a" les gestionnaires. Comme:

private CollissionManager _collissionManager;
private BulletManager _bulletManager;

Donnez-lui quelques fonctions getter pour obtenir chaque classe de gestionnaire (getBulletManager ()). Peut-être que cette classe elle-même est un singleton ou qu'elle est accessible à partir d'un (vous avez probablement un singleton de jeu central quelque part de toute façon). Il n'y a rien de mal avec des données et un comportement codés en dur bien définis.

Ne créez pas un ManagerManager qui vous permet d'enregistrer des Managers à l'aide d'une clé, qui peut être récupérée à l'aide de cette clé par d'autres classes qui souhaitent utiliser le Manager. C'est un excellent système et très flexible, mais où l'on parle d'un jeu ici. Vous savez exactement quels systèmes sont en jeu. Pourquoi faire comme si tu ne le fais pas? Parce que c'est un système pour les gens qui pensent que le code est plus important que les données. Ils diront "Le code est flexible, les données le remplissent". Mais le code n'est que des données. Le système que j'ai décrit est beaucoup plus facile, plus fiable, plus facile à entretenir et beaucoup plus flexible (par exemple, si le comportement d'un gestionnaire diffère des autres gestionnaires, vous n'avez qu'à changer quelques lignes au lieu de retravailler l'ensemble du système)

Mensonge n ° 2 - Le code doit être conçu autour d'un modèle du monde

Vous avez donc une entité dans le monde du jeu. L'entité a un certain nombre de composants définissant son comportement. Vous créez donc une classe Entity avec une liste d'objets Component et une fonction Update () qui appelle la fonction Update () de chaque Component. Droite?

Non :) C'est la conception autour d'un modèle du monde: vous avez une balle dans votre jeu, donc vous ajoutez une classe Bullet. Ensuite, vous mettez à jour chaque puce et passez à la suivante. Cela tuera absolument vos performances et vous donnera une horrible base de code alambiquée avec du code en double partout et aucune structuration logique de code similaire. (Consultez ma réponse ici pour une explication plus détaillée des raisons pour lesquelles la conception OO traditionnelle craint, ou recherchez la conception orientée données)

Jetons un œil à la situation sans notre biais OO. Nous voulons ce qui suit, ni plus ni moins (veuillez noter qu'il n'y a pas d'obligation de créer une classe pour une entité ou un objet):

  • Vous avez un tas d'entités
  • Les entités comprennent un certain nombre de composants qui définissent le comportement de l'entité
  • Vous souhaitez mettre à jour chaque composant du jeu à chaque image, de préférence de manière contrôlée
  • Outre l'identification des composants comme appartenant ensemble, l'entité elle-même n'a rien à faire. C'est un lien / ID pour quelques composants.

Et regardons la situation. Votre système de composants mettra à jour le comportement de chaque objet du jeu à chaque image. Il s'agit certainement d'un système critique de votre moteur. La performance est importante ici!

Si vous connaissez l'architecture informatique ou la conception orientée données, vous savez comment obtenir les meilleures performances: une mémoire compacte et en regroupant l'exécution du code. Si vous exécutez des extraits de code A, B et C comme ceci: ABCABCABC, vous n'obtiendrez pas les mêmes performances que lorsque vous l'exécutez comme ceci: AAABBBCCC. Ce n'est pas seulement parce que le cache d'instructions et de données sera utilisé plus efficacement, mais aussi parce que si vous exécutez tous les «A» les uns après les autres, il y a beaucoup de place pour l'optimisation: supprimer le code en double, précalculer les données utilisées par tous les "A", etc.

Donc, si nous voulons mettre à jour tous les composants, ne faisons pas d'eux des classes / objets avec une fonction de mise à jour. N'appelons pas cette fonction de mise à jour pour chaque composant de chaque entité. C'est la solution "ABCABCABC". Regroupons toutes les mises à jour de composants identiques. Ensuite, nous pouvons mettre à jour tous les composants A, suivis de B, etc. De quoi avons-nous besoin pour faire cela?

Tout d'abord, nous avons besoin de gestionnaires de composants. Pour chaque type de composant du jeu, nous avons besoin d'une classe de manager. Il a une fonction de mise à jour qui mettra à jour tous les composants de ce type. Il a une fonction de création qui ajoutera un nouveau composant de ce type et une fonction de suppression qui détruira le composant spécifié. Il peut y avoir d'autres fonctions d'aide pour obtenir et définir des données spécifiques à ce composant (par exemple: définir le modèle 3D pour le composant de modèle). Notez que le gestionnaire est en quelque sorte une boîte noire pour le monde extérieur. Nous ne savons pas comment les données de chaque composant sont stockées. Nous ne savons pas comment chaque composant est mis à jour. Peu nous importe, tant que les composants se comportent comme ils le devraient.

Ensuite, nous avons besoin d'une entité. Vous pourriez en faire un cours, mais ce n'est pas vraiment nécessaire. Une entité ne peut être rien de plus qu'un ID entier unique ou une chaîne hachée (donc aussi un entier). Lorsque vous créez un composant pour l'entité, vous transmettez l'ID comme argument au gestionnaire. Lorsque vous souhaitez supprimer le composant, vous transmettez à nouveau l'ID. Il peut y avoir certains avantages à ajouter un peu plus de données à l'entité au lieu de simplement en faire un ID, mais ce ne seront que des fonctions d'assistance car, comme je l'ai indiqué dans les exigences, tout le comportement de l'entité est défini par les composants eux-mêmes. C'est votre moteur, alors faites ce qui a du sens pour vous.

Ce dont nous avons besoin, c'est d'un gestionnaire d'entités. Cette classe génère soit des ID uniques si vous utilisez la solution ID uniquement, soit elle peut être utilisée pour créer / gérer des objets Entity. Il peut également conserver une liste de toutes les entités du jeu si vous en avez besoin. L'Entity Manager peut être la classe centrale de votre système de composants, stockant les références à tous les ComponentManagers de votre jeu et appelant leurs fonctions de mise à jour dans le bon ordre. De cette façon, tout ce que la boucle de jeu doit faire est d'appeler EntityManager.update () et l'ensemble du système est bien séparé du reste de votre moteur.

C'est la vue à vol d'oiseau, regardons comment fonctionnent les gestionnaires de composants. Voici ce dont vous avez besoin:

  • Créer des données de composant lorsque create (entityID) est appelé
  • Supprimer les données de composant lors de l'appel de remove (entityID)
  • Mettre à jour toutes les données des composants (applicables) lorsque update () est appelé (c.-à-d. Que tous les composants n'ont pas besoin de mettre à jour chaque trame)

Le dernier est l'endroit où vous définissez le comportement / la logique des composants et dépend complètement du type de composant que vous écrivez. Le composant Animation mettra à jour les données d'animation en fonction de l'image sur laquelle il se trouve. Le DragableComponent mettra à jour uniquement un composant qui est déplacé par la souris. Le Composant Physique mettra à jour les données du système physique. Cependant, comme vous mettez à jour tous les composants du même type en une seule fois, vous pouvez effectuer certaines optimisations qui ne sont pas possibles lorsque chaque composant est un objet distinct avec une fonction de mise à jour qui peut être appelée à tout moment.

Notez que je n'ai toujours jamais appelé à la création d'une classe XxxComponent pour contenir les données des composants. C'est à toi de voir. Vous aimez la conception orientée données? Structurez ensuite les données dans des tableaux séparés pour chaque variable. Aimez-vous la conception orientée objet? (Je ne le recommanderais pas, cela tuera toujours vos performances dans de nombreux endroits) Ensuite, créez un objet XxxComponent qui contiendra les données de chaque composant.

La grande chose au sujet des gestionnaires est l'encapsulation. Maintenant, l'encapsulation est l'une des philosophies les plus horriblement mal utilisées dans le monde de la programmation. C'est ainsi qu'il devrait être utilisé. Seul le gestionnaire sait quelles données de composant sont stockées, où fonctionne la logique d'un composant. Il y a quelques fonctions pour obtenir / définir des données mais c'est tout. Vous pouvez réécrire l'intégralité du gestionnaire et ses classes sous-jacentes et si vous ne changez pas l'interface publique, personne ne le remarque même. Moteur physique modifié? Réécrivez simplement PhysicsComponentManager et vous avez terminé.

Ensuite, il y a une dernière chose: la communication et le partage de données entre les composants. Maintenant, c'est délicat et il n'y a pas de solution unique. Vous pouvez créer des fonctions get / set dans les gestionnaires pour permettre, par exemple, au composant collision d'obtenir la position du composant position (c'est-à-dire PositionManager.getPosition (entityID)). Vous pouvez utiliser un système d'événements. Vous pouvez stocker des données partagées dans l'entité (la solution la plus laide à mon avis). Vous pourriez utiliser (c'est souvent utilisé) un système de messagerie. Ou utilisez une combinaison de plusieurs systèmes! Je n'ai pas le temps ni l'expérience pour entrer dans chacun de ces systèmes, mais la recherche Google et stackoverflow sont vos amis.

Marché
la source
Je trouve cette réponse très intéressante. Juste une question (j'espère que vous ou quelqu'un pouvez me répondre). Comment parvenez-vous à éliminer l'entité sur un système basé sur des composants DOD? Même Artemis utilise Entity en tant que classe, je ne suis pas sûr que ce soit très idiot.
Wolfrevo Kcats
1
Que voulez-vous dire par l'éliminer? Voulez-vous dire un système d'entités sans classe d'entité? La raison pour laquelle Artemis a une entité est parce que dans Artemis, la classe Entity gère ses propres composants. Dans le système que j'ai proposé, les classes ComponentManager gèrent les composants. Ainsi, au lieu d'avoir besoin d'une classe Entity, vous pouvez simplement avoir un ID entier unique. Supposons donc que vous ayez l'entité 254, qui a une composante position. Lorsque vous souhaitez modifier la position, vous pouvez appeler PositionCompMgr.setPosition (int id, Vector3 newPos), avec 254 comme paramètre id.
Mart
Mais comment gérez-vous les identifiants? Que faire si vous souhaitez supprimer un composant d'une entité pour l'attribuer ultérieurement à un autre? Que faire si vous souhaitez supprimer une entité et en ajouter une nouvelle? Que faire si vous souhaitez qu'un composant soit partagé entre deux ou plusieurs entités? Ça m'intéresse vraiment.
Wolfrevo Kcats
1
EntityManager peut être utilisé pour donner de nouveaux ID. Il pourrait également être utilisé pour créer des entités complètes basées sur des modèles prédéfinis (par exemple, créer "EnemyNinja" qui génère un nouvel ID et crée tous les composants qui composent un ninja ennemi tels que le rendu, la collision, l'IA, peut-être un composant pour le combat de mêlée , etc). Il peut également avoir une fonction removeEntity qui appelle automatiquement toutes les fonctions de suppression de ComponentManager. Le ComponentManager peut vérifier s'il possède des données de composant pour l'entité donnée et, dans l'affirmative, supprimer ces données.
Mart
1
Déplacer un composant d'une entité à une autre? Ajoutez simplement une fonction swapComponentOwner (int oldEntity, int newEntity) à chaque ComponentManager. Les données sont toutes là dans ComponentManager, tout ce dont vous avez besoin est une fonction pour changer le propriétaire auquel il appartient. Chaque ComponentManager aura quelque chose comme un index ou une carte pour stocker quelles données appartiennent à quel ID d'entité. Modifiez simplement l'ID d'entité de l'ancien au nouvel ID. Je ne sais pas si le partage de composants est facile dans le système que j'ai pensé, mais à quel point cela peut-il être difficile? Au lieu d'un lien Entity ID <-> Component Data dans la table d'index, il y en a plusieurs.
Mart
3

Pour que ces composants fonctionnent, ils doivent mettre à jour chaque image, la façon la plus simple de le faire est de parcourir l'arborescence de la scène, puis de mettre à jour chaque composant pour chaque entité.

C'est l'approche naïve typique des mises à jour de composants (et il n'y a rien de mal à ce que cela soit naïf, si cela fonctionne pour vous). L'un des gros problèmes qu'il vous a réellement abordés - vous travaillez via l'interface du composant (par exemple IComponent), donc vous ne savez rien de ce que vous venez de mettre à jour. Vous ne savez probablement rien non plus sur l'ordre des composants au sein de l'entité, donc

  1. vous êtes susceptible de mettre à jour fréquemment des composants de types différents (localité de référence de code médiocre, essentiellement)
  2. ce système ne se prête pas bien aux mises à jour simultanées car vous n'êtes pas en mesure d'identifier les dépendances de données et donc de répartir les mises à jour en groupes locaux d'objets non liés.

J'ai pensé à utiliser un objet gestionnaire singleton pour chacun des types de composants, mais qui présente les inconvénients habituels de l'utilisation d'un singleton, un moyen de remédier à cela est un peu d'utiliser l'injection de dépendances, mais cela semble exagéré pour ce problème.

Un singleton n'est pas vraiment nécessaire ici, et vous devez donc l'éviter car il présente les inconvénients que vous avez mentionnés. L'injection de dépendances n'est pas exagérée - le cœur du concept est que vous passez les choses dont un objet a besoin à cet objet, idéalement dans le constructeur. Vous n'avez pas besoin d'un framework DI lourd (comme Ninject ) pour cela - passez simplement un paramètre supplémentaire à un constructeur quelque part.

Un moteur de rendu est un système fondamental et il prend probablement en charge la création et la gestion de la durée de vie d'un groupe d'objets pouvant être rendus qui correspondent à des éléments visuels de votre jeu (sprites ou modèles, probablement). De même, un moteur physique a probablement un contrôle à vie sur les choses qui représentent les entités qui peuvent se déplacer dans la simulation physique (corps rigides). Chacun de ces systèmes pertinents devrait posséder, dans une certaine mesure, ces objets et être responsable de leur mise à jour.

Les composants que vous utilisez dans votre système de composition d'entité de jeu doivent simplement être des enveloppes autour des instances de ces systèmes de niveau inférieur - votre composant de position peut simplement envelopper un corps rigide, votre composant visuel enveloppe simplement un sprite ou un modèle rendable, et cetera.

Ensuite, le système lui-même propriétaire des objets de niveau inférieur est responsable de leur mise à jour, et peut le faire en bloc et d'une manière qui lui permet de multithread cette mise à jour si nécessaire. Votre boucle de jeu principale contrôle l'ordre brut de mise à jour de ces systèmes (physique d'abord, puis rendu, ou autre). Si vous avez un sous-système qui n'a pas de contrôle à vie ou de mise à jour sur les instances qu'il distribue, vous pouvez créer un wrapper simple pour gérer également la mise à jour de tous les composants pertinents pour ce système, et décider où le placer mise à jour par rapport au reste de vos mises à jour système (cela se produit souvent, je trouve, avec des composants "script").

Cette approche est parfois connue sous le nom d' approche de composant externe , si vous recherchez plus de détails.


la source