Nazara Engine

Moteur de jeu libre en C++14

a marqué ce sujet comme résolu.

Je l'ai déjà commencé (ainsi que le plan d'ensemble) sur PdP, mais ça ne posera pas de problème de le proposer également ici (et pourquoi pas aussi sur OC, après tout l'objectif est qu'il soit lu). :)

Tu pourras me compter dans les bêta testeurs :) Bonne chance pour cette année !

thco

Merci, j'en prends bonne note !

+0 -0

Je l'ai déjà commencé (ainsi que le plan d'ensemble) sur PdP, mais ça ne posera pas de problème de le proposer également ici (et pourquoi pas aussi sur OC, après tout l'objectif est qu'il soit lu). :)

Lynix

Super ça ! Par contre, il faudra que tu te poses la question sur le référencement. Il me semble que Google va détecter que ton tutoriel apparait sur plusieurs sites et va référencer la première occurrence qu'il détectera et "sanctionnera" les autres. Je ne sais pas pour les autres plateformes mais si tu décides que Zeste de Savoir n'est pas la plateforme officielle pour ton tutoriel, il est possible de mentionner la source originale pour ces questions de référencement.

En tout cas, j'ai hâte de voir ce tutoriel. :)

Bonsoir,

Comme je l'ai dis, je travaille maintenant sur l'interface du moteur pour le rendre utilisable, et vos retours m'intéressent !
En effet, l'interface actuelle ne me semble vraiment pas des plus pratiques, un exemple:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
int main()
{
    NzInitializer<NzAudio, NzGraphics> nazara; // Initialisation du module audio et du module graphique
    if (!nazara)
    {
        // Une erreur s'est produite dans l'initialisation d'un des modules
        std::cout << "Failed to initialize Nazara, see NazaraLog.log for further informations" << std::endl;
        std::getchar(); // On laise le temps de voir l'erreur

        return EXIT_FAILURE;
    }

    // Création d'une fenêtre
    NzRenderWindow window(NzVideoMode(800, 600, 32), "Nazara Engine");

    // Création de la scène
    NzScene scene;

    NzCamera camera;
    camera.SetTarget(window); // On lie la caméra à la fenêtre

    scene.SetViewer(camera); // On indique à la scène que le viewer (Le point de vue) sera la caméra

    // Création d'un modèle
    NzModel spaceship;
    spaceship.LoadFromFile("resources/Spaceship/spaceship.obj"); // On charge un vaisseau depuis un fichier .obj

    // Model hérite de SceneNode (héritant de Node) et peut donc faire partie de la scène de cette façon
    // spaceship.SetParent(scene.GetRoot()); (Parenter un SceneNode au SceneRoot le lie à une scène)
    // pour plus de facilité, une scène est implicitement convertible vers le SceneRoot, permettant de faire:
    spaceship.SetParent(scene);

    // Même chose avec les lumières, sprites, etc

    // Boucle principale
    while (window.IsOpen())
    {
        // Gestion des évènements

        // L'affichage d'une scène passe par quatre opérations à chaque image

        // Appelle régulièrement la méthode Update() de chaque SceneNode nécessitant une mise à jour même invisible
        // (Exemple: Animation du squelette d'un SkeletalModel)
        scene.Update();

        // Parcourt (récursivement) les SceneNode enfants du root node
        // et mets à jour l'état de visibilité de chacun (pour l'instant ça se résume à du frustum culling),
        // appelle également la méthode AddToRenderQueue() de chaque SceneNode visible, ajoutant évidemment le node à la file
        // de rendu de l'image
        scene.Cull();

        // Traitement de la file de rendu par la RenderTechnique (et donc appels à la carte graphique)
        scene.Draw();

        // Mise à jour de la fenêtre (inversion du back et du front buffer, etc)
        window.Display();
    }

Le problème ici c'est que ce code est très adapté pour faire des exemples ou des jeux très simples tenant en une fonction, mais beaucoup moins adapté à la création d'un vrai jeu.
Pourquoi me demanderez-vous ?
Parce que c'est à nous de nous assurer de la survie de nos objets, qui ne survivront pas en dehors du bloc de leur création.

Par exemple, si je veux créer un système de particule à chaque appui sur la barre d'espace, je vais devoir l'allouer dynamiquement (d'une façon ou d'une autre) pour qu'il survive au bloc de traitement de l'événement de l'appui sur la touche.
Ce qui peut signifier le stocker dans un std::vector, se promener avec un pointeur, etc.
Rien de très pratique.

J'avais donc pensé adopter une autre gestion des nodes, quelque chose comme:

1
2
NzModel* model = scene.CreateNode<NzModel>("MonBeauVaisseau"); // Nom optionnel
model->LoadFromFile("blah.fbx");

Avec une suppression assurée lors de la destruction de la scène (ou un appel à scene.Clear()), ou plus spécifiquement:

1
2
3
4
5
6
7
NzModel* spaceship = scene.GetNode<NzModel>("MonBeauVaisseau"); // Erreur si le type ne correspond pas ou si aucun node ne porte ce nom

// Meilleure façon de le détruire ?
scene.RemoveNode(spaceship); // 1) Destruction depuis la scène
spaceship->Remove(); // 2) Destruction depuis le node lui-même
delete spaceship ? // 3) Suppression du node lui-même
// Et le pire étant que je peux très bien avoir les trois de possible (mais ça ne serait pas une bonne idée)

À côté de ça, il y a également la gestion des erreurs à revoir, je dois par exemple autoriser la redirection du flux d'erreur, gérer plusieurs types d'erreurs et associer un contexte à chaque erreur (permettant par exemple de ne pas spammer le log/la console avec une erreur se produisant en boucle).
Mais ça, c'est pour plus tard.

Et dans une autre mesure, l'interface de la SFML que j'ai pompé lors de la création du moteur (car il faut bien admettre qu'elle est pratique) n'est peut-être pas non plus ce qui est le plus utile pour un moteur de jeu, je pense notamment à la gestion des événements (un système de callback serait sans doute plus pratique ?).

Autre point important, les ressources.
Dans Nazara, les ressources sont des objets lourds (sons, images, textures, maillages, …) qui ne sont pas copiés mais référencés un peu partout dans le moteur, ils sont donc soumis à un comptage de référence.
Note: Les matériaux sont également des ressources car leur unicité est importante d'un point de vue performances, et aussi parce qu'ils référencent beaucoup de textures.

Pour l'instant, ils sont traités comme de simples objets mais devraient être alloués dynamiquement à chaque fois (pour la simple raison qu'ils peuvent se suicider une fois leur compteur de référence tombé à zéro).
Exemple:

1
2
3
4
5
6
7
{
    NzTexture* texture = new NzTexture; // Nombre de références: 0
    texture->SetPersistent(false); // Supprimer lorsque le compteur de référence tombe à zéro

    NzTextureRef tex = texture; // TextureRef (NzResourceRef<NzTexture>) maintient automatiquement une référence sur l'objet, à la façon d'un pointeur intelligent.
    // Dans cet exemple, texture est supprimée en même temps que l'objet tex, car son nombre de référence tombe à zéro.
}

Je n'aime pas spécialement cette interface, j'avais donc pensé à la remplacer par quelque chose comme:

1
NzTextureRef texture = NzTexture::Create(); // Texture::Create renvoie un pointeur pour plus de flexibilité

ou encore introduire un manager de ressources, permettant d'associer un nom aux ressources (et éventuellement de les garder persistantes).

1
NzMaterial* material = NzMaterialManager::Get("NomBizarre"); // Création si le nom n'existe pas, sinon récupération

(les deux mesures sont complémentaires).

À noter que l'interdépendances de ressources est aussi un problème que je n'ai pas encore réglé, chaque ressource permet d'enregistrer des ResourceListeners qui seront notifiés lors de la modification/libération de la ressource.
Je ne suis pas convaincu par ce système, c'est pourquoi je vous en parle, pour le revoir avec vous !

Bref, je vous demande votre avis sur l'interface de vos rêves pour un moteur de jeu, qu'elle soit la plus pratique/agréable à utiliser !
Merci !

Super ça ! Par contre, il faudra que tu te poses la question sur le référencement. Il me semble que Google va détecter que ton tutoriel apparait sur plusieurs sites et va référencer la première occurrence qu'il détectera et "sanctionnera" les autres. Je ne sais pas pour les autres plateformes mais si tu décides que Zeste de Savoir n'est pas la plateforme officielle pour ton tutoriel, il est possible de mentionner la source originale pour ces questions de référencement.

Andr0

J'en prends note, merci !

+2 -0

Salut,

Tout d'abord bravo pour ton travail !

Pour ce qui des remarques, je suis d'accord sur le principe. Beaucoup de moteurs utilisent des constructeurs de GameObject en passant par des fonctions Static. Ca évite énormément de problèmes car le lien est automatiquement établie (entre node et scene par exemple) et le renvoi de pointeur est presque une obligation. A mes yeux ce serait une erreur de laisser la possibilité à l'utilisateur de créer un GameObject sur la stack car rien ne l'empêcherai de passer son adresse à un autre objet alors qu'il serait détruit juste après.

Sinon j'aurais juste une petite remarque sur la boucle de rendu. Je ne vois pas trop l'intérêt d'un Cull manuel. Tu pourrais simplement l'appeler par défaut dans le Draw et ajouter une deuxième Draw qui prendrait un boolean si jamais on veut Draw sans faire de Cull (cas assez exceptionnel quand même). Parce que dans l'hypothèse ou ton moteur serait assez utiliser par des novices ils n'en comprendraient pas trop l'intérêt. Avec ou sans Cull le résultat serait le même visuellement (mais pas au niveau des perfs ^^ ).

Quoi qu'il en soit bonne continuation !

+0 -0

Pour ce qui des remarques, je suis d'accord sur le principe. Beaucoup de moteurs utilisent des constructeurs de GameObject en passant par des fonctions Static. Ca évite énormément de problèmes car le lien est automatiquement établie (entre node et scene par exemple) et le renvoi de pointeur est presque une obligation. A mes yeux ce serait une erreur de laisser la possibilité à l'utilisateur de créer un GameObject sur la stack car rien ne l'empêcherai de passer son adresse à un autre objet alors qu'il serait détruit juste après.

Loptr

Je suis assez d'accord, cependant je pense que ça peut avoir son intérêt de laisser l'utilisateur libre maître de la création des nodes en dehors de la scène, je vais expliquer pourquoi plus bas.

Sinon j'aurais juste une petite remarque sur la boucle de rendu. Je ne vois pas trop l'intérêt d'un Cull manuel. Tu pourrais simplement l'appeler par défaut dans le Draw et ajouter une deuxième Draw qui prendrait un boolean si jamais on veut Draw sans faire de Cull (cas assez exceptionnel quand même). Parce que dans l'hypothèse ou ton moteur serait assez utiliser par des novices ils n'en comprendraient pas trop l'intérêt. Avec ou sans Cull le résultat serait le même visuellement (mais pas au niveau des perfs ^^ ).

Loptr

L'intérêt dans la séparation des appels est de pouvoir faire des opérations entre chaque.
Par exemple, l'appel à Cull() va vider la RenderQueue et la remplir, si l'utilisateur souhaite ajouter ses propres objets dans la RenderQueue sans pour autant passer par le système traditionnel, il ne peut le faire qu'entre l'appel à Cull() et l'appel à Draw().
Bon évidemment là on part dans le bas-niveau et ça ne concerne pas 99% des utilisateurs, donc je pensais simplement faire une méthode effectuant les appels dans l'ordre, pour simplifier l'interface.

Salut, Tout d'abord bravo pour ton travail !

Loptr

Quoi qu'il en soit bonne continuation !

Loptr

Merci !

Donc je disais, en supposant que je garde l'interface de création des nodes présentées plus haut (ce qui a des chances d'arriver car elle me plaît et ne semble pas souffrir de vrai défaut):

1
NzModel* model = scene.CreateNode<NzModel>("MonBeauVaisseau");

Un problème ici c'est de devoir paramétrer le nouveau modèle à chaque fois, de le charger depuis un fichier (ce qui ne se fait qu'une fois grâce aux futurs resources manager), de sélectionner un skin, voire changer les matériaux.

Je propose donc comme solution d'exploiter le constructeur de copie.
Soyons clairs, la classe Model est une classe légère qui ne contient aucune ressource, elle fait référence (en simple, contient un pointeur) vers un Mesh et des Material, ceux-ci ne sont pas copiés, les références (pointeurs) le sont.

Je propose donc deux solutions, la première:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
NzModel spaceshipTemplate;
spaceshipTemplate.LoadFromFile("spaceship.3ds");
spaceshipTemplate.SetSkin(2);
// Traitements touchant tous les vaisseaux

// Plus loin dans le code, construction d'une flotte
for (unsigned int i = 0; i < 100; ++i)
{
    NzModel* spaceship = scene.CreateNode<NzModel>("", spaceshipTemplate); // Pas de nom, construit via le template
    spaceship->SetPosition(i*100.f, 0.f, 0.f);
    // Autres traitements uniques à une instance
}

Et la seconde:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
NzModel* spaceshipTemplate = scene.CreateTemplate<NzModel>("Spaceship10Template");
spaceshipTemplate->LoadFromFile("spaceship.3ds");
spaceshipTemplate->SetSkin(2);
// Traitements touchant tous les vaisseaux

// Plus loin dans le code, construction d'une flotte
for (unsigned int i = 0; i < 100; ++i)
{
    NzModel* spaceship = scene.CreateNode<NzModel>("", "Spaceship10Template"); // Pas de nom, construit via le template nommé
    spaceship->SetPosition(i*100.f, 0.f, 0.f);
    // Autres traitements uniques à une instance
}

La différence ici est que le template fait partie de la classe Scene, et c'est elle qui gère sa durée de vie.
Les templates seraient des nodes comme les autres à l'exception qu'ils ne feraient pas partie de la scène en tant qu'objets visibles, et qu'ils ne seraient pas soumis à un appel à scene.Clear(); (mais on peut imaginer une méthode ClearTemplates(), ou un argument supplémentaire à Clear).

Qu'en pensez-vous ? N'hésitez pas à me soumettre vos idées/avis si vous les trouvez pertinents, ça m'aidera à faire avancer le moteur et à vous le proposer plus rapidement comme outil de travail ! (Et puis surtout, un outil qui ressemble à quelque chose que vous voulez utiliser).

+0 -0

Finalement je vais faire en sorte que la scène soit responsable de la création/destruction des nodes, ça me permettra de contrôler la façon dont la scène gère la mémoire (pour par exemple allouer ses nodes dans un MemoryPool).

Le moteur a beaucoup avancé ces derniers jours, les textes sont maintenant pleinement supportés (avec Cabin comme police par défaut), et les particules (toujours expérimentales) ont été ramenés dans la branche master.

Toutes les fonctionnalités ne sont pas encore présentes (Il n'y a pas encore de classe TextDrawer pour faire du "RichText" comme je le montrais dans mes précédents screenshots ou dans la vidéo anniversaire).

Voici l'interface qui a été retenue pour le texte:

1
2
3
4
5
6
7
8
NzSimpleTextDrawer textDrawer;
textDrawer.SetFont(font);
textDrawer.SetText("Hello World");
textDrawer.SetCharacterSize(36);

NzTextSprite text;
text.SetParent(scene);
text.SetText(textDrawer);

Ceci permet une grosse flexibilité, la classe TextDrawer étant responsable du positionnement des caractères et pouvant avoir l'interface qu'on veut, il serait possible d'avoir une classe interprétant le MarkDown, le BBCode, supportant les flux (c'est prévu) à passer à TextSprite.

Par ailleurs, il existe ce raccourci très utile:

1
text.SetText(NzSimpleTextDrawer::Draw(font, "Hello World", 36));

(Et je pense que je vais renommer SetText en Update aussi, ça aura plus de sens).
À terme, avec le Font Manager et la nouvelle interface des scènes (autrement dit, d'ici fin de semaine), nous aurons ceci:

1
2
NzTextSprite* text = scene.CreateNode<NzTextSprite>();
text->Update(NzSimpleTextDrawer::Draw("Arial", "Hello World", 72));

J'ai hâte :D

Oh et comme maintenant le texte et les particules sont supportées…

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
nzUInt64 acc = 0;
for (nzUInt64 t : time)
    acc += t;

nzUInt64 updateTime = acc/1000;

NzStringStream info;
info << "Particle count: " << system.GetParticleCount();
info << "\nParticle life: " << generator->baseLife << "s";
info << "\nUpdate time: " << updateTime << "ms";
info << "\nUpdate time (per particle): " << acc/std::max(system.GetParticleCount(), 1U) << "ns";

particleInfo.SetText(NzSimpleTextDrawer::Draw(info, 32));

(Comme le texte est mis à jour plutôt rapidement, je me suis permis de le mettre à jour 30 fois par seconde).

Je vous souhaite une bonne après-midi, j'ai encore du pain sur la planche !

+0 -0

Juste pour dire que je viens de me rendre compte que j'ai commis deux erreurs, la première dans le screenshot plus haut est une erreur d'unité, le temps de mise à jour du système ne se compte pas en millisecondes mais bien en microsecondes (heureusement !).

Et donc, je viens également de corriger une énorme erreur qui rendait la génération de particules extrêmement lente (de l'ordre des millisecondes), ce qui m'a pas mal rassuré sur le coup.

Screenshot à jour:

Le gain de temps ne se voit pas ici car les particules n'étaient déjà plus générées (limite) sur le dernier screenshot, d'ailleurs on observe une baisse de performances, mais étant donné que le code n'a pas du tout changé, je ne peux que penser que mon ordinateur est un peu plus chargé qu'il y a douze heures.

En revanche, les FPS ne bougent pas, ce qui signifie que le bottleneck (ce qui empêche les FPS de monter plus haut) est au niveau du rendu (application fillrate-limited), et c'est très bien car c'est l'objectif !

Donc voilà, on se retrouve avec 30k particules simulées sur le CPU en monothread en ~450 microsecondes, je pense que c'est un bon score !
(Oh et au passage j'ai également optimisé le rendu du texte qui se mets à jour en maintenant 30 microsecondes de moins qu'avant, soit environ 50 microsecondes pour le texte du screenshot)

+1 -0

Bien le bonsoir !

Alors pour vous tenir informés des derniers avancements: Dernièrement, le moteur a bien avancé, subi des corrections de bugs et de changement d'API, et un contributeur (Gawaboumga) travaille sur l'intégration de tests unitaires et quelques corrections.

La classe Resource a été séparée en RefCounted (reprenant tout le côté listener et comptage de référence) et Resource (qui se résume pour l'instant à un attribut "filepath").

Dans les changements introduits, nous avons:

1
2
NzMaterial* material = new NzMaterial;
material->SetPersistent(false);

qui a été remplacé par:

1
NzMaterialRef material = NzMaterial::New();

Le moteur n'oblige donc plus à passer par la gestion manuelle de mémoire (bien que ça soit toujours possible, pour les éventuels masochistes), et fait en sorte de passer par des références pour les ressources tout le temps, faisant du code du C++ moderne, exception-safe (vive le RAII), avec le moins de fuites mémoires possible.
Les Libraries et Managers ont également fait leur entrée.
Une Library est une collection de noms associés à des références sur des objets, permettant de les retrouver facilement dans le code.
On peut imaginer ceci:

1
2
3
4
5
6
7
8
9
NzTextureRef texture = NzTexture::New();
texture->LoadFromFile("resources/overlay.png");

NzTextureLibrary::Register("Overlay", texture);

// Bien après, dans un autre fichier lointain, très lointain.

NzMaterialRef material = NzMaterial::New();
material->SetDiffuseMap("Overlay");

Ce procédé était déjà utilisé pour les Shaders et UberShaders, il a été étendu à toutes les classes (ou presque, les buffers par exemple n'en sont pas) étant comptées par référence.

Quant aux managers, attendus depuis la création du moteur, ils ont un rôle simple: Empêcher de recharger la même ressource un millier de fois.

1
2
// Charge spaceship.obj la première fois, renvoie une référence vers la ressource déjà chargée les autres fois
NzMeshRef spaceship = NzMesh::Get("spaceship.obj"); 

Cette amélioration, simple mais essentielle, permet de booster le chargement de certaines ressources, de diminuer la consommation mémoire, voire même d'accélérer le rendu (réutilisation des mêmes ressources), mais soyons clairs: c'est une faute de ma part de ne pas l'avoir intégré plus tôt.

Et ça fonctionne aussi avec les matériaux:

1
2
// Si la TextureLibrary n'a pas d'entrée de ce nom, va faire appel au TextureManager
material->SetDiffuseMap("overlay.png");

Le manager permet aussi une purge, pour libérer toutes les ressources qu'il est le seul à posséder (utile en cas de changement de niveau dans un jeu par exemple, on purge les managers après avoir chargé le nouveau niveau pour libérer de la mémoire).

Bref au final, les ajouts de ces derniers jours sont assez simples, voire basiques pour certains, mais ils sont surtout essentiels pour l'utilisateur.

Dans les prochains jours, je vais continuer la correction de bugs et l'amélioration de l'API, je vais également attaquer un peu le tutoriel (et pourquoi pas le générateur de doc), je cherche toujours quelqu'un pour l'implémentation Linux aussi.

Dans un futur un peu plus éloigné, il faudra aussi que je permette aux sprites/textes d'avoir des normales et des tangentes (pour supporter pleinement l'éclairage).

Oh j'oubliais, l'interface des scènes dont je parlais dans un précédent post est implémenté, et l'exemple FirstScene a été modifié pour en tenir compte, mais vous savez quoi ? Elle ne me satisfait pas pleinement.

Donc je reprends:

1
2
3
4
NzScene scene;

NzModel* spaceship = scene.CreateNode<NzModel>();
// scene est responsable de la durée de vie du node, et le node est automatiquement parenté à la scène.

Comment gérer le parenting de nodes à d'autres nodes ? Un argument à la méthode CreateNode ? Comment gérer la durée de vie de ces node-là ? (Dont on peut supposer qu'ils devraient disparaître en même temps que leur parent). Comment gérer le parenting à autre chose que des SceneNode (la caméra par exemple, en tant qu'élément ne faisant pas partie du rendu, n'est qu'un simple Node) dans ces cas-là ?

Bref, je ne vais pas y aller par quatre chemins, je pense enlever la gestion des scènes du moteur, c'est de trop haut niveau pour le "socle" qu'est Nazara, je pense qu'elles ont plus leur place dans le NDK, la bibliothèque qui va se lier à tous les modules du moteur et proposer ce qu'il faut pour un moteur de jeu.
Le rendu directement via les modules de plus bas-niveau ne disparaîtrait évidemment pas, mais serait un peu plus chiante à gérer (gestion manuelle des objets, de la visibilité, des techniques et files de rendu), mais je vois ça comme un mal pour un bien (et je suis toujours en pleine réflexion), ça a plus de sens tant au niveau de l'interface que techniquement.

J'aimerai également discuter avec vous d'une idée que j'ai eu et qui m'intéresse beaucoup malgré sa complexité d'implémentation: Pour l'instant, la scène contient des Node et des SceneNode, dans une hiérarchie claire, les SceneNode sont les objets qui vont affecter le rendu visuellement (lumières, sprites, modèles, textes), possèdent une bounding box et subissent du culling, ce qui n'est pas affichable (simple Node, Camera, etc.) est alors un Node.
Lors du rendu, la scène va parcourir l'arbre de descendance des nodes, détecter les scene node et ajouter ceux qui sont visibles à la RenderQueue.

Dans mon idée, j'aimerai virer les Nodes, ne garder que les SceneNode (la caméra serait alors un SceneNode), rien qu'eux.
Quid des lumières, modèles, etc ? Ils seraient des "effets" attachables à un SceneNode. Pourquoi complexifier l'interface ? Pour un avantage très simple: un modèle pourrait être attaché à plusieurs SceneNode.

Imaginez une barrière (comme celle de la démo House), vous pourriez n'avoir qu'un seul modèle attaché à une quarantaine de SceneNode, ou encore une grosse armée de soldats marchant au pas: un seul ou quelques SkeletalModel rattachés à plusieurs nodes.
On a ici une diminution de la consommation mémoire (légère mais présente), et surtout une architecture qui encourage le batching: il serait plus facile de reprendre le même objet plutôt que de le dupliquer (comme actuellement).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// On charge un soldat et on lui applique l'animation de course
NzSkeletalModel soldier;
soldier.LoadFromFile("soldier.fbx");
soldier.SetSequence("run");

for (unsigned int i = 0; i < 100; ++i)
{
    NzSceneNode* node = armyNode->CreateChildren();
    node->SetPosition((i%10)*100.f, 0.f, (i/10) * 100.f); // Positionnement

    node->Attach(soldier); // On attache le soldat au SceneNode
}

Résultat ? Le squelette n'est calculé qu'une seule fois, la bounding box (locale) également, le skinning n'est appliqué qu'une fois, et tout est rendu en une fois (très probablement avec de l'instancing).
(Pour être clair: Le moteur est déjà capable de ces optimisations, mais l'API est un peu un frein).

D'un point de vue technique, le node devrait alors stocker une matrice/bounding volume par effet attaché, rien d'insurmontable, ça se complique un peu pour les sprites/textes qui devraient alors stocker leurs sommets (globaux) dans le node aussi, mais encore une fois rien d'insurmontable.

Donc voilà, n'hésitez pas à commenter, pour l'instant rien n'est fait, donc tout peut changer :)

+1 -0

Bien le bonjour !

Malheureusement pas encore grand chose à dire du côté de Nazara, je suis toujours en train de me battre avec l'ECS (surtout sur ma feuille de papier), et je n'ai pas prévu de travailler dessus dans les prochains jours pour la simple raison que je m'improvise des vacances à Paris, à partir d'aujourd'hui 16h jusqu'à mardi 17h (soit cinq jours).

Comme je risque d'avoir un peu de temps libre, je voulais voir s'il y avait des gens intéressés pour une IRL, ce n'est pas tous les jours que je suis à Paris et ça peut être sympathique de se retrouver entre programmeurs autour d'un verre. S'il y a des intéressés, n'hésitez pas à m'envoyer un message privé ;)

+1 -0

Bonjour !

Petit bulletin d'information: (à lire avec l'accent belge) Je suis de retour dans ma Belgique natale depuis quelques jours, après un bon petit séjour à Paris.
J'ai eu le plaisir de rencontrer quelques-uns d'entre-vous (dédicace à hardcpp et informaticienzero :P ), et il faut bien avouer quelque chose, parler de programmation en face à face, que ça soit autour d'une bière, ou en train de manger un KFC (fast-food dans lequel je n'avais jamais encore foutu les pieds), ça fait bizarre :D
Quoiqu'il en soit, il est très possible que je refasse un petit tour de France dans pas trop longtemps, un peu mieux organisé, en passant de nouveau quelques jours à Paris, et vers Tours (ayant été invité à un marathon de programmation dans le coin), je vous tiendrais informé !

Concernant Nazara, j'ai repris le travail depuis mon retour, et j'ai implémenté une idée que j'ai eu pendant mon séjour: casser un principe de base de l'ECS.
Je sais, dit comme ça c'est pas top mais permettez-moi d'expliquer la façon dont j'avais implémenté le tout au début.

Dans ma première implémentation, une entité était un nombre, et la classe Ndk::Entity servait uniquement d'accesseur vers le monde (Ndk::World). Cela signifie qu'il pouvait il y avoir plusieurs instances de Ndk::Entity pointant vers la même entité.
Afin de gérer la vie et la mort des entités, l'identifiant était divisé en deux morceaux (façon anax et entityx): l'ID à proprement parler et un compteur, incrémenté à chaque mort d'une entité avec cet ID.
L'objectif était de garantir l'unicité de chaque identifiant tout en réutilisant un même ID (qui servait d'accesseur dans plusieurs tableaux), de la sorte qu'un Ndk::Entity soit invalide, et le reste, après la mort d'une entité.
Je n'étais pas vraiment un grand fan de ce système, qui n'avait aucune espèce de protection contre l'overflow (bon, il aurait fallu tuer une même entité quatre milliards de fois pour que ça soit un problème) ou qui me semblait un peu trop compliqué pour ce que c'était.
Quant aux données de l'entité, elles étaient entièrement contenues dans le monde, sous la forme de plusieurs tableaux (AoS).

La nouvelle implémentation fonctionne différemment: plutôt que de considérer les entités comme de simples nombres, elles ont maintenant une classe (Ndk::Entity) qui existe de façon unique pour chaque entité qui sont contenues dans le monde (Ndk::World), les identifiants se résument maintenant à un nombre en un seul morceau, réutilisable à souhait (tuer l'entité #42 et créer une entité juste après fera en sorte qu'elle ait l'ID #42).
Afin de gérer justement l'accès aux entités, une nouvelle classe a été créée (Ndk::EntityHandle), qui est concrètement un genre de pointeur intelligent: si l'entité est tuée, tous les handles pointant vers celle-ci sont invalidés aussitôt.
Pour gérer cela, une entité stocke un tableau de pointeurs vers ses handles, et les informe en cas de mouvement (en cas de réallocation du tableau global des entités, dans le monde) ou en cas de mort.

Avec ce système les données vont être organisées façon SoA, que j'imagine être plus cache-friendly, les identifiants sont réutilisés sans cesse (mais on s'en fout car les handles nous garantissent l'invalidité en cas de mort), et les entités peuvent être plus compliquées de base.
Par exemple j'ai dans l'idée de les faire hériter de la classe NzNode, afin de leur garantir de base la possibilité d'être placées/tournée/attachées à une autre entité/etc. sans passer par un component.
En parlant des components, j'envisage de les stocker dans des MemoryPool pour les rendre un peu plus cache-friendly, et diminuer le coût de création d'une entité (en diminuant les allocations de mémoire).

Donc voilà, je n'ai toujours pas de composants ni de systèmes pour l'instant, j'avance à tâtons, n'hésitez pas à me conseiller/me guider si vous avez une idée !

+0 -0

Bonjour tout le monde !

Quelques informations ! Tout d'abord, l'ECS avance bien, les systèmes sont implémentés et sont correctement gérés par le monde (je procède à quelques tests en attendant le commit).

Un exemple au niveau du code:

Tout d'abord, créons trois components

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct PositionComponent : public Ndk::Component<PositionComponent>
{
    NzVector3f position;

    static nzUInt32 ComponentId;
};

nzUInt32 PositionComponent::ComponentId = Ndk::BaseComponent::GetNextId();

struct VelocityComponent : public Ndk::Component<VelocityComponent>
{
    NzVector3f velocity;

    static nzUInt32 ComponentId;
};

nzUInt32 VelocityComponent::ComponentId = Ndk::BaseComponent::GetNextId();

struct TestComponent : public Ndk::Component<TestComponent>
{
    NzString name = "Hello world";

    static nzUInt32 ComponentId;
};

nzUInt32 TestComponent::ComponentId = Ndk::BaseComponent::GetNextId();

(Note: les ID des components/systèmes ne peuvent pas être définis dans un header, mais ici je n'ai qu'un main.cpp)

Ensuite, créons un système chargé de la mise à jour des entités ayant à la fois des components de position et de vitesse, mais n'ayant pas de TestComponent (pour l'exemple):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class VelocitySystem : public Ndk::System<VelocitySystem>
{
    public:
        VelocitySystem()
        {
            Requires<PositionComponent, VelocityComponent>();
            Excludes<TestComponent>();
        }

        void Update(float elapsedTime)
        {
            for (const Ndk::EntityHandle& entity : GetEntities())
            {
                PositionComponent& posComponent = entity->GetComponent<PositionComponent>();
                VelocityComponent& velComponent = entity->GetComponent<VelocityComponent>();

                posComponent.position += velComponent.velocity * elapsedTime;
            }
        }

        static nzUInt32 SystemId;
};

nzUInt32 VelocitySystem::SystemId = Ndk::BaseSystem::GetNextId();

Bien, maintenant que nous avons définis les composants et systèmes, nous pouvons créer notre monde:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
int main()
{
    Ndk::World world;

    // Ajout du système auprès du monde
    VelocitySystem& system = world.AddSystem<VelocitySystem>();

    // Création de trois entités et récupération des handles
    std::vector<Ndk::EntityHandle> entities = world.CreateEntities(3);

    // Ajout d'un premier composant via template (#0)
    TestComponent& t1 = entities[0]->AddComponent<TestComponent>();
    t1.name = "Hello le monde !";

    // Récupération du composant via template
    TestComponent& t2 = entities[0]->GetComponent<TestComponent>();
    std::cout << t2.name << std::endl; // "Hello le monde !"

    // Ajout d'une copie du composant à une autre entité (#1)
    TestComponent& t3 = *static_cast<TestComponent*>(entities[1]->AddComponent(std::unique_ptr<Ndk::BaseComponent>(t2.Clone())));
    std::cout << t3.name << std::endl; // "Hello le monde !"

    entities[1]->AddComponent<PositionComponent>();
    entities[1]->AddComponent<VelocityComponent>();

    // Ajout d'une position/vitesse (#2)
    entities[2]->AddComponent<PositionComponent>().position = NzVector3f(0.f); // Initialisation
    entities[2]->AddComponent<VelocityComponent>().velocity = NzVector3f::Up();

    // On tue l'entité #0
    entities[0]->Kill();

    // Mise à jour du monde
    world.Update(1.f); // mise à jour d'une seconde

    // À ce stade, notre système est associé avec une seule entité (#2) car elle est la seule à correspondre aux filtres (#1 contient un TestComponent qui est exclu du système)

    std::cout << entities[2]->GetComponent<PositionComponent>().position << std::endl; // affichera une valeur très proche de NzVector3f::Up()

    // Juste pour le fun
    std::cout << entities[2] << std::endl; // EntityHandle(Entity(2))
    std::cout << entities[0] << std::endl; // EntityHandle(Null entity)
}

Voilà qui illustre bien l'interface.
Quelques hésitations que j'ai pour l'instant: world.Update() met à jour les entités (association aux systèmes, mort) et les systèmes (appel méthode Update()), mais dans les deux ECS que j'ai étudié (Anax et EntityX) la mise à jour des systèmes était laissée au programmeur:

1
2
3
4
5
6
7
8
9
void Game::update(float deltaTime)
{
    m_World.refresh();

    m_PlayerInputSystem.update(deltaTime);
    m_MovementSystem.update(deltaTime);
    m_AnimationSystem.update(deltaTime);
    m_CollisionSystem.update(deltaTime);    
}

Quel avantage ? J'imagine que c'est pour mettre à jour les systèmes dans un certain ordre (mais est-ce réellement important d'une frame à l'autre ?). Pourquoi pas un système en trois étapes (PreUpdate, Update, PostUpdate) ? Ou un système de priorité ?
Garder la mise à jour des systèmes au niveau du monde me permet de simplifier l'interface et d'implémenter la mise à jour par pas fixe par exemple (garantissant la stabilité du monde même si le framerate est instable).

J'en profite aussi pour faire une petite parenthèse sur une classe que j'ai ajouté hier au moteur, et qui sert beaucoup dans l'implémentation de l'ECS: la classe Bitset.

Pour la petite histoire, j'avais besoin de stocker un grand nombre de bits (filtrage des composants, états des entités, …), un nombre que je ne connaissais pas à l'avance et qui pouvait changer dynamiquement, exit std::bitset.
Je me suis alors tourné vers std::vector<bool>, le plus grand reproche fait à cette spécialisation est que ce n'est pas un conteneur de la même façon que std::vector. Je me suis dit que je n'avais qu'à ne pas l'utiliser en tant que tel du coup :D

Avant de découvrir qu'en fait ça manquait cruellement de fonctionnalités.
Par exemple, itérer sur tous les bits activés demande de vérifier chaque bit un à un, alors qu'avec une bonne classe Bitset on peut radicalement accélérer les choses (tester les bits par groupe de 32 par exemple).
… Une bonne classe Bitset comme Boost::dynamic_bitset !

… Je vais me faire incendier par certains pour ce que je vais dire, mais je vous jure que j'ai essayé. J'ai réfléchi à l'utilisation de Boost pour de vrai.
Et puis j'ai étudié les dépendances nécessaires pour cette simple classe, et non, c'est trop pour juste une classe.
Je pense sincèrement que Boost est une bibliothèque à ne pas prendre à la légère, soit on l'utilise pour de bon, soit on ne l'utilise pas du tout.
Et bien sûr, la perspective d'avoir Boost comme dépendance pour Nazara ne m'enchantait pas.

Mea culpa, j'ai donc réinventé la roue avec ma propre classe Bitset (en m'inspirant de l'interface de dynamic_bitset).

1
2
3
4
5
6
7
8
9
NzBitset<> bitset(6); // 000000
bitset[3] = true;     // 001000
bitset.Flip();        // 110111
bitset[3].Flip();     // 111111
bitset.TestAll();     // true
bitset.Reset();       // 000000
bitset.TestAny();     // false

&bitset[2] // static_assert failed: It is impossible to take the address of a bit in a bitset

Cependant, cela m'a permis d'introduire quelques fonctionnalités très intéressantes qu'on ne retrouve pas dans Boost:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
NzBitset<> bitset1("00110010");
NzBitset<> bitset2("1100");
NzBitset<> bitset3;

// On peut effectuer des opérations sur deux bitsets de taille différente
bitset2 ^= bitset1; // 0011 1110

// Le test d'égalité ne vérifie que les bits activés, ainsi deux bitsets n'ont pas besoin d'avoir la même taille pour être équivalents
// de la même façon que (uint8) 00001100 == (uint16) 00000000 00001100
bitset2 == NzBitset<>("111110") // true

// Il existe aussi des méthodes permettant d'agir comme si le bitset n'avait pas de limite de taille:
bitset2.Set(10, true); // assert failed
bitset2.UnboundedSet(10, true); // 100 0011 1110
bitset2.UnboundedTest(42); // false (les bits en dehors du bitset sont considérés comme ayant une valeur de 0)

// Opérations élémentaires
bitset2.Count(); // 6 bits activés
bitset2.FindFirst(); // 1 (indice du premier bit activé)
bitset2.FindNext(5); // 10 (indice du premier bit activé à partir du bit X)
// Une boucle for sur les bits activés ressemble donc à
// for (int i = bitset2.FindFirst(); i != bitset2.npos; i = bitset2.FindNext(i))

// Et pour finir, des méthodes permettant d'éviter les allocations dynamiques inutiles
// Cet opérateur va former un bitset temporaire, allouant la place requise pour stocker le résultat avant de le déplacer dans bitset3
bitset3 = bitset1 & bitset2; // 000 0011 0010

// Cette méthode va directement stocker le résultat dans bitset3, et n'allouer que si bitset3 n'est pas assez grand pour le résultat
bitset3.PerformsAND(bitset1, bitset2);

// À noter que bitset3 peut être envoyé comme argument sans problème
bitset3.PerformsXOR(bitset3, bitset3); // 000 0000 0000
bitset3.TestNone(); // true

Voilà pour l'avancement des derniers jours, n'hésitez pas à commenter et critiquer ;)

Edit: En bonus, le code filtrant les entités, à la fois performant et simple grâce aux bitsets:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    bool BaseSystem::Filters(const EntityHandle& entity) const
    {
        const NzBitset<>& components = entity->GetComponentBits();

        m_filterResult.PerformsAND(m_requiredComponents, components);
        if (m_filterResult != m_requiredComponents)
            return false; // Au moins un component requis n'est pas présent

        m_filterResult.PerformsAND(m_excludedComponents, components);
        if (m_filterResult.TestAny())
            return false; // Au moins un component exclu est présent

        return true;
    }
+0 -0

La plupart des bibliotheques de Boost sont independantes. La plupart sont header only, surtout a cause des templates un peu partout.

Dans ma pratique recente de Boost, dans un cadre profesionnel, on utilise que 3 elements (filesystem, interprocess et thread de memoire) et on compile une fois pour toute de maniere statique et link avec nos produits au moment de construire notre produit.

Je suis donc etonne par la remarque sur les dependances parce que je ne vois pas trop desquelles tu veux parler pour le coup.

Sinon, petite remarque, generalement du point de vu performance std::vector<bool> est meilleur que std::bitset alors du coup, j'utilise ca internalement comme structure de donnee et propose une interface par dessus plus correcte semantiquement, avec les operations qui vont bien.
Comme je suis conscient que c'est dependant du materiel et environnemment, la structure interne est definie par un template et mon processus de construction (coucou CMake) test dans plein de petits cas les performances pour switcher de maniere transparente pour l'utilisateur d'un conteneur a l'autre (pour peu qu'il lance la construction avec le flag BENCHMARK ou dans ce gout la).

+0 -0

La plupart des bibliotheques de Boost sont independantes. La plupart sont header only, surtout a cause des templates un peu partout.

Dans ma pratique recente de Boost, dans un cadre profesionnel, on utilise que 3 elements (filesystem, interprocess et thread de memoire) et on compile une fois pour toute de maniere statique et link avec nos produits au moment de construire notre produit.

Je suis donc etonne par la remarque sur les dependances parce que je ne vois pas trop desquelles tu veux parler pour le coup.

Höd

J'ai voulu faire un arbre de dépendance, le nombre de fichiers inclus pour une simple classe bitset est énorme.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
boost/dynamic_bitset/dynamic_bitset.hpp
#include <boost/dynamic_bitset_fwd.hpp>
#include <boost/detail/dynamic_bitset.hpp>
-#include <boost/config.hpp>
-#include <boost/detail/workaround.hpp>
#include <boost/detail/iterator.hpp>
#include <boost/move/move.hpp>
-#include <boost/move/detail/config_begin.hpp>
-#include <boost/move/utility.hpp>
--#include <boost/move/utility_core.hpp>
---#include <boost/move/core.hpp>
---#include <boost/move/detail/meta_utils.hpp>
---#include <boost/static_assert.hpp>
----#include <boost/detail/workaround.hpp>
--#include <boost/move/traits.hpp>
---#include <boost/type_traits/has_trivial_destructor.hpp>
----#include <boost/type_traits/intrinsics.hpp>
-----#include <boost/type_traits/is_same.hpp>

... <= Le moment précis où j'ai perdu la volonté de continuer

-----#include <boost/type_traits/is_reference.hpp>
...
-----#include <boost/type_traits/is_volatile.hpp>
...
----#include <boost/type_traits/is_pod.hpp>
...
----#include <boost/type_traits/detail/ice_or.hpp>
...
---#include <boost/type_traits/is_nothrow_move_constructible.hpp>
...
---#include <boost/type_traits/is_nothrow_move_assignable.hpp>
...
---#include <boost/type_traits/is_copy_constructible.hpp>
...
-#include <boost/move/iterator.hpp>
...
-#include <boost/move/traits.hpp>
...
-#include <boost/move/algorithm.hpp>
...
#include <boost/limits.hpp>
...
#include <boost/pending/lowest_bit.hpp>
...
#include <boost/static_assert.hpp>
...
#include <boost/utility/addressof.hpp>
...
#include <boost/detail/no_exceptions_support.hpp>
...
#include <boost/throw_exception.hpp>
...

Ça fait autant de fichiers à distribuer avec le moteur (et je ne parle même pas du temps de compilation qui explose).

Sinon, petite remarque, generalement du point de vu performance std::vector<bool> est meilleur que std::bitset alors du coup, j'utilise ca internalement comme structure de donnee et propose une interface par dessus plus correcte semantiquement, avec les operations qui vont bien.

Höd

std::vector<bool> est implémenté comme un bitset, et std::bitset est à taille fixe, je ne vois vraiment pas comment bitset pourrait se faire battre dans ces circonstances.

De plus, std::vector<bool> ne te donne pas accès aux blocs, ce qui t'empêche de faire justement les opérations qui vont bien (ex: comment trouver le premier bit non-nul efficacement avec std::vector<bool> ?).

+0 -0

J'ai voulu faire un arbre de dépendance, le nombre de fichiers inclus pour une simple classe bitset est énorme. Ça fait autant de fichiers à distribuer avec le moteur (et je ne parle même pas du temps de compilation qui explose).

Dans un répertoire lib par exemple, que tu dédies aux bibliothèques externes. Je ne vois pas bien le problème d'avoir beaucoup d'inclusion de fichier puisque cela n'implique pas forcément une taille d'exécutable plus importante (ce qui de toute manière n'est pas un problème pour un moteur 3D je suppose) et que les includes de Boost sont bien fait (au sens où ils n'incluent que le strict minimum et use de déclaration anticipées pour réduire le temps de compilation autant que faire se peut). Je ne comprends pas comment l'utilisation de BitSet peut rallonger le temps de compilation si tu utilises les traits de base qui doivent bénéficier d'une déclaration justement pour réduire les passes du compilateur au minimum.

std::vector<bool> est implémenté comme un bitset, et std::bitset est à taille fixe, je ne vois vraiment pas comment bitset pourrait se faire battre dans ces circonstances. De plus, std::vector<bool> ne te donne pas accès aux blocs, ce qui t'empêche de faire justement les opérations qui vont bien (ex: comment trouver le premier bit non-nul efficacement avec std::vector<bool> ?).

Lynix

Cela dépend foncièrement de ce que tu fais dessus, de combien tu en crées, de comment, de quelle taille, combien de fois tu les parcours, de l'environnement parallèle, etc. C'est pour ça que quand on a constaté qu'on avait des différences significatives selon l'utilisation faite par l'utilisateur final, on a mis en place ce système de benchmark qui change la définition du conteneur par défaut.

+0 -0

J'ai voulu faire un arbre de dépendance, le nombre de fichiers inclus pour une simple classe bitset est énorme. Ça fait autant de fichiers à distribuer avec le moteur (et je ne parle même pas du temps de compilation qui explose).

Dans un répertoire lib par exemple, que tu dédies aux bibliothèques externes. Je ne vois pas bien le problème d'avoir beaucoup d'inclusion de fichier puisque cela n'implique pas forcément une taille d'exécutable plus importante (ce qui de toute manière n'est pas un problème pour un moteur 3D je suppose)

Höd

En effet, mais comme il s'agit d'une dépendance au niveau de l'interface (et on peut difficilement faire autrement avec une classe template), cela reviendrait à obliger les utilisateur à posséder Boost pour utiliser le moteur (il y a bien sûr des bibliothèques externes nécessaires pour le compiler, dont les binaires sont nécessaires pour l'exécution, mais aucune ne se retrouve dans l'interface).

Cela dépend foncièrement de ce que tu fais dessus, de combien tu en crées, de comment, de quelle taille, combien de fois tu les parcours, de l'environnement parallèle, etc. C'est pour ça que quand on a constaté qu'on avait des différences significatives selon l'utilisation faite par l'utilisateur final, on a mis en place ce système de benchmark qui change la définition du conteneur par défaut.

Höd

Ce que je voulais dire c'est que std::vector<bool> est un bitset fermé, les blocs ne sont pas exposés dans l'interface et donc aucune opération ne peut être effectuée (autrement que bit à bit).
Je ne doute pas qu'il y a des cas où c'est suffisant, mais je demande à voir un cas où std::vector<bool> se révèle avantageux par rapport à une classe Bitset, que ce soit en terme de place (normalement la même) qu'en terme de performance (et là normalement y'a pas photo).

+0 -0

Hello, je suis actuellement dans le train m'emmenant vers la ville de Tours, on ne sait jamais qu'il y ait des gens habitant pas loin, souhaitant boire un verre !

Côté Nazara, j'ai commencé l'intégration de composants après une dernière révision du système, je suis sur mon téléphone donc il me serait difficile de donner plus de détails mais j'y veillerai.

Plus de nouvelles à venir !

+2 -0

Alors, les quelques nouvelles promises.

Tout d'abord, concernant le SDK de Nazara, j'ai été obligé de remanier l'interface une fois de plus, mais normalement c'est la dernière !

Les composants sont toujours définis de la même façon:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// .hpp
struct VelocityComponent : public Ndk::Component<VelocityComponent>
{
    NzVector3f velocity;

    static Ndk::ComponentIndex componentIndex;
};

// .cpp
Ndk::ComponentIndex VelocityComponent::componentIndex;

Mais l'initialisation est maintenant explicite (suite à des problèmes lors du lancement du programme):

1
2
3
int main()
{
    Ndk::InitializeComponent<VelocityComponent>("Velocity");

Le nom du composant est codé sous un entier de 64 bits, le nom ne peut pas faire plus de huit caractères (caractère de fin exclus), mais il peut en faire moins, les composants de base commencent par le préfixe Ndk.

En parlant de composants de base, j'en ai ajouté un premier: Ndk::NodeComponent.

C'est un composant simple qui hérite de la classe NzNode, il rajoute une position, rotation, mise à l'échelle, matrice de transformation (que je pense déplacer vers un composant à part) et système de parentage de nodes.

Il possède aussi une surcharge permettant d'accrocher une entité directement, ce qui donne:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    Ndk::World world;

    std::vector<Ndk::EntityHandle> entities = world.CreateEntities(2);

    entities[0]->AddComponent<Ndk::NodeComponent>();
    entities[1]->AddComponent<Ndk::NodeComponent>();

    entities[1]->GetComponent<Ndk::NodeComponent>().SetPosition({42.f, 0.f, 0.f});
    entities[0]->GetComponent<Ndk::NodeComponent>().SetParent(entities[1]); // Surcharge permettant d'indiquer une entité directement

    std::cout << entities[0]->GetComponent<Ndk::NodeComponent>().GetPosition() << std::endl;

L'avantage de ce système est qu'une entité peut s'attacher à n'importe quel NzNode du moteur.

J'avance à tâtons pour l'instant, je ne suis pas encore certain de la nomenclature que je vais adopter (utiliser Component comme suffixe me semble un peu lourd..) ni du découpage que je vais adopter (toujours en pleine réflexion sur ce point).

L'objectif final serait d'avoir par exemple un CollisionComponent héritant des caractéristiques du node et servant de collision du moteur physique "fixe" (n'ayant pas de déplacement propre), à l'inverse d'un PhysicsComponent qui influerait sur les caractéristiques du node plutôt que de les récupérer, et bien entendu les deux components que je viens de citer pourront être attachés ensembles sur une même entité.

Côté audio, je pensais pour commencer faire un ListenerComponent, mettant à jour la position du listener audio global automatiquement selon les caractéristiques du node (qu'elles soient affectées ou non par un éventuel PhysicsComponent), viendrait ensuite évidemment un SoundComponent (Ou SoundEmitterComponent ?) permettant d'attacher un son, pouvant évidemment être placé de la même façon.

Alors côté module graphique, ça se complique !

J'aime toujours mon idée de base, celle d'avoir un composant "récepteur" et général, un genre de GraphicsComponent, sur lequel s'accrocheraient des lumières, des modèles, etc.

L'intérêt serait de favoriser le batching dans l'interface (faire en sorte que le rendu par batching soit la façon la plus simple de faire), afin de maximiser les performances sans demander un effort constant de la part de l'utilisateur du moteur.

Exemple très vite fait:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// On charge un soldat et on lui applique l'animation de course
NzSkeletalModel soldier;
soldier.LoadFromFile("soldier.fbx");
soldier.SetSequence("run");

for (unsigned int i = 0; i < 100; ++i)
{
    Ndk::EntityHandle entity = world.CreateEntity();
    auto& graphicsComponent = entity->AddComponent<Ndk::GraphicsComponent>();
    auto& nodeComponent = entity->AddComponent<Ndk::NodeComponent>();

    nodeComponent->SetPosition((i%10)*100.f, 0.f, (i/10) * 100.f); // Positionnement

    graphicsComponent->Attach(soldier); // On attache le soldat à l'entité
}

De la sorte, on se retrouve avec 100 soldats marchant au pas, et batchés.

J'aimerai quand même attacher un SkeletonComponent pouvant prendre le pas sur le SkeletalModel s'il est présent, mais c'est à voir.

Et puis bien sûr, il y a toute la gestion du rendu, les multiples caméras, les techniques de rendu, etc. C'est ce qui risque d'être le plus compliqué à mettre en place.

Bien entendu, dans tous les exemples que j'ai cité, il y a quelques systèmes qui interviennent, mais eux sont majoritairement "cachés" du point de vue de l'utilisateur.

Bref, comme vous le voyez pour l'instant je patauge un peu, n'ayant jamais touché à l'ECS, n'hésitez pas à me dire ce que vous en pensez, mes idées ne demandent qu'à être améliorées !

+1 -0

Alors, plusieurs nouvelles du côté d'un module longtemps oublié, le module physique.
Pour commencer, j'ai remanié la classe de géométries, elle utilise maintenant le comptage par référence, et ne nécessite plus une instance du monde physique pour être créée (une même instance de PhysGeom peut être utilisée pour plusieurs entités, peu importe la simulation dont elles font parties).
Ce qui m'a permis de faire le premier composant physique: CollisionComponent.<br />Ce composant fait ce qu'il indique, ajouter des collisions physiques à une entité

Exemple d'utilisation:

1
2
auto& collision = spaceshipEntity->AddComponent<Ndk::CollisionComponent>();
collision.SetGeom(NzBoxGeom::New(spaceshipModel->GetBoundingVolume().aabb.GetLengths()));

Et voilà, il suffit de ceci pour qu'une entité (ici le vaisseau de FirstScene) interagisse avec le moteur physique, elle aura une géométrie physique (ici une boite englobant le modèle) qui sera affectée par le NodeComponent.

Il est également possible d'utiliser un PhysicsComponent conjointement (ou non) avec le CollisionComponent pour obtenir l'effet inverse: le node sera alors déplacé par le moteur physique (et tout ce qui est attaché au node avec).
Cela signifie beaucoup de choses, il est possible de créer un ensemble de nodes (des lumières, modèles, etc.) affectés par le moteur physique.

J'imagine la création d'une lumière de type point qui réagisse à la physique, ça se ferait en quelques lignes de code à peine et ça donnerait un bel effet au jeu.

En attendant, voici une petite scène de test avec le vaisseau de FirstScene qui est une entité de l'ECS, ayant trois composants:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
auto& nodeComponent = spaceshipEntity->AddComponent<Ndk::NodeComponent>();
nodeComponent.SetParent(scene);

auto& velocityComponent = spaceshipEntity->AddComponent<Ndk::VelocityComponent>();
velocityComponent = NzVector3f::Forward();

NzTrimeshGeomRef spaceshipGeom = NzTrimeshGeom::New(ptr, triangleIterator, staticMesh->GetTriangleCount());

auto& collision = spaceshipEntity->AddComponent<Ndk::CollisionComponent>();
collision.SetGeom(spaceshipGeom);

Et des sphères:

1
2
3
4
5
6
7
8
auto& nodeComponent = sphereEntity->AddComponent<Ndk::NodeComponent>();
nodeComponent.SetParent(scene);

auto& collision = sphereEntity->AddComponent<Ndk::CollisionComponent>();
collision.SetGeom(NzSphereGeom::New(0.5f));

auto& phys = sphereEntity->AddComponent<Ndk::PhysicsComponent>();
phys.SetMass(1.f);

La physique est gérée par deux systèmes, StaticPhysicsSystem (gérant le vaisseau) et PhysicsSystem (gérant les sphères).
Le déplacement du vaisseau est assuré par un VelocityComponent agissant sur le NodeComponent (via le VelocitySystem), déplacement répercuté sur le CollisionComponent (le node déplace la géométrie de collision).
En revanche, les sphères fonctionnent à l'opposée, le PhysicsSystem simule le monde physique et applique les déplacements au node associé (NodeComponent), ce qui se répercute dans la hiérarchie du node en question.

Prochaine étape, faire fonctionner le module graphique avec l'ECS.

Autre nouvelle importante, j'ai trouvé du travail et je déménage sur Tours dans environ une semaine, je suis maintenant développeur de jeux professionnel débutant :D
Et cela, grâce à Nazara (c'est ce qui m'a permis de me montrer et de sortir de la masse), et donc grâce à l'importance que vous avez donné à mon projet. Merci à vous.

Je vous rassure tout de suite, je n'ai pas l'intention d'arrêter de travailler sur Nazara, qui ne devrait pas trop être impacté.

+5 -0

(Vous me pardonnerez mon copier-coller, mais mon temps est limité par celui de la compilation).

Bon, comme ça fait presque un mois je me permets de poster un petit bulletin de news.

Il y a eu quelques changements, mon déménagement est officialisé, je réside donc en France (pays de sauvage, impossible de trouver de simple chips au paprika) et vous l'aurez deviné, je suis en ce moment très fatigué :D

Non sincèrement, en ce moment je suis dans une fatigue mentale assez sévère, je dois penser à beaucoup de choses (par exemple je n'ai toujours pas de frigo, ni de carte visa, ni vraiment de meubles, ni d'ordinateur correct) et bien entendu, j'ai mon nouveau travail à côté. De ce côté-là, tout se passe bien, même si j'ai toujours du mal à programmer des choses "basiques" pour la simple raison que je ne connais pas encore vraiment bien ce framework d'un million de lignes de code.

Néanmoins, il m'arrive assez régulièrement de profiter de ma pause de midi pour avancer sur Nazara, tout comme le soir d'habitude (sauf certains soirs comme celui-ci où je ne pense qu'à dormir).

Côté Nazara donc, ça avance plutôt pas mal en ce moment, pour vous en convaincre je vous invite à regarder le graphe des différentes branches (je ne travaille presque qu'exclusivement sur la branche du SDK en ce moment).

J'ai donc commencé à me débarrasser des SceneNode, des Scene, etc., notamment aujourd'hui je me suis enfin débarrassé de la classe LightManager qui n'avait pas vraiment de raison d'être (et qui était très mal nommée).

Le moteur compile à nouveau, mais est inutilisable pour l'instant (sur la branche NDK, la branche master continue de fonctionner) et le restera tant que je n'aurai pas terminé. Néanmoins ça progresse (les classe Light, Model et SkeletalModel sont désolidarisées de SceneNode, il me reste donc les particules et les sprites dont ceux de texte).

Une fois que tout ceci sera enfin terminé (courant de ce mois je dirais), je commencerai l'implémentation du Shadow-Mapping pour me remonter le moral (ça fait un moment que le moteur graphique n'a pas progressé) avant de pouvoir enfin commencer un jeu, un tutoriel digne de ce nom et une documentation.

Voilà pour les nouvelles, de cette façon j'espère rassurer les pessimistes, le moteur va très bien et rien n'a changé. Si ce n'est que je suis dans une période un peu compliquée de ma vie pour l'instant.

Oh j'oubliais, j'ai pris la décision de faire évoluer le code du moteur pour qu'il atteigne le même niveau que celui du SDK, cela implique notamment un namespace au lieu des préfixes, si si, le truc que tout le monde me réclame depuis trois ans.

Reste à trouver un namespace plaisant, Ndk est pour le SDK du moteur, orienté jeu vidéos et plus haut niveau, il m'en faut maintenant un pour le côté bas-niveau, c'est-à-dire celui qui est actuellement préfixé. N'hésitez pas à m'en proposer car pour l'instant je n'ai rien trouvé.

Suite à cela, j'ai ouvert un sondage permettant de répondre à trois petites questions vis-à-vis du moteur.

Sachez que je suis désolé d'être (beaucoup) plus présent sur OpenClassrooms, mais le fait est qu'il y a plus de monde qui passe sur le topic OC que sur les autres, et c'est bien dommage (n'étant pas un grand fan d'OC). Mais ne vous inquiétez pas, je poste les véritables avancées partout :D

Au passage, hier soir j'ai réussi à obtenir un rendu avec Nazara sans utiliser les Scene et SceneNode mais en utilisant le SDK, ce qui est une avancée considérable :)

En vous souhaitant une bonne journée !

+3 -0

Tiens moi j'ai un question en tant que Artist 3D, tu n'en est peut être pas la mais au niveau des shaders c'est du PBR ?

stilobique

Pas pour l'instant, c'est du simple Phong Lighting, mais le PBR fait partie des choses prévues (et si j'ai bien compris, ce dont je doute toujours, ça n'entraîne qu'une modification des matériaux et le rajout de nouveaux ÜberShader).

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte