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.
la source
Réponses:
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:
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):
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:
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.
la source
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é, doncUn 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