Quels sont les inconvénients de l'utilisation de DrawableGameComponent pour chaque instance d'un objet de jeu?


25

J'ai lu à de nombreux endroits que DrawableGameComponents devrait être enregistré pour des choses comme des "niveaux" ou une sorte de gestionnaires au lieu de les utiliser, par exemple, pour des personnages ou des tuiles (comme ce type le dit ici ). Mais je ne comprends pas pourquoi il en est ainsi. J'ai lu ce post et cela avait beaucoup de sens pour moi, mais ce sont la minorité.

Habituellement, je ne ferais pas trop attention à des choses comme celles-ci, mais dans ce cas, je voudrais savoir pourquoi la majorité apparente pense que ce n'est pas la voie à suivre. Peut-être que je manque quelque chose.


Je suis curieux de savoir pourquoi, deux ans plus tard, vous avez supprimé la marque "accepté" de ma réponse ? Normalement, je ne m'en soucierais pas vraiment - mais dans ce cas, cela provoque une fausse représentation exaspérante de VeraShackle de ma réponse à la bulle vers le haut :(
Andrew Russell

@AndrewRussell J'ai lu ce fil il n'y a pas longtemps (en raison de certaines réponses et votes) et j'ai pensé que je n'étais pas qualifié pour donner une opinion sur le fait que votre réponse soit la bonne ou non. Comme vous le dites, cependant, je ne pense pas que la réponse de VeraShackle devrait être celle avec le plus de votes positifs, donc je marque à nouveau votre réponse comme correcte.
Kenji Kina

1
Je peux condenser ma réponse comme suit: "L' utilisation de DrawableGameComponentvous enferme dans un seul ensemble de signatures de méthode et un modèle de données conservé spécifique. Ce n'est généralement pas le bon choix au départ, et le verrouillage gêne considérablement l'évolution du code à l'avenir. "Mais laissez-moi vous dire ce qui se passe ici ...
Andrew Russell

1
DrawableGameComponentfait partie de l'API XNA de base (encore une fois: principalement pour des raisons historiques). Les programmeurs novices l'utilisent car il est "là" et cela "fonctionne" et ils ne savent pas mieux. Ils voient ma réponse leur disant qu'ils ont « tort » et - en raison de la nature de la psychologie humaine - rejettent cela et choisissent l'autre «côté». Ce qui se trouve être la non-réponse argumentative de VeraShackle; qui s'est avéré prendre de l'ampleur parce que je ne l'ai pas appelé immédiatement (voir ces commentaires). J'estime que ma réfutation éventuelle reste suffisante.
Andrew Russell

