Tactiques pour déplacer la logique de rendu hors de la classe GameObject

10

Lorsque vous créez des jeux, vous créez souvent l'objet de jeu suivant dont toutes les entités héritent:

public class GameObject{
    abstract void Update(...);
    abstract void Draw(...);
}

Donc, dans la boucle de mise à jour, vous itérez sur tous les objets du jeu et leur donnez une chance de changer d'état, puis dans la boucle de dessin suivante, vous itérez à nouveau sur tous les objets du jeu et leur donnez une chance de se dessiner.

Bien que cela fonctionne assez bien dans un jeu simple avec un simple rendu vers l'avant, cela conduit souvent à quelques objets de jeu gigantesques qui doivent stocker leurs modèles, à plusieurs textures et, pire encore, à une méthode de tirage au sort qui crée un couplage étroit entre l'objet de jeu, la stratégie de rendu actuelle et toutes les classes liées au rendu.

Si je devais changer la stratégie de rendu d'avant en différé, je devrais mettre à jour beaucoup d'objets de jeu. Et les objets de jeu que je fabrique ne sont pas aussi réutilisables qu'ils pourraient l'être. Bien sûr, l'héritage et / ou la composition peuvent m'aider à lutter contre la duplication de code et faciliter un peu la modification de l'implémentation, mais cela fait toujours défaut.

Une meilleure façon, peut-être, serait de supprimer complètement la méthode Draw de la classe GameObject et de créer une classe Renderer. Le GameObject devrait toujours contenir des données sur ses visuels, comme le modèle avec lequel le représenter et les textures à peindre sur le modèle, mais la manière de procéder serait laissée au rendu. Cependant, il y a souvent beaucoup de cas de frontière dans le rendu, même si cela supprimerait le couplage étroit du GameObject au Renderer, le Renderer devrait toujours être au courant de tous les objets du jeu qui le rendraient gras, tout en sachant et couplage étroit. Cela violerait pas mal de bonnes pratiques. Peut-être que la conception orientée données pourrait faire l'affaire. Les objets de jeu seraient certainement des données, mais comment le moteur de rendu serait-il entraîné par cela? Je ne suis pas sûr.

Je suis donc perdu et je ne vois pas de bonne solution. J'ai essayé d'utiliser les principes de MVC et dans le passé, j'avais quelques idées sur la façon de l'utiliser dans les jeux, mais récemment, cela ne semble pas aussi applicable que je le pensais. J'aimerais savoir comment vous abordez tous ce problème.

Quoi qu'il en soit, récapitulons, je suis intéressé par la façon dont les objectifs de conception suivants peuvent être atteints.

  • Aucune logique de rendu dans l'objet de jeu
  • Couplage lâche entre les objets du jeu et le moteur de rendu
  • Aucun rendu qui sait tout
  • De préférence, commutation d'exécution entre les moteurs de rendu

La configuration de projet idéale serait une «logique de jeu» distincte et un projet de logique de rendu qui n'ont pas besoin de se référencer.

Ce train de pensée a commencé lorsque j'ai entendu John Carmack dire sur Twitter qu'il avait un système si flexible qu'il pouvait échanger des moteurs de rendu au moment de l'exécution et même dire à son système d'utiliser les deux moteurs de rendu (un moteur de rendu logiciel et un moteur de rendu accéléré par le matériel) en même temps afin qu'il puisse inspecter les différences. Les systèmes que j'ai programmés jusqu'à présent ne sont même pas aussi flexibles

Roy T.
la source

Réponses:

7

Un premier pas rapide vers le découplage:

Les objets de jeu font référence à un identifiant de ce que sont leurs visuels mais pas aux données, disons quelque chose de simple comme une chaîne. Exemple: "human_male"

Le moteur de rendu est responsable du chargement et de la maintenance des références "human_male" et de la transmission aux objets d'une poignée à utiliser.

Exemple dans un pseudocode horrible:

GameObject( initialization parameters )
  me.render_handle = Renderer_Create( parameters.render_string )

- elsewhere
Renderer_Create( string )

  new data handle = Resources_Load( string );
  return new data handle

- some time later
GameObject( something happens to me parameters )
  me.state = something.what_happens
  Renderer_ApplyState( me.render_handle, me.state.effect_type )

- some time later
Renderer_Render()
  for each renderable thing
    for each rendering back end
        setup graphics for thing.effect
        render it

