-
-
Notifications
You must be signed in to change notification settings - Fork 81
(FR) Tutoriel: [01] Hello World
Ce tutoriel vous expliquera comment concevoir en quelques lignes à peine une application fenêtrée avec Nazara, disposant d'un monde régi par un ECS ainsi qu'un élément graphique.
Nazara est un moteur modulaire, et le module que nous allons utiliser ici est NazaraGraphics, nous aurons donc besoin de celui-ci et ses dépendances (NazaraCore, NazaraUtility, NazaraPlatform et NazaraRenderer).
Avec xmake, ceci suffirait :
add_repositories("nazara-repo https://github.com/NazaraEngine/xmake-repo.git")
add_requires("nazaraengine", { debug = is_mode("debug") })
add_rules("mode.debug", "mode.release")
set_runtimes(is_mode("debug") and "MDd" or "MD")
set_languages("c++20")
add_defines("NAZARA_ENTT")
target("NazaraProject")
add_files("src/main.cpp")
add_packages("nazaraengine", { components = {"graphics"} })
Nous partirons de ce code de base, courant en C++ :
#include <iostream>
int main()
{
return EXIT_SUCCESS;
}
Pour commencer, nous allons instancier un objet de type Nz::Application
(du module Core), celui-ci est template et nous pouvons lui indiquer les modules que nous comptons utiliser pour qu'il les initialise, ici Nz::Graphics
(du module Graphics).
#include <Nazara/Core.hpp>
#include <Nazara/Graphics.hpp>
#include <iostream>
int main()
{
Nz::Application<Nz::Graphics> app;
return app.Run();
}
Nz::Application
est également la classe qui va initialiser et libérer le moteur, ainsi que s'occuper de faire tourner l'application du début à la fin.
La façon la plus simple de l'utiliser (mais pas la seule) consiste à appeler sa méthode Run
dès que tous les préparatifs sont effectués.
Cette méthode va faire tourner une boucle infinie, si vous exécutez le code maintenant vous risquez de voir un coeur de processeur tourner à fond.
De base, une application ne fait pas grand chose, mais nous allons pouvoir lui rajouter des composants (ApplicationComponent
), ceux-ci permettent de remplir les différentes fonctions dont nous avons besoin, encore une fois de façon modulaire.
Dans cet exemple, comme nous souhaitons ouvrir une fenêtre, nous allons rajouter un WindowingAppComponent
et lui demander d'en ouvrir une pour nous.
Rajoutez le header #include <Nazara/Platform/WindowingAppComponent.hpp>
pour y avoir accès et rajoutez ceci avant le return
:
auto& windowing = app.AddComponent<Nz::WindowingAppComponent>();
Ceci rajoute un composant applicatif permettant de s'occuper du fenêtrage et de récupérer une référence dessus. C'est via ce composant que nous allons pouvoir gérer toutes les fenêtres (ou plus souvent, l'unique fenêtre) de notre application.
La prochaine étape qui nous intéresse consiste à créer une fenêtre, cela se fait aisément en utilisant le composant :
Nz::Window& mainWindow = windowing.CreateWindow(Nz::VideoMode(1280, 720), "Tut01 - Hello world");
Cette ligne va créer la fenêtre en question en précisant des paramètres comme le mode vidéo (définissant la taille de rendu de la fenêtre, c'est-à-dire sans compter les bordures), ainsi que son titre et optionnellement les flags de style (présence de bordure, possibilité de la redimensionner, etc.).
Nous obtenons donc le code suivant :
#include <Nazara/Core.hpp>
#include <Nazara/Graphics.hpp>
#include <Nazara/Platform/WindowingAppComponent.hpp>
#include <iostream>
int main()
{
Nz::Application<Nz::Graphics> app;
auto& windowing = app.AddComponent<Nz::WindowingAppComponent>();
Nz::Window& mainWindow = windowing.CreateWindow(Nz::VideoMode(1280, 720), "Tut01 - Hello world");
return app.Run();
}
Normalement en exécutant ce bout de code, vous devriez voir apparaitre une fenêtre de la taille et du titre demandés, que vous pouvez redimensionner, déplacer, et fermer (ce qui ferma l'application). Tout ceci est grâce au Nz::WindowingAppComponent
.
Évidemment, peu de gens se contentent d'une fenêtre vide, et nous allons vouloir obtenir un rendu dedans, et pour ce faire nous allons rajouter un "monde" pour gérer une caméra et des entités de façon plus générale.
Dans la terminologie utilisée par Nazara, un monde est l'endroit où existent et évoluent les entités.
Pour tout ceci, le moteur s'intègre facilement avec EnTT (attention à bien utiliser la même version que celle du moteur - vérifiez les releases).
EnTT est un ECS (Entity-Component-System), qui permet de gérer des entités (vides) à laquelles on peut rattacher à la volée des composants qui vont être gérés par des systèmes, il s'agit d'une façon très populaire de gérer les entités dans le monde du jeu vidéo.
Note: Il est tout à fait possible d'utiliser le moteur avec une autre bibliothèque qu'EnTT via l'interface plus bas niveau du moteur, ce n'est pas exposé dans ce tutoriel pour des mesures de simplicité.
Commençons par rajouter le composant gérant les mondes à notre application, Nz::EntitySystemAppComponent
, et demandons-lui ensuite de nous créer un monde utilisant EnTT, un Nz::EnttWorld
.
auto& ecs = app.AddComponent<Nz::EntitySystemAppComponent>();
auto& world = ecs.AddWorld<Nz::EnttWorld>();
Nous pouvons de cette façon créer plusieurs mondes dans une même application, mais cela est rarement nécessaire et nous pouvons nous contenter d'un seul pour le moment.
Notre monde est pour l'instant vide, nous allons lui ajouter un système.
Dans un ECS, un système est notamment responsable de la mise à jour des entités, ils représentent la partie rendu, physique, audio, etc. de notre scène.
Comme nous souhaitons afficher quelque chose sur notre scène, nous allons ajouter le Nz::RenderSystem
, responsable de toute la partie affichage.
auto& renderSystem = world.AddSystem<Nz::RenderSystem>();
À ce stade, rien ne change, il ne nous manque plus que deux choses pour obtenir le rendu tant désiré :
- Préparer la fenêtre pour recevoir un rendu.
- Créer une caméra
Pour la première étape, nous allons devoir créer une swapchain pour notre fenêtre, derrière ce mot compliqué se cache en réalité simplement la surface de rendu que la carte graphique va pouvoir utiliser pour afficher quelque chose. En clair c'est une façon de rendre visible notre fenêtre au GPU.
C'est aussi simple que de rajouter cette ligne:
Nz::WindowSwapchain& windowSwapchain = renderSystem.CreateSwapchain(mainWindow);
Mais pour obtenir un rendu, il nous faut avant tout préciser le point de vue de notre monde, ce qui permettra au RenderSystem de savoir quoi afficher et où l'afficher.
Nous allons donc maintenant créer la caméra, qui va être une entité de notre monde.
entt::handle cameraEntity = world.CreateEntity();
Félicitations, vous avez votre première entité !
Note : dans un ECS tel qu'EnTT, chaque entité n'est rien de plus qu'un identifiant numérique unique, et il nous faudrait passer par le monde, entt::registry
, pour la manipuler. entt::handle
est une petite surcouche permettant de nous simplifier la vie avec des fonctions membres.
Notre entité nouvellement créée ne contient absolument rien à part un identifiant unique. Pour faire en sorte qu'elle nous serve de caméra, nous devons lui rajouter des composants pour stocker des informations et permettre aux systèmes de les manipuler.
Le premier composant qui nous intéresse (pour une grande partie des entités) est le NodeComponent
, celui-ci donne une position/rotation/etc. à notre entité (et nous permet même de l'attacher à d'autres entités). Dans notre cas nous allons laisser la caméra à sa position par défaut (en zéro).
Nous pouvons donc simplement ajouter une position à notre caméra comme ceci:
cameraEntity.emplace<Nz::NodeComponent>();
Et pour vraiment en faire une entité digne du nom de caméra, ajoutons-lui maintenant un CameraComponent
, contrairement au NodeComponent
celui-ci nécessite des paramètres de construction, à savoir la cible du rendu.
Il va nous falloir un objet faisant le pont entre notre caméra et notre swapchain (RenderTarget), nous aurons ici besoin d'un objet RenderWindow
, qui est un objet léger permettant le rendu sur une swapchain (il existe également RenderTexture
permettant de faire ce qu'on appelle du Render-To-Texture, mais ce sera pous la suite).
// Construction d'un objet dérivé de Nz::RenderTarget dans un pointeur intelligent
std::make_shared<Nz::RenderWindow>(windowSwapchain)
(si vous n'êtes pas familier avec les pointeurs intelligents, ce n'est pas grave, passez à la suite)
Le second paramètre est le type de projection utilisée (si ce terme vous parait obscur, sachez qu'on utilise le plus généralement une projection orthographique en 2D et une projection de type perspective en 3D).
En résumé, ajoutez donc ceci.
auto& cameraComponent = cameraEntity.emplace<Nz::CameraComponent>(std::make_shared<Nz::RenderWindow>(windowSwapchain), Nz::ProjectionType::Orthographic);
cameraComponent.UpdateClearColor(Nz::Color(0.46f, 0.48f, 0.84f, 1.f));
Si vous n'étiez pas familiers avec les ECS, vous voici en plein dedans ! Ils permettent notamment une très grande flexibilité, chaque entité pouvant être associées à tous les composants désirés en fonction des comportements souhaités.
La première ligne rajoute un CameraComponent
à notre entité en lui passant les paramètres évoqués plus haut et récupère une référence dessus.
La seconde ligne change une propriété de notre caméra pour que nous ayons un fond plus beau que du noir.
Et voilà ce que vous devriez obtenir à ce stade :
#include <Nazara/Core.hpp>
#include <Nazara/Graphics.hpp>
#include <Nazara/Platform/WindowingAppComponent.hpp>
#include <Nazara/Renderer.hpp>
#include <Nazara/TextRenderer.hpp>
#include <iostream>
int main()
{
// Mise en place de l'application, de la fenêtre et du monde
Nz::Application<Nz::Graphics> app;
auto& windowing = app.AddComponent<Nz::WindowingAppComponent>();
Nz::Window& mainWindow = windowing.CreateWindow(Nz::VideoMode(1280, 720), "Tut01 - Hello world");
auto& ecs = app.AddComponent<Nz::EntitySystemAppComponent>();
auto& world = ecs.AddWorld<Nz::EnttWorld>();
auto& renderSystem = world.AddSystem<Nz::RenderSystem>();
Nz::WindowSwapchain& windowSwapchain = renderSystem.CreateSwapchain(mainWindow);
// Création de la caméra
entt::handle cameraEntity = world.CreateEntity();
{
cameraEntity.emplace<Nz::NodeComponent>();
auto& cameraComponent = cameraEntity.emplace<Nz::CameraComponent>(std::make_shared<Nz::RenderWindow>(windowSwapchain), Nz::ProjectionType::Orthographic);
cameraComponent.UpdateClearColor(Nz::Color(0.46f, 0.48f, 0.84f, 1.f));
}
return app.Run();
}
Note : remarquez que j'ai mis le paramétrage de la caméra dans son propre bloc (scope), ceci est pour isoler les références récupérées sur les composants de nos entités, car ils peuvent s'invalider très vite dans un ECS. Ne stockez donc jamais une référence sur un composant dans un ECS, à la place récupérez-la avec la méthode .get<Type>();
au moment où vous en avez besoin.
Que de lignes pour obtenir une simple couleur me direz-vous ! Mais toute cette complexité se justifie par le fait que nous avons préparé notre application à gérer des entités et rendre quelque chose en utilisant la carte graphique, car cette couleur vient bien du GPU qui l'affiche ensuite dans notre fenêtre.
Nous avons presque terminé. Maintenant que nous avons de quoi afficher, il ne nous manque que quelque chose à afficher !
Pour perpétuer la belle tradition des hello worlds, nous allons afficher à l'écran du texte.
Pour ce faire, nous aurons besoin de deux nouvelles classes dédiées à la gestion des textes d'un point de vue graphique. Rassurez-vous, elles sont plutôt simples :
- Nz::SimpleTextDrawer : Le dessinateur, c'est cette classe qui est responsable de positionner chaque lettre au bon endroit, de leur appliquer une couleur, une taille, etc.
- Nz::TextSprite : Le sprite textuel, c'est lui qui va afficher le texte que nous lui aurons associé.
Note : SimpleTextDrawer
n'est qu'une implémentation d'un TextDrawer, faite pour gérer un texte d'une même police, couleur, taille et style. Plus tard nous verrons la classe RichTextDrawer
, faite pour mélanger plusieurs polices, styles, etc. sur un même texte.
Commençons par décrire notre texte à l'aide d'un SimpleTextDrawer
:
Nz::SimpleTextDrawer textDrawer;
textDrawer.SetText("Hello world !");
textDrawer.SetCharacterSize(72);
textDrawer.SetTextOutlineThickness(4.f);
Ici nous décrivons un texte "Hello world !" de taille 72 pixels, avec une bordure de 4 pixels. L'ordre des appels n'a ici aucune importance.
N'hésitez pas à vous amuser à changer diverses propriétés pour votre texte pour expérimenter.
Pour afficher ce texte, nous allons maintenant créer un TextSprite
et lui donner notre texte :
std::shared_ptr<Nz::TextSprite> textSprite = std::make_shared<Nz::TextSprite>();
textSprite->Update(textDrawer);
La première ligne instancie un TextSprite via un shared_ptr (c'est important), et la seconde lui dit de se mettre à jour par rapport à notre drawer pour dessiner le texte et recevoir le résultat.
Note : Un TextDrawer n'a pas besoin de vivre aussi longtemps que le TextSprite, à vrai dire le TextDrawer n'est utilisé par le TextSprite qu'au moment de l'appel à TextSprite::Update
. Mais cela peut être utile de garder votre TextDrawer si vous souhaitez appliquer des modifications par la suite, notre TextSprite ne possède que des informations brutes pour la carte graphique ; les informations de couleur, de police et même le texte affiché ne peuvent être récupérés qu'au niveau du TextDrawer
.
Maintenant que nous avons quelque chose à afficher, il nous reste une dernière étape : créer une entité pour afficher notre texte, vous connaissez le début :
entt::handle textEntity = world.CreateEntity();
auto& nodeComponent = textEntity.emplace<Nz::NodeComponent>();
Nous gardons une référence sur le NodeComponent, elle nous sera utile pour la suite.
Le composant que nous allons maintenant rajouter est celui servant à l'affichage, c'est-à-dire un GraphicsComponent
. Ce composant très simple est un point d'ancrage sur lequel nous pouvons attacher la plupart des choses pouvant être affichées à l'écran, qui font partie de la catégorie des représentables instanciés (un même objet pouvant être attachés à un nombre illimité d'entités, nous verrons tout ceci plus en détail dans un autre tutoriel), dont le TextSprite
fait partie (et la raison pour laquelle nous avons utilisé un std::shared_ptr
.
Rajoutez donc ce code :
auto& gfxComponent = textEntity.emplace<Nz::GraphicsComponent>();
gfxComponent.AttachRenderable(textSprite);
Et observez le résultat !
#include <Nazara/Core.hpp>
#include <Nazara/Graphics.hpp>
#include <Nazara/Platform/WindowingAppComponent.hpp>
#include <Nazara/Renderer.hpp>
#include <Nazara/TextRenderer.hpp>
#include <iostream>
int main()
{
// Mise en place de l'application, de la fenêtre et du monde
Nz::Application<Nz::Graphics> app;
auto& windowing = app.AddComponent<Nz::WindowingAppComponent>();
Nz::Window& mainWindow = windowing.CreateWindow(Nz::VideoMode(1280, 720), "Tut01 - Hello world");
auto& ecs = app.AddComponent<Nz::EntitySystemAppComponent>();
auto& world = ecs.AddWorld<Nz::EnttWorld>();
auto& renderSystem = world.AddSystem<Nz::RenderSystem>();
Nz::WindowSwapchain& windowSwapchain = renderSystem.CreateSwapchain(mainWindow);
// Création de la caméra
entt::handle cameraEntity = world.CreateEntity();
{
cameraEntity.emplace<Nz::NodeComponent>();
auto& cameraComponent = cameraEntity.emplace<Nz::CameraComponent>(std::make_shared<Nz::RenderWindow>(windowSwapchain), Nz::ProjectionType::Orthographic);
cameraComponent.UpdateClearColor(Nz::Color(0.46f, 0.48f, 0.84f, 1.f));
}
// Création d'un texte
Nz::SimpleTextDrawer textDrawer;
textDrawer.SetText("Hello world !");
textDrawer.SetCharacterSize(72);
textDrawer.SetTextOutlineThickness(4.f);
std::shared_ptr<Nz::TextSprite> textSprite = std::make_shared<Nz::TextSprite>();
textSprite->Update(textDrawer);
entt::handle textEntity = world.CreateEntity();
{
auto& nodeComponent = textEntity.emplace<Nz::NodeComponent>();
auto& gfxComponent = textEntity.emplace<Nz::GraphicsComponent>();
gfxComponent.AttachRenderable(textSprite);
}
return app.Run();
}
Enfin quelque chose à l'écran !
Le point d'origine du texte (et des sprites en général) étant le repère en bas à gauche, notre texte se retrouve collé au coin de la fenêtre. Voyons si nous pouvons arranger ça.
Rajoutez ce dernier bout de code après avoir attaché le texte au GraphicsComponent :
Nz::Boxf textBox = textSprite->GetAABB();
Nz::Vector2ui windowSize = mainWindow.GetSize();
nodeComponent.SetPosition({ windowSize.x / 2 - textBox.width / 2, windowSize.y / 2 - textBox.height / 2 });
Ce qu'il fait est clair, nous récupérons l'AABB (Axis-Aligned Bounding Box) du composant graphique de notre entité, c'est-à-dire la 'boite' qui l'englobe (et qui contient donc sa taille). Nous nous servons ensuite des dimensions de notre texte et de la fenêtre pour positionner le texte exactement au centre de celle-ci.
Avec notre code final :
#include <Nazara/Core.hpp>
#include <Nazara/Graphics.hpp>
#include <Nazara/Platform/WindowingAppComponent.hpp>
#include <Nazara/Renderer.hpp>
#include <Nazara/TextRenderer.hpp>
#include <iostream>
int main()
{
// Mise en place de l'application, de la fenêtre et du monde
Nz::Application<Nz::Graphics> app;
auto& windowing = app.AddComponent<Nz::WindowingAppComponent>();
Nz::Window& mainWindow = windowing.CreateWindow(Nz::VideoMode(1280, 720), "Tut01 - Hello world");
auto& ecs = app.AddComponent<Nz::EntitySystemAppComponent>();
auto& world = ecs.AddWorld<Nz::EnttWorld>();
auto& renderSystem = world.AddSystem<Nz::RenderSystem>();
Nz::WindowSwapchain& windowSwapchain = renderSystem.CreateSwapchain(mainWindow);
// Création de la caméra
entt::handle cameraEntity = world.CreateEntity();
{
cameraEntity.emplace<Nz::NodeComponent>();
auto& cameraComponent = cameraEntity.emplace<Nz::CameraComponent>(std::make_shared<Nz::RenderWindow>(windowSwapchain), Nz::ProjectionType::Orthographic);
cameraComponent.UpdateClearColor(Nz::Color(0.46f, 0.48f, 0.84f, 1.f));
}
// Création d'un texte
Nz::SimpleTextDrawer textDrawer;
textDrawer.SetText("Hello world !");
textDrawer.SetCharacterSize(72);
textDrawer.SetTextOutlineThickness(4.f);
std::shared_ptr<Nz::TextSprite> textSprite = std::make_shared<Nz::TextSprite>();
textSprite->Update(textDrawer);
entt::handle textEntity = world.CreateEntity();
{
auto& nodeComponent = textEntity.emplace<Nz::NodeComponent>();
auto& gfxComponent = textEntity.emplace<Nz::GraphicsComponent>();
gfxComponent.AttachRenderable(textSprite);
Nz::Boxf textBox = textSprite->GetAABB();
Nz::Vector2ui windowSize = mainWindow.GetSize();
nodeComponent.SetPosition({ windowSize.x / 2 - textBox.width / 2, windowSize.y / 2 - textBox.height / 2 });
}
return app.Run();
}