1
Si quelqu'un est intéressé à remettre en question la légitimité de ma réponse sur la base des votes positifs: alors qu'il est vrai que (GDSE étant à l'affût des novices susmentionnés), la réponse de VeraShackle a réussi à acquérir quelques votes positifs plus que les miens ces dernières années, n'oubliez pas que j'ai des milliers de votes supplémentaires à l'échelle du réseau, principalement sur XNA. J'ai implémenté l'API XNA sur plusieurs plateformes. Et je suis un développeur de jeux professionnel utilisant XNA. Le pedigree de ma réponse est impeccable.
Andrew Russell

Réponses:


22

Les "composants du jeu" ont été une partie très précoce de la conception XNA 1.0. Ils étaient censés fonctionner comme des contrôles pour WinForms. L'idée était que vous pouviez les glisser-déposer dans votre jeu en utilisant une interface graphique (un peu comme le bit pour ajouter des contrôles non visuels, comme des minuteries, etc.) et leur définir des propriétés.

Vous pourriez donc avoir des composants comme peut-être un appareil photo ou un ... compteur FPS? ... ou ...?

Tu vois? Malheureusement, cette idée ne fonctionne pas vraiment pour les jeux du monde réel. Le type de composants que vous souhaitez pour un jeu n'est pas vraiment réutilisable. Vous pouvez avoir un composant "joueur", mais il sera très différent du composant "joueur" de tous les autres jeux.

J'imagine que c'est pour cette raison, et pour un simple manque de temps, que l'interface graphique a été tirée avant XNA 1.0.

Mais l'API est toujours utilisable ... Voici pourquoi vous ne devriez pas l'utiliser:

Fondamentalement, cela se résume à ce qui est le plus facile à écrire, à lire, à déboguer et à maintenir en tant que développeur de jeux. Faire quelque chose comme ça dans votre code de chargement:

player.DrawOrder = 100;
enemy.DrawOrder = 200;

Est beaucoup moins clair que de simplement faire ceci:

virtual void Draw(GameTime gameTime)
{
    player.Draw();
    enemy.Draw();
}

Surtout quand, dans un jeu du monde réel, vous pourriez vous retrouver avec quelque chose comme ça:

GraphicsDevice.Viewport = cameraOne.Viewport;
player.Draw(cameraOne, spriteBatch);
enemy.Draw(cameraOne, spriteBatch);

GraphicsDevice.Viewport = cameraTwo.Viewport;
player.Draw(cameraTwo, spriteBatch);
enemy.Draw(cameraTwo, spriteBatch);

(Certes, vous pouvez partager la caméra et spriteBatch en utilisant une Servicesarchitecture similaire . Mais c'est aussi une mauvaise idée pour la même raison.)

Cette architecture - ou son absence - est tellement plus légère et flexible!

Je peux facilement changer l'ordre de dessin ou ajouter plusieurs fenêtres ou ajouter un effet cible de rendu, simplement en changeant quelques lignes. Pour le faire dans l'autre système, je dois penser à ce que font tous ces composants - répartis sur plusieurs fichiers - et dans quel ordre.

C'est un peu comme la différence entre la programmation déclarative et la programmation impérative. Il s'avère que, pour le développement de jeux, la programmation impérative est beaucoup plus préférable.


2
J'avais l'impression qu'ils étaient destinés à la programmation basée sur les composants , qui est apparemment largement utilisée pour la programmation de jeux.
BlueRaja - Danny Pflughoeft

3
Les classes de composants de jeu XNA ne sont pas vraiment directement applicables à ce modèle. Ce modèle consiste à composer des objets à partir de plusieurs composants. Les classes XNA sont adaptées à un composant par objet. S'ils s'intègrent parfaitement dans votre conception, utilisez-les par tous les moyens. Mais vous ne devez pas construire votre design autour d'eux! Voir aussi ma réponse ici - cherchez où je mentionne DrawableGameComponent.
Andrew Russell

1
Je suis d'accord que l'architecture GameComponent / Service n'est pas du tout utile et sonne comme des concepts d'application Office ou Web étant forcés dans un cadre de jeu. Mais je préférerais de loin utiliser la player.DrawOrder = 100;méthode plutôt que celle impérative pour l'ordre des tirages. Je termine un jeu Flixel en ce moment, qui dessine des objets dans l'ordre dans lequel vous les ajoutez à la scène. Le système FlxGroup atténue une partie de cela, mais avoir une sorte de tri d'index Z intégré aurait été bien.
michael.bartnett

@ michael.bartnett Notez que Flixel est une API en mode retenu, tandis que XNA (sauf, évidemment, le système de composants de jeu) est une API en mode immédiat. Si vous utilisez une API en mode conservé ou implémentez la vôtre , la possibilité de modifier l'ordre de tirage à la volée est certainement importante. En fait, la nécessité de changer l'ordre de tirage à la volée est une bonne raison de faire quelque chose en mode retenu (l'exemple d'un système de fenêtrage me vient à l'esprit). Mais si vous pouvez écrire quelque chose en mode immédiat, si l'API de la plate-forme (comme XNA) est construite de cette façon, alors IMO vous devriez.
Andrew Russell


26

Hmmm. Je dois contester celui-ci pour un peu d'équilibre ici, je le crains.

Il semble y avoir 5 accusations dans la réponse d'Andrew, alors je vais essayer de traiter chacune à son tour:

1) Les composants sont une conception obsolète et abandonnée.

L'idée du modèle de conception des composants existait bien avant XNA - en substance, c'est simplement une décision de faire des objets, plutôt que l'objet de jeu sur-archivé, la maison de leur propre comportement de mise à jour et de dessin. Pour les objets de sa collection de composants, le jeu appellera ces méthodes (et quelques autres) automatiquement au moment approprié - de manière cruciale, l'objet du jeu n'a lui-même à `` connaître '' aucun de ce code. Si vous souhaitez réutiliser vos classes de jeu dans différents jeux (et donc différents objets de jeu), ce découplage d'objets est toujours fondamentalement une bonne idée. Un rapide coup d'œil aux dernières démos (produites par des professionnels) de XNA4.0 de l'AppHub confirme mon impression que Microsoft est toujours (à juste titre à mon avis) fermement derrière celui-ci.

2) Andrew ne peut pas penser à plus de 2 classes de jeu réutilisables.

Je peux. En fait je peux penser aux charges! Je viens de vérifier ma bibliothèque partagée actuelle, et elle comprend plus de 80 composants et services réutilisables . Pour vous donner une saveur, vous pourriez avoir:

  • Gestionnaires d'état du jeu / écran
  • Émetteurs de particules
  • Primitives dessinables
  • Contrôleurs AI
  • Contrôleurs d'entrée
  • Contrôleurs d'animation
  • Panneaux d'affichage
  • Gestionnaires de physique et de collision
  • Appareils photo

Toute idée de jeu particulière aura besoin d'au moins 5 à 10 éléments de cet ensemble - c'est garanti - et ils peuvent être entièrement fonctionnels dans un tout nouveau jeu avec une seule ligne de code.

3) Le contrôle de l'ordre de tirage par l'ordre des appels de tirage explicites est préférable à l'utilisation de la propriété DrawOrder du composant.

