Skip to content
Snippets Groups Projects
Théau's avatar
BATON Theau authored
6b5ae8c1
History

Projet Fondamantaux de la Programmation Graphique (F.I.G.)

Étudiant

  • BATON Théau

Introduction

Ce document décrit la conception et le développement des différents moteurs et des couches qui composent le jeu. Il est important de noter que tout ce qui est rendu graphiquement se base sur une abstraction OpenGL que j'ai développée et que je n'ai pas utilisé de bibliothèque graphique de plus haut niveau qu'OpenGL. J'aurais beaucoup aimé réaliser le moteur graphique en me basant sur Vulkan, mais mes connaissances et le temps ont manqué.

Architetcure

Pour réaliser mon jeu, j'ai conçu une architecture qui sépare en différentes parties les composantes du logiciel. L'architecture du jeu se divise en 3 différentes couches, chaque couche est quant à elle composée de 2 autres parties : la partie avant et la partie arrière. Le but de la couche avant a pour but d'offrir des classes à une plus grosse couche située au-dessus, là où la couche arrière a pour but d'offrir des classes pour la couche avant en se basant sur une plus grosse couche en dessous.

Couche Moteur

La couche moteur contient des classes qui permettent de gérer certaines choses assez bas niveau par rapport aux jeux, par exemple, le moteur graphique permet de dessiner à l'écran des objets assez rudimentaires comme une image. Le moteur physique va plutôt essayer de détecter et stocker les collisions entre différents objets.

Partie Arrière

La partie arrière du moteur graphique offre plein de classes pour "jouer" avec OpenGL; par exemple, on y trouve les classes qui permettent de manipuler les buffers (V.A.O., V.B.O, E.B.O, F.B.O.), les textures ou encore les shaders programs. La partie arrière du moteur physique va fournir des classes Position et SquareBox qui se veulent assez générales.

Partie Avant

La partie avant a pour but de concevoir des objets en se basant sur la partie arrière. Pour le moteur graphique, la partie avant possède des objets tels que des Image, Sprite, ... Ces objets sont indépendants entre eux et héritent uniquement de classes géométriques qui servent à décrire la manière dont les primitives sont envoyées à la carte graphique. La technique pour dessiner chacun de ces objets est présente dans les Modules qui ont pour responsabilité de dessiner certains types d'objets d'une certaine manière. Pour permettre une telle souplesse, j'ai utilisé une technique qui s'appelle l'effacement de type. Je reviendrai plus tard sur son fonctionnement et son utilisation. D'autres classes sont présentes, comme celle du moteur graphique ou du renderer, mais je reviendrai sur leur importance et dans quel ordre ces objets interviennent pour afficher des choses à l'écran. Pour le moteur physique, la partie avant possède des classes qui permettent de détecter la collision entre elle ainsi qu'un moteur qui a pour but de regarder et de stocker les collisions entre les objets physiques.


Les classes de la couche moteur sont présentes dans :

/engine/graphics/
/engine/physic/

Couche Noyaux

La couche noyau (kernel) a pour but d'abstraire les moteurs et d'offrir une interface sur laquelle on pourra construire le jeu. Ici, aucun choix en lien direct avec le jeu ne doit être pris pour s'assurer que nous pourrons construire ce qui nous plait à la couche supérieure.

Partie Arrière

La partie arrière du noyau tente d'abstraire du mieux possible les moteurs. C'est pour cela que des interfaces Components apparaissent, ces dernières permettent de représenter une composante physique ou graphique d'un futur objet pour la couche supérieure. On trouve aussi les Linkers qui sont présents uniquement pour les composants graphiques et qui permettent d'envoyer des objets de manière groupée au moteur graphique pour éviter un nombre d'appels de dessin trop important et ainsi obtenir un gain de performance.

Partie Avant

On retrouve ici des implémentations des interfaces de components présents à la couche inférieure. Cette implémentation a pour but de simplifier l'utilisation de certains objets présents dans les moteurs. Par exemple, on voudrait éviter de devoir manipuler directement les U.V. pour un sprite, du coup ici on crée une interface à sprite qui simplifie l'utilisation de la classe sprite du moteur graphique. On intègre aussi le concept de Props (nom inspiré par le moteur Source), qui sont des objets ayant pour but de réunir des composants physiques et graphiques. Par exemple, un PropsStatic est composé d'un component graphique Sprite et d'un component physique Fixed. Dans la partie avant, on trouve aussi les Resolver. Cet objet a pour but d'appeler à intervalles réguliers certaines méthodes, telles que la mise à jour physique ou graphique des composants d'un Props. Il ne reste plus que la classe Kernel qui va stocker des Props, initialiser et faire tourner les moteurs, appeler les Resolver.


Les classes de la couche noyaux sont présentes dans :

/kernel/

Couche Jeux

La couche jeux est la couche où l'on va créer nos mécaniques pour le jeu. La couche jeux est aussi la moins élaborée pour des raisons de temps 😞.

Partie Arrière

La partie arrière a pour but de récupérer les class props préfaites dans le noyau et d'expliquer ce que peut être un Enemy, un Player ou encore la manière dont sont représentés les dégâts dans le jeu. Tout cela en restant assez général et en laissant à la partie supérieure décider de la manière dont les objets vont interagir entre eux lors d'une collision par exemple.

Partie Avant

Ici, on code les mécaniques du jeu, par exemple comment un Player prend des dégâts ou quelle statistique compose une entité dans le jeu.


Les classes de la couche jeu sont présentes dans :

/game/

Jeux

À la base, je voulais partir sur un moteur isométrique (on peut même encore trouver des classes qui permettent de décrire la manière dont les objets doivent être graphiquement organisés pour un rendu isométrique). Mais par manque de ressources, je suis partie sur une approche 2D en vue de dessus. Néanmoins, voici des rendus créés à l'aide de la partie arrière du moteur graphique uniquement :

