Pourquoi devrais-je séparer les objets du rendu?


11

Disclamer: Je sais ce qu'est un modèle de système d'entité et je ne l' utilise pas.

J'ai beaucoup lu sur la séparation des objets et le rendu. À propos du fait que la logique du jeu doit être indépendante du moteur de rendu sous-jacent. C'est très bien et dandy et c'est parfaitement logique, mais cela provoque aussi beaucoup d'autres douleurs:

  • besoin de synchronisation entre l'objet logique et l'objet de rendu (celui qui garde l'état de l'animation, les sprites etc)
  • besoin d'ouvrir l'objet logique au public afin que l'objet de rendu lise l'état réel de l'objet logique (ce qui conduit souvent l'objet logique à se transformer facilement en un objet getter et setter muet)

Cela ne me semble pas être une bonne solution. D'un autre côté, il est très intuitif d'imaginer un objet comme sa représentation 3D (ou 2D) et également très facile à entretenir (et peut-être aussi beaucoup plus encapsulé).

Existe-t-il un moyen de maintenir la représentation graphique et la logique de jeu couplées (en évitant les problèmes de synchronisation) mais en éloignant le moteur de rendu? Ou existe-t-il un moyen de séparer la logique de jeu et le rendu qui ne cause pas les inconvénients ci-dessus?

(peut-être avec des exemples, je ne suis pas très bon pour comprendre les discussions abstraites)


1
Il serait également utile de fournir un exemple de ce que vous voulez dire lorsque vous dites que vous n'utilisez pas le modèle de système d'entité, et de la manière dont vous pensez que cela doit être lié à la question de savoir si vous devez séparer le souci du rendu de celui de l'entité / logique de jeu.
michael.bartnett

@ michael.bartnett, je ne sépare pas les objets en petits composants réutilisables qui sont gérés par les systèmes, comme le font la plupart des implémentations des modèles. Mon code est plutôt une tentative de modèle MVC. Mais cela n'a pas vraiment d'importance car la question ne dépend d'aucun code (pas même d'une langue). J'ai mis le révélateur parce que je savais que certains auraient essayé de me convaincre d'utiliser l'ECS, qui semble guérir le cancer. Et, comme vous pouvez le voir, c'est arrivé de toute façon.
Chaussure

Réponses:


13

Supposons que vous ayez une scène composée d'un monde , d'un joueur et d'un boss. Oh, et c'est un jeu à la troisième personne, vous avez donc aussi un appareil photo .

Votre scène ressemble donc à ceci:

class Scene {
    World* world
    Player* player
    Enemy* boss
    Camera* camera
}

(Au moins, ce sont les données de base . La façon dont vous contenez les données dépend de vous.)

Vous ne voulez mettre à jour et rendre la scène que lorsque vous jouez au jeu, pas en pause ou dans le menu principal ... donc vous l'attachez à l'état du jeu!

State* gameState = new State();
gameState->addScene(scene);

Maintenant, votre état de jeu a une scène. Ensuite, vous voulez exécuter la logique sur la scène et rendre la scène. Pour la logique, vous exécutez simplement une fonction de mise à jour.

State::update(double delta) {
    scene->update(delta);
}

De cette façon, vous pouvez conserver toute la logique du jeu dans la Sceneclasse. Et juste à titre de référence, un système de composants d'entité pourrait le faire comme ceci à la place:

State::update(double delta) {
    physicsSystem->applyPhysics(scene);
}

Quoi qu'il en soit, vous avez maintenant réussi à mettre à jour votre scène. Maintenant, vous voulez l'afficher! Pour lequel nous faisons quelque chose de similaire à ce qui précède:

State::render() {
    renderSystem->render(scene);
}

Voilà. Le renderSystem lit les informations de la scène et affiche l'image appropriée. Simplifiée, la méthode de rendu de la scène pourrait ressembler à ceci:

RenderSystem::renderScene(Scene* scene) {
    Camera* camera = scene->camera;
    lookAt(camera); // Set up the appropriate viewing matrices based on 
                    // the camera location and direction

    renderHeightmap(scene->getWorld()->getHeightMap()); // Just as an example, you might
                                                        // use a height map as your world
                                                        // representation.
    renderModel(scene->getPlayer()->getType()); // getType() will return, for example "orc"
                                                // or "human"

    renderModel(scene->getBoss()->getType());
}

Vraiment simplifié, vous auriez encore besoin, par exemple, d'appliquer une rotation et une traduction en fonction de l'endroit où se trouve votre joueur et où il regarde. (Mon exemple est un jeu en 3D, si vous optez pour la 2D, ce sera une promenade dans le parc).

J'espère que c'est ce que vous cherchiez? Comme vous pouvez vous en souvenir avec ce qui précède, le système de rendu ne se soucie pas de la logique du jeu . Il utilise uniquement l'état actuel de la scène pour le rendu, c'est-à-dire qu'il en extrait les informations nécessaires pour le rendu. Et la logique du jeu? Peu importe ce que fait le moteur de rendu. Heck, il ne se soucie pas du tout s'il est affiché!

Et vous n'avez pas non plus besoin d'attacher d'informations de rendu à la scène. Il devrait suffire que le moteur de rendu sache qu'il doit rendre un orc. Vous aurez déjà chargé un modèle orc, que le moteur de rendu sait alors afficher.

Cela devrait répondre à vos besoins. La représentation graphique et la logique sont couplées , car elles utilisent toutes deux les mêmes données. Pourtant, ils sont séparés , car aucun ne dépend de l'autre!

EDIT: Et juste pour répondre pourquoi on le ferait comme ça? Parce que c'est plus facile est la raison la plus simple. Vous n'avez pas besoin de penser à "tel ou tel est arrivé, je devrais maintenant mettre à jour les graphiques". Au lieu de cela, vous faites bouger les choses et chaque image du jeu examine ce qui se passe actuellement et l'interprète d'une manière ou d'une autre, vous donnant un résultat à l'écran.


7

Votre titre pose une question différente de celle de votre contenu corporel. Dans le titre, vous demandez pourquoi la logique et le rendu doivent être séparés, mais dans le corps, vous demandez les implémentations des systèmes de logique / graphiques / rendu.

La deuxième question a été traitée précédemment , je vais donc me concentrer sur la première question.

Raisons de séparer la logique et le rendu:

  1. La notion largement répandue que les objets devraient faire une chose
  2. Et si vous voulez passer de la 2D à la 3D? Et si vous décidez de passer d'un système de rendu à un autre au milieu du projet? Vous ne voulez pas parcourir tout votre code et faire d'énormes changements au milieu de votre logique de jeu.
  3. Vous auriez probablement des raisons de répéter des sections de code, ce qui est généralement considéré comme une mauvaise idée.
  4. Vous pouvez créer des systèmes pour contrôler des pans de rendu ou de logique potentiellement énormes sans communiquer individuellement avec de petits morceaux.
  5. Et si vous voulez assigner une gemme à un joueur mais que le système est ralenti par le nombre de facettes de la gemme? Si vous avez suffisamment bien résumé votre système de rendu, vous pouvez le mettre à jour à différents taux pour tenir compte des opérations de rendu coûteuses.
  6. Cela vous permet de penser à des choses qui comptent vraiment pour ce que vous faites. Pourquoi enrouler votre cerveau autour des transformations matricielles et des décalages d'images-objets et des coordonnées d'écran lorsque tout ce que vous voulez faire est d'implémenter un mécanisme de double saut, de piocher une carte ou d'équiper une épée? Vous ne voulez pas que le sprite représente votre épée équipée en rose vif juste parce que vous vouliez le déplacer de votre main droite vers votre gauche.

Dans un cadre de POO, l'instanciation de nouveaux objets a un coût, mais d'après mon expérience, le coût des ressources système est un petit prix à payer pour la capacité de penser et de mettre en œuvre les choses spécifiques que je dois faire.


6

Cette réponse est seulement de construire une intuition de la raison pour laquelle la séparation du rendu et de la logique est importante, plutôt que de suggérer directement des exemples pratiques.

Supposons que nous ayons un gros éléphant , personne dans la pièce ne peut voir l'éléphant entier. peut-être que tout le monde est même en désaccord sur ce que c'est réellement. Parce que tout le monde voit une partie différente de l'éléphant et ne peut traiter que cette partie. Mais en fin de compte, cela ne change pas le fait qu'il s'agit d'un gros éléphant.

L'éléphant représente l'objet du jeu avec tous ses détails. Mais personne n'a réellement besoin de tout savoir sur l'éléphant (objet de jeu) pour pouvoir faire sa fonctionnalité.

Coupler la logique du jeu et le rendu, c'est en fait faire voir tout le monde à l'éléphant. Si quelque chose a changé, tout le monde doit le savoir. Alors que dans la plupart des cas, ils ont seulement besoin de voir la partie qui les intéresse uniquement. Si quelque chose a changé la personne qui en a connaissance, n'a qu'à informer l'autre personne du résultat de ce changement, c'est ce qui est le plus important pour lui. (pensez à cela comme une communication via des messages ou des interfaces).

entrez la description de l'image ici

Les points que vous avez mentionnés ne sont pas des inconvénients, ils ne sont des inconvénients que s'il y avait plus de dépendances qu'il ne devrait y en avoir dans le moteur, en d'autres termes, les systèmes voient les parties de l'éléphant plus qu'ils ne devraient avoir. Et cela signifie que le moteur n'a pas été "correctement" conçu.

Vous n'avez besoin de synchronisation avec sa définition formelle que si vous utilisez un moteur multithread où il place la logique et le rendu dans deux threads différents, et même un moteur qui nécessite beaucoup de synchronisation entre les systèmes n'est pas particulièrement bien conçu.

Sinon, la façon naturelle de traiter un tel cas est de concevoir le système comme entrée / sortie. La mise à jour fait la logique et génère le résultat. Le rendu est uniquement alimenté avec les résultats de la mise à jour. Vous n'avez pas vraiment besoin de tout exposer. Vous exposez uniquement une interface qui communique entre les deux étapes. La communication entre les différentes parties du moteur doit se faire via des abstractions (interfaces) et / ou des messages. Aucune logique ou état interne ne doit être exposé.

Prenons un exemple de graphique de scène simple pour expliquer l'idée.

La mise à jour se fait généralement via une seule boucle appelée boucle de jeu (ou éventuellement via plusieurs boucles de jeu, chacune s'exécutant dans un thread séparé). Une fois la boucle mise à jour, jamais l'objet de jeu. Il suffit de dire via la messagerie ou les interfaces que les objets 1 et 2 ont été mis à jour et de l'alimenter avec la transformation finale.

Le système de rendu ne prend que la transformation finale et ne sait pas ce qui a réellement changé sur l'objet (par exemple, une collision spécifique s'est produite, etc.). Maintenant, pour rendre cet objet, il n'a besoin que de l'ID de cet objet et de la transformation finale. Après cela, le rendu alimentera l'api de rendu avec le maillage et la transformation finale sans rien savoir d'autre.

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.