Bonsoir.
Comme certains le savent déjà, je suis l'auteur d'un moteur de jeu en C++ (Nazara Engine) et actuellement en pleine conception.
Actuellement le moteur est tout à fait capable de gérer la 3D à travers son ECS, voici comment ça fonctionne pour la 3D actuellement:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | Ndk::World world; // Caméra Ndk::EntityHandle cameraEntity = world.CreateEntity(); auto& camera = cameraEntity->AddComponent<Ndk::CameraComponent>(); auto& cameraNode = cameraEntity->AddComponent<Ndk::NodeComponent>(); // Vaisseau NzModelRef spaceshipModel = NzModel::New(); spaceshipModel->LoadFromFile("resources/Spaceship/spaceship.obj"); Ndk::EntityHandle spaceshipEntity = world.CreateEntity(); spaceshipEntity->AddComponent<Ndk::NodeComponent>(); auto& graphicsComponent = spaceshipEntity->AddComponent<Ndk::GraphicsComponent>(); graphicsComponent.Attach(spaceshipModel); // Boucle de rendu (pseudo-code) while (...) world.Update(elapsedTime); |
Le code est relativement simple à comprendre:
- Création du "monde" (contenant des systèmes de base, notamment pour le rendu)
- Création et chargement d'un modèle de vaisseau (le maillage, les matériaux/textures associés)
- Création d'une entité
- Attachement d'un NodeComponent à l'entité (pour lui permettre d'avoir une position/rotation etc.)
- Attachement d'un GraphicsComponent à l'entité, sur lequel on attache le modèle de vaisseau.
Et voilà, nous avons une entité affichée par le moteur située quelque part dans le monde, qui sera affichée lors de la mise à jour des systèmes.
L'affichage se fait par le RenderSystem qui va itérer sur les entités possédant un CameraComponent, triées par leur membre "layer index" (permettant de spécifier l'ordre du rendu), ensuite il y a itération sur les entités possédant un GraphicsComponent pour l'affichage.
Ce qui donne en pseudo-code ceci:
1 2 3 4 5 6 7 8 9 10 | void RenderSystem::OnUpdate(float elapsedTime) { for (const Ndk::EntityHandle& camera : m_cameras) { for (const Ndk::EntityHandle& drawable : m_drawables) AddToRenderQueue(); Draw(); } } |
Voilà pour le fonctionnement actuel.
Mon problème:
Le problème intervient lorsque je veux rajouter de la 2D à tout ceci.
Alors tout d'abord, le moteur ne fait aucune distinction entre la 2D et la 3D, il ne voit que différentes primitives et s'occupe de les afficher comme on le lui demande.
De fait, un sprite, du texte, un modèle, tout ceci appartient autant à la 3D qu'à la 2D dans Nazara.
Du coup, pour passer de la 3D à la 2D, il n'y a qu'un changement à faire, c'est changer le type de projection, chose qui se fait très simplement (par exemple avec une seconde caméra:)
1 2 3 4 5 | Ndk::EntityHandle view = world.CreateEntity(); view->AddComponent<Ndk::NodeComponent>(); auto& viewCamera = view->AddComponent<Ndk::CameraComponent>(); viewCamera.SetProjectionType(nzProjectionType_Orthogonal); // "Rendu 2D" |
Mon véritable problème est que le RenderSystem va traiter les mêmes entités en 2D qu'en 3D, ce n'est certainement pas ce qui est voulu.
Il me faut donc trouver un moyen de séparer les deux, plusieurs solutions s'offrent alors à moi:
- Ne rien changer, faire en sorte qu'un second World doive être utilisé pour le rendu 2D, entraînant une séparation totale entre les entités/systèmes, mais pas au niveau des "InstancedRenderable" (sprites, modèles, etc.). Le problème de cette solution est d'être contraignante pour l'utilisateur (mais néanmoins parfaitement logique).
-
Ajouter un Bitset au niveau du GraphicsComponent permettant d'indiquer les layers de visibilité:
1 2 3 4 5 6
Ndk::EntityHandle cameraEntity = world.CreateEntity(); auto& camera = cameraEntity->AddComponent<Ndk::CameraComponent>(); camera.SetLayer(1); auto& graphicsComponent = spaceshipEntity->AddComponent<Ndk::GraphicsComponent>(); graphicsComponent.EnableVisibility(1, true); // bitset en interne
Cette solution ne me plaît pas forcément car elle se base sur le fait que l'indice de couche des caméras ne variera pas, et puis que se passe-t-il quand je veux rajouter une énième caméra, mes entités seront-elles par défaut visibles ou invisible pour celle-ci ? Sans oublier que ça risque d'être chiant à changer par la suite.
En bref, ça ne facilite pas vraiment la vie de l'utilisateur. -
Un autre système de visibilité (comme un champs indépendant visibilité, pour former des groupes de visibilités, ou autre).
Ceci n'est cependant que la première partie de mon problème, voici la seconde:
Le repère
Un autre problème quand on mélange 2D/3D est au niveau des "renderables" générés, par exemple les sprites, problème en image:
Ici le sprite est en 2D, composé de quatre sommets qui sont positionnés comme l'indique la texture.
Le problème apparaît quand on met ce même sprite en 3D:
Les quatre sommets sont toujours positionnés comme l'indique la texture, le problème vient ici du repère qui n'est tout simplement pas le même entre la 2D (Y vers le bas) et la 3D (Y vers le haut).
Ce problème apparaît notamment avec les sprites et les textes, ceux-ci générant eux-même leurs sommets selon un repère arbitraire.
-
Spécifier le repère à utiliser au niveau du GraphicsComponent:
1
textGraphics.SetCoordinateSystem(nzCoordinateSystem_2D);
Le problème de cette méthode est la lourdeur, je pense que ça embêterait plus d'un utilisateur de devoir faire ça pour chaque InstancedRenderable.
-
Spécifier le repère au niveau du RenderSystem.
Si on part du principe que les World sont distincts pour la 2D/3D, on peut gérer directement le système de coordonnées au niveau du RenderSystem du monde:1
world.GetSystem<Ndk::RenderSystem>().SetCoordinateSystem(nzCoordinateSystem_2D);
Après avoir écrit tout ça, je pense partir dans la logique des mondes distincts (ce qui m'embête un peu) avec le système de coordonnées définit au niveau du RenderSystem, c'est ce qui me semble le plus propre et le moins contraignant.
Mais je voulais quand même vos avis sur l'architecture que j'emprunte, si vous aviez une meilleure idée, ou des questions pour faire avancer la chose, n'hésitez surtout pas !
Merci d'avoir lu.