Eh bien, je suis essentiellement d'accord avec Andrew sur celui-ci. MAIS, si vous laissez toutes les propriétés DrawOrder de votre composant par défaut, vous pouvez toujours contrôler explicitement leur ordre de dessin par l'ordre dans lequel vous les ajoutez à la collection. Ceci est vraiment identique en termes de «visibilité» à l'ordre explicite de leurs appels de tirage manuel.

4) Appeler vous-même une charge des méthodes Draw d'un objet est plus «léger» que de les appeler par une méthode de framework existante.

Pourquoi? De plus, dans tous les projets, sauf les plus triviaux, votre logique de mise à jour et votre code Draw éclipseront totalement votre méthode d'appel en termes de performances en tout cas.

5) Placer votre code dans un fichier est préférable, lors du débogage, que de l'avoir dans le fichier de l'objet individuel.

Eh bien, tout d'abord, lorsque je débogue dans Visual Studio, l'emplacement d'un point d'arrêt en termes de fichiers sur le disque est presque invisible de nos jours - l'IDE traite de l'emplacement sans aucun drame. Mais considérez également un instant que vous avez un problème avec votre dessin Skybox. Passer à la méthode Draw de Skybox est-il vraiment plus onéreux que de localiser le code de dessin Skybox dans la méthode de dessin maître (vraisemblablement très longue) dans l'objet Game? Non, pas vraiment - en fait, je dirais que c'est tout le contraire.

Donc, en résumé - veuillez examiner attentivement les arguments d'Andrew ici. Je ne pense pas qu'ils donnent réellement des raisons substantielles d'écrire du code de jeu enchevêtré. Les avantages du modèle de composant découplé (utilisé judicieusement) sont énormes.


2
Je viens de remarquer ce post, et je dois faire une réfutation forte: je n'ai aucun problème avec les classes réutilisables! (Qui peut avoir dessiner et les méthodes de mise à jour et ainsi de suite.) Je prends spécifique question à l' utilisation ( Drawable) GameComponentpour fournir l' architecture de votre jeu, en raison de la perte de contrôle explicite sur tirage / commande de mise à jour (vous êtes d' accord avec dans # 3) la rigidité de leurs interfaces (que vous n'abordez pas).
Andrew Russell

1
Votre point n ° 4 prend mon utilisation du mot «léger» pour signifier la performance, où je veux dire clairement la flexibilité architecturale. Vos points restants # 1, # 2, et surtout # 5 semblent ériger l' homme de paille absurde que je pense que la plupart / tout le code devrait être inséré dans votre classe de jeu principale! Lisez ma réponse ici pour certaines de mes réflexions plus détaillées sur l'architecture.
Andrew Russell

5
Enfin: si vous allez poster une réponse à ma réponse, disant que je me trompe, il aurait été courtois d'ajouter également une réponse à ma réponse pour que je voie une notification à ce sujet, plutôt que de devoir trébucher il deux mois plus tard.
Andrew Russell

J'ai peut-être mal compris, mais comment cela répond-il à la question? Dois-je utiliser un GameComponent pour mes sprites ou non?
Superbest


4

Je suis un peu en désaccord avec Andrew, mais je pense aussi qu'il y a un temps et un lieu pour tout. Je n'utiliserais pas un Drawable(GameComponent)pour quelque chose d'aussi simple qu'un Sprite ou un Ennemi. Cependant, je l'utiliserais pour un SpriteManagerou EnemyManager.

Pour en revenir à ce qu'Andrew a dit à propos de l'ordre de tirage et de mise à jour, je pense que ce Sprite.DrawOrderserait très déroutant pour de nombreux sprites. En outre, il est relativement facile de mettre en place un DrawOrderet UpdateOrderpour un petit nombre (ou raisonnable) de gestionnaires qui le sont (Drawable)GameComponents.

Je pense que Microsoft les aurait supprimés s'ils avaient vraiment été rigides et que personne ne les avait utilisés. Mais ils ne l'ont pas parce qu'ils fonctionnent parfaitement bien, si le rôle est bon. Je les utilise moi-même pour développer des Game.Servicesmises à jour en arrière-plan.


2

J'y pensais aussi, et je me suis dit: par exemple, pour voler l'exemple dans votre lien, dans un jeu RTS, vous pourriez faire de chaque unité une instance de composant de jeu. D'accord.

Mais vous pouvez également créer un composant UnitManager, qui conserve une liste d'unités, les dessine et les met à jour si nécessaire. J'imagine que la raison pour laquelle vous voulez transformer vos unités en composants de jeu en premier lieu est de compartimenter le code, alors cette solution atteindrait-elle adéquatement cet objectif?


2

Dans les commentaires, j'ai posté une version condensée de ma réponse ancienne réponse:

L'utilisation DrawableGameComponentvous enferme dans un seul ensemble de signatures de méthode et dans un modèle de données conservé spécifique. Ce n'est généralement pas le bon choix au départ, et le verrouillage gêne considérablement l'évolution du code à l'avenir.

Ici, je vais essayer d'illustrer pourquoi c'est le cas en publiant du code de mon projet actuel.


L'objet racine qui maintient l'état du monde du jeu a une Updateméthode qui ressemble à ceci:

void Update(UpdateContext updateContext, MultiInputState currentInput)

Il existe un certain nombre de points d'entrée vers Update, selon que le jeu fonctionne ou non sur le réseau. Il est possible, par exemple, que l'état du jeu soit restauré à un point précédent dans le temps, puis simulé de nouveau.

Il existe également des méthodes de mise à jour supplémentaires, telles que:

void PlayerJoin(int playerIndex, string playerName, UpdateContext updateContext)

Dans l'état du jeu, il y a une liste d'acteurs à mettre à jour. Mais c'est plus qu'un simple Updateappel. Il y a des passes supplémentaires avant et après pour gérer la physique. Il y a aussi des trucs de fantaisie pour la génération / destruction différée des acteurs, ainsi que des transitions de niveau.

En dehors de l'état du jeu, il existe un système d'interface utilisateur qui doit être géré de manière indépendante. Mais certains écrans de l'interface utilisateur doivent également pouvoir s'exécuter dans un contexte réseau.


Pour le dessin, en raison de la nature du jeu, il existe un système de tri et d'élimination personnalisé qui prend les entrées de ces méthodes:

virtual void RegisterToDraw(SortedDrawList sortedDrawList, Definitions definitions)
virtual void RegisterShadow(ShadowCasterList shadowCasterList, Definitions definitions)

Et puis, une fois qu'il sait quoi dessiner, appelle automatiquement:

virtual void Draw(DrawContext drawContext, int tag)

Le tagparamètre est obligatoire car certains acteurs peuvent s'accrocher à d'autres acteurs - et doivent donc pouvoir dessiner à la fois au-dessus et en dessous de l'acteur tenu. Le système de tri garantit que cela se produit dans le bon ordre.

Il y a aussi le système de menus à dessiner. Et un système hiérarchique pour dessiner l'interface utilisateur, autour du HUD, autour du jeu.


Comparez maintenant ces exigences avec ce qui DrawableGameComponentfournit:

public class DrawableGameComponent
{
    public virtual void Update(GameTime gameTime);
    public virtual void Draw(GameTime gameTime);
    public int UpdateOrder { get; set; }
    public int DrawOrder { get; set; }
}

Il n'y a aucun moyen raisonnable de passer d'un système utilisant DrawableGameComponentle système décrit ci-dessus. (Et ce ne sont pas des exigences inhabituelles pour un jeu d'une complexité même modeste).

De plus, il est très important de noter que ces exigences ne sont pas simplement apparues au début du projet. Ils ont évolué au cours de plusieurs mois de travail de développement.


Ce que les gens font généralement, s'ils commencent sur la DrawableGameComponentroute, c'est qu'ils commencent à construire sur des hacks:

  • Au lieu d'ajouter des méthodes, ils effectuent une conversion moche de leur propre type de base (pourquoi ne pas simplement l'utiliser directement?).

  • Au lieu de remplacer le code pour trier et appeler Draw/ Update, ils font des choses très lentes pour changer les numéros de commande à la volée.

  • Et le pire de tout: au lieu d'ajouter des paramètres aux méthodes, ils commencent à "passer" des "paramètres" dans des emplacements de mémoire qui ne sont pas la pile (l'utilisation de Servicespour cela est populaire). C'est compliqué, sujet aux erreurs, lent, fragile, rarement thread-safe et susceptible de devenir un problème si vous ajoutez des réseaux, créez des outils externes, etc.

    Cela rend également la réutilisation du code plus difficile , car maintenant vos dépendances sont cachées à l'intérieur du "composant", au lieu d'être publiées à la limite de l'API.

  • Ou, ils voient presque à quel point tout cela est fou, et commencent à faire des "managers" DrawableGameComponentpour contourner ces problèmes. Sauf qu'ils ont toujours ces problèmes aux frontières du "manager".

    Invariablement, ces «gestionnaires» pourraient facilement être remplacés par une série d'appels de méthode dans l'ordre souhaité. Tout le reste est trop compliqué.

Bien sûr, une fois que vous commencez à utiliser ces hacks, il faut du vrai travail pour les annuler une fois que vous réalisez que vous en avez besoin de plus que ce qui est DrawableGameComponentfourni. Vous devenez " enfermé ". Et la tentation est souvent de continuer à empiler des hacks - ce qui, en pratique, vous enlèvera et limitera les types de jeux que vous pouvez créer à des jeux relativement simplistes.


Beaucoup de gens semblent arriver à ce point et avoir une réaction viscérale - peut-être parce qu'ils utilisent DrawableGameComponentet ne veulent pas accepter d'avoir "tort". Ils s'accrochent donc au fait qu'il est au moins possible de le faire "fonctionner".

Bien sûr, cela peut être fait pour "fonctionner"! Nous sommes des programmeurs - faire fonctionner les choses sous des contraintes inhabituelles est pratiquement notre travail. Mais cette retenue n'est pas nécessaire, utile ou rationnelle ...


Ce qui est particulièrement ahurissant est à ce sujet , il est étonnamment trivial à pas de côté cette question ensemble. Ici, je vais même vous donner le code:

public class Actor
{
    public virtual void Update(GameTime gameTime);
    public virtual void Draw(GameTime gameTime);
}

List<Actor> actors = new List<Actor>();

foreach(var actor in actors)
    actor.Update(gameTime);

foreach(var actor in actors)
    actor.Draw(gameTime);

(Il est simple d'ajouter le tri selon DrawOrderet UpdateOrder. Une façon simple est de gérer deux listes et de les utiliser List<T>.Sort(). Il est également trivial de gérer Load/ UnloadContent.)

Peu importe ce que ce code est réellement . Ce qui est important, c'est que je puisse le modifier trivialement .

Quand je me rends compte que j'ai besoin d'un système de chronométrage différent gameTime, ou j'ai besoin de plus de paramètres (ou d'un UpdateContextobjet entier avec environ 15 choses dedans), ou j'ai besoin d'un ordre d'opérations complètement différent pour le dessin, ou j'ai besoin de méthodes supplémentaires pour différentes passes de mise à jour, je peux simplement aller de l'avant et le faire .

Et je peux le faire immédiatement. Je n'ai pas besoin de me préoccuper de choisir DrawableGameComponentmon code. Ou pire: piratez-le d'une manière qui rendra encore plus difficile la prochaine fois qu'un tel besoin se manifestera.


Il n'y a vraiment qu'un seul scénario où c'est un bon choix à utiliser DrawableGameComponent: et c'est si vous créez et publiez littéralement un middleware de type glisser-déposer pour XNA. C'était son but d'origine. Ce que - j'ajouterai - personne ne fait!

(De même, cela n'a vraiment de sens queServices lorsque vous créez des types de contenu personnalisés pour ContentManager.)


Bien que je sois d'accord avec vos points, je ne vois pas non plus de problème particulier dans l'utilisation de choses héritées de DrawableGameComponentcertains composants, en particulier pour des choses comme les écrans de menu (menus, beaucoup de boutons, options et choses), ou les superpositions FPS / debug vous avez mentionné. Dans ces cas, vous n'avez généralement pas besoin d'un contrôle fin sur l'ordre de tirage et vous vous retrouvez avec moins de code que vous devez écrire manuellement (ce qui est une bonne chose, sauf si vous devez écrire ce code). Mais je suppose que c'est à peu près le scénario de glisser-déposer dont vous parliez.
Groo
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.