- finally
GameObject_Destroy()
  Renderer_Destroy( me.render_handle )

Désolé pour ce gâchis, de toute façon vos conditions sont remplies par ce simple changement loin de la POO pure basée sur la vue de choses comme des objets du monde réel et en POO basée sur les responsabilités.

  • Aucune logique de rendu dans l'objet de jeu (fait, tout ce que l'objet sait est une poignée pour qu'il puisse appliquer des effets à lui-même)
  • Couplage lâche entre les objets du jeu et le moteur de rendu (terminé, tout contact se fait via une poignée abstraite, des états qui peuvent être appliqués et pas quoi faire avec ces états)
  • Aucun moteur de rendu qui sait tout (fait, ne sait que lui-même)
  • De préférence, la commutation d'exécution entre les moteurs de rendu (cela se fait à l'étape Renderer_Render (), vous devez cependant écrire les deux backends)

Les mots clés sur lesquels vous pouvez rechercher pour aller au-delà d'une simple refactorisation des classes seraient "système d'entité / composant" et "injection de dépendance" et potentiellement des modèles d'interface graphique "MVC" juste pour faire tourner les anciens engrenages du cerveau.

Patrick Hughes
la source
C'est extrêmement différent de tout ce que j'ai fait auparavant, on dirait qu'il a un peu de potentiel. Heureusement, je ne suis contraint par aucun moteur existant, je peux donc simplement bricoler. Je vais également rechercher les termes que vous avez mentionnés, bien que l'injection de dépendance me fasse toujours mal au cerveau: P.
Roy T.
2

Ce que j'ai fait pour mon propre moteur, c'est de tout regrouper en modules. J'ai donc ma GameObjectclasse et elle contient une poignée pour:

  • ModuleSprite - dessin de sprites
  • ModuleWeapon - tirer des fusils
  • ModuleScriptingBase - scripting
  • ModuleParticles - effets de particules
  • ModuleCollision - détection et réponse aux collisions

J'ai donc une Playerclasse et une Bulletclasse. Les deux dérivent GameObjectet sont ajoutés au Scene. Mais Playera les modules suivants:

  • ModuleSprite
  • ModuleWeapon
  • ModuleParticles
  • ModuleCollision

Et Bulleta ces modules:

  • ModuleSprite
  • ModuleCollision

Cette façon d'organiser les choses évite le "Diamant de la mort" où vous avez un Vehicle, un VehicleLandet un VehicleWateret maintenant vous voulez un VehicleAmphibious. Au lieu de cela, vous avez un Vehicleet il peut avoir un ModuleWateret un ModuleLand.

Bonus supplémentaire: vous pouvez créer des objets en utilisant un ensemble de propriétés. Tout ce que vous devez savoir est le type de base (Player, Enemy, Bullet, etc.), puis créez des poignées pour les modules dont vous avez besoin pour ce type.

Dans ma scène, je fais ce qui suit:

  • Appelez le Updatepour toutes les GameObjectpoignées.
  • Faites une vérification de collision et une réponse de collision pour ceux qui ont un ModuleCollision poignée.
  • Appelez le UpdatePostpour toutes les GameObjectpoignées pour faire connaître leur position finale après la physique.
  • Détruisez les objets dont le drapeau est défini.
  • Ajoutez de nouveaux objets de la m_ObjectsCreatedliste à la m_Objectsliste.

Et je pourrais l'organiser davantage: par modules plutôt que par objet. Ensuite, je rendais une liste de ModuleSprite, mettais à jour un tas de ModuleScriptingBaseet faisais des collisions avec une liste de ModuleCollision.

chevalier666
la source
Cela ressemble à la composition au maximum! Très agréable. Cependant, je ne vois pas beaucoup de conseils spécifiques sur le rendu. Comment gérez-vous cela, simplement en ajoutant différents modules?
Roy T.
Oh oui. C'est l'inconvénient de ce système: si vous avez une exigence spécifique pour un GameObject(par exemple un moyen de rendre un "serpent" de Sprites), vous devrez soit créer un enfant ModuleSpritepour cette fonctionnalité spécifique ( ModuleSpriteSnake), soit ajouter un nouveau module ( ModuleSnake). Heureusement, ce ne sont que des pointeurs, mais j'ai vu du code où GameObjectfaisait littéralement tout ce qu'un objet pouvait faire.
knight666