Le jeu consiste juste en un personnage qui se promène sur une plage avec des obstacles, il n'y a pas d'ennemis et pas d'objets, mais il est important de noter que toutes les classes nécessaires à l'élaboration d'ennemis ou d'objets sont présentes et pourraient être réalisées avec plus de temps.

Programmation Graphique

Dans cette partie, je vais revenir sur le fonctionnement du moteur graphique, en d'autres mots, comment je dessine des trucs à l'écran en assurant une perte de performance minimale.
L'idée, lorsque l'on fait un jeu 2D, est d'utiliser de l'Instance Rendering qui est une technique qui permet de dessiner en 1 appel de dessin (drawcall) plusieurs fois la même géométrie. On utilise cette technique plutôt que du Batch Rendering car cette dernière offre plus de performance à long terme pour les jeux 2D et est plus simple à mettre en place.
Pour ce faire, j'ai divisé la partie moteur de plusieurs familles de classes qui ont chacune leurs responsabilités dans le rendu d'objet.

Geometry

Les classes de geometry servent uniquement à savoir quelle forme vont prendre les objets. Par exemple, la classe Quads permet de récupérer la geometry d'un quad. Une géométrie possède les attributs de position, mais peut aussi posséder les normales ou les coordonnées UV. C'est une classe contenant donc les données géométriques qui vont être envoyées à la carte graphique par l'intermédiaire d'une classe Vertex Array. À noter qu'il existe 2 types de géométries : les géométries classiques et les Static. Les géométries dites statiques sont celles connues à l'avance, par exemple, la géométrie d'un cube simple et connu à l'avance et qui peut donc être hardcodée pour gagner en espace mémoire à l'exécution. Un maillage tiré d'un fichier .obj ne peut pas être connu avant le lancement du programme et ne peut donc pas être considéré comme static.

Objet

Chaque objet hérite d'une géométrie et est composé de différents éléments qui permettent de les représenter (tels que des images ou des transformations). Il est important de constater que les objets ne possèdent aucune méthode ou information pour les dessiner. En effet, d'autres classes sont prévues à cet effet.

Module

Les modules ont la responsabilité de dessiner des objets. Ils sont composés de toutes les classes qui permettent d'effectuer un rendu et d'une méthode qui effectue le rendu d'un objet ou de plusieurs objets. L'utilisation de l'instance rendering s'effectue dans ces classes.

Moteur Graphique

Le moteur graphique stocke une liste de couches qui vont être chacune rendues dans un frame buffer différent pour enfin être superposées dans le frame buffer qui sera affiché à l'écran. Cette technique permet de séparer le rendu en différentes couches pour certaines utilisations. Chaque couche possède alors son propre frame buffer, son propre renderer et sa propre liste d'objets à rendre.

Renderable

La classe Renderable permet d'abstraire un objet qui doit être dessiné. Chaque Renderable lie un module avec un objet ou une liste d'objets. L'objet ou la liste d'objets ne possède aucune méthode liée à la manière dont ils doivent être dessinés, c'est au module qu'incombe cette tâche. La classe Renderable peut être vue comme un conteneur qui lie un module à des objets. Pour stocker ces objets de manière correcte dans le renderable tout en possédant une méthode pour les dessiner, j'ai utilisé une technique nommée "l'Effacement De Type", dont voici la source :

Type Erasure Inspiration : Type Erasure - CppCon 2021 - Klaus Iglberger

En résumé, cette technique combine différents design patterns pour permettre de dessiner des objets sans que ces derniers aient de méthode ou de fonction en lien avec le dessin. Cela permet aussi d'avoir une méthode uniforme pour dessiner tous ces objets. (ext_render(...)).

En résumer :

Pour résumer cette partie, voici une liste des méthodes appelées pour dessiner des trucs à l'écran en faisant intervenir tout un tas d'objets ayant chacun leur importance dans le processus de rendu :

  • GraphicEngine::step(...) Pour Chaque Couche à dessiner :
    • Layer::draw()
      • Renderer::render(...)
        • ext_render(...) [Effacement De Type]
  • Supperposition des couches rendu fait par les layers.
  • Window::swapBuffers()

Construction

Pour construire l'application :

Debug mode:

cmake --build ./build

Release mode:

cmake --build ./build --config Release

Clean target:

cmake --build ./build --target clean

Bibliothèque utilisé :

Conlusion

Cet exercice m'a remontré que créer un jeu vidéo depuis presque 0 est un exercice extrêmement complexe. Cela m'a néanmoins permis de comprendre ce que j'ai dans cet exercice et plus généralement dans la programmation. C'est-à-dire la programmation bas niveau, l'optimisation et la gestion de la mémoire et la conception. C'est peut-être pour cela que la partie moteur et kernel du projet sont beaucoup plus étoffées que le jeu 🤔. Néanmoins, grâce à ce projet, j'ai pu apprendre beaucoup de choses en matière de conception. Je n'ai pas pu aller très loin dans la réalisation du jeu, malgré que toutes les classes pour le faire soient présentes. J'aurais aimé avoir plus de temps pour peaufiner certains détails, mais cela n'aurait qu'un cercle vicieux dans lequel je me serais retrouvé à vouloir peaufiner chaque aspect du jeu et reviendrait à un travail de plusieurs années.

Le Jeux

Je voulais faire un jeu où il fallait défendre la plage de plusieurs ennemis, mais je n'ai pas eu le temps de coder ces dits ennemis, de coder l'animation du personnage qui se déplace et qui se bat.

Toute ressemblance avec un certain Zelda : Oracle of Seasons est fortuite.

Ressource