Salutations, voyageur !
LE BLOC DE RÉCOLTE EST ACHEVÉ ! Et ce n'est pas une mince affaire que de le proclamer bordel de uc §§ Si je devais utiliser une métaphore pour décrire ce segment de développement, c'est qu'on était partis pour une petite randonnée tranquille dans les collines, avec un jambon-beurre et un pepsi dans le sac. Une tempête s'est levée, on a aperçut en exclusivité la première meute d'ours bruns EVER qui cherchait de la viande fraîche pour se nourrir, pour finalement se retrouver emportés par un glissement de terrain et revenir à la maison un mois plus tard EN PORTANT LES CORPS EN DÉCOMPOSITION DE NOS GUIDES NATURE AAAAAAA-
La note de patch
"Un monde ouvert procédural avec des milliers de ressources à gérer, c'est toujours un super projet pour commencer le game dev." - littéralement personne sur Terre.
Pour vous remettre en mémoire les objectifs de ce segment numéro 5, nous avions comme but d'implémenter la génération des biomes sur notre carte de jeu, de placer procéduralement toutes leurs ressources ainsi que de les rendre récoltables (soit à la main, avec un outil, et au travers de deux mini-jeux en ce qui concerne les arbres et les gisements de pierre).
1) Dessine-moi un marais: Tout commençait bien avec l'élaboration d'un code de génération de biome, inspiré d'une version créée il y a presque un an. Le principe est simple: on génère des splatmaps procédurales sur une texture 2D, où chaque canal couleur d'une texture correspond à la présence, ou non, d'un biome. Par exemple, ma première texture stocke la présence de prairie (rouge), forêt (vert) et bleu (plateau). La seconde gère les rivières (rouge) et les marais (bleu) (des images de ces textures sont disponibles un tout petit peu plus bas dans la compilation de GIF et d'images du bloc).
On assemble ces deux textures avec un algorithme de priorisation, et tada, on obtient une magnifique carte 2D de la répartition de nos quatre biomes principaux et de notre rivière. Une fois ces informations stockées au chaud, on utilise un shader custom qui associe le canal couleur de chaque texture à un visuel de biome. On l'applique au mesh du sol de notre monde, et voilà, on marche sur des feuilles dans les sous-bois, ou sur de la boue dans les marais. En bonus, on s'est même permis de diviser le mesh de la carte en chunks plus petits, qui sont masqués par occlusion lorsqu'ils s'éloignent trop de la caméra du joueur. Pas besoin de calculer le rendu graphique d'un bout de terre que ne vous voyez pas.
Côté données, une fonction nous permet rapidement de savoir à quel biome correspond un point précis sur la carte. Cela nous permet de savoir où instancier nos ressources pour chaque biome. La méthode est simple, les paramètres sont faciles à changer dans un écran de personnalisation de carte, et les résultats sont immédiatement disponibles à la prévisualisation - bref, tout va bien.
2) Je récolte fort, moi monsieur, je récolte: J'ai ensuite attaqué l'extraction des ressources en elle-même. Après avoir joyeusement créé mes arbres et mes veines de pierre avec trois meshs collés ensemble avec du scotch, j'ai programmé les classes de récolte, dont une spécifique aux plantes, qui nous servira plus tard dans la gestion de leur croissance et de leur reproduction.
L'ensemble des ressources récoltables peut, pour l'instant, se classer dans les catégories suivantes:
- récoltable à la main, avec un très court délai (fleurs, plantes au sol, une ruche...)
- récoltable à la main, un peu plus long, et en quantité limitée sur chaque ressource (les branches fines d'un arbre, les petites pierres d'un gisement)
- récoltable uniquement avec une serre-faux, qui permet d'exploiter complètement le noeud (détruire un buisson large par exemple, ou débiter un tronc tombé au sol)
- et les récoltables spéciaux, j'ai nommé les gisements de pierre, et les arbres matures, qui ont besoin d'une serre-faux et qui ont en exclusivité un splendide mini-jeu chacun. Ce mini-jeu est optionnel, mais le réussir permet de remplir une barre de récolte qui vous donne des ressources bonus et qui peut grandement accélérer la récupération de ressources.
La serre-faux est un outil polyvalent manié par les vyrrlins. Vous n'aurez jamais à jongler entre pic, hache, houe, faux, marteau et j'en passe: la serre-faux est universelle, et sa qualité comptera beaucoup dans la productivité de son propriétaire.
On a donc tout un système opérationnel de récolte où votre personnage s’attelle à sa tâche et débite peu à peu les objets qu'il est possible d'obtenir sur une précieuse ressource naturelle. Ce système est robuste et devrait être là pour durer, même s'il sera possible d'ajuster certaines mécaniques pour qu'elles aient un feeling fluide dans le jeu complet.
3) Et là, j'ai planté 3 500 chênes sur ma carte: Une fois codés mes splendides arbres dont les troncs majestueux chutent au sol, ainsi que mes gisements de pierre qui éclatent en vomissant leurs blocs bruts, j'ai essayé de les instancier procéduralement dans mes biomes.
J'ai donc, avec mes gros doigts, codé l'algorithme pour les placer sur ma carte. J'ai fait ça bien, avec une densité spécifique à chaque ressource, des espacements moyens, des coefficients d'agglomération pour essayer de faire en sorte que certaines s'instancient par paquets. J'ai bien implémenté une vérification supplémentaire pour éviter que les ressources ne se chevauchent entre elles. Et fière de moi, j'ai tout lancé, full speed no fear sur mon PC dont la configuration date de 2017 (ne vous inquiétez pas que si vous rencontrez des problèmes de performance dans le jeu final, je les aurais vus bien avant vous).
Eeeeeet ma fenêtre de jeu a chuté de 140 fps à 70, avec des creux à 50, voir moins, dans ma forêt de pins sur nos plateaux. Des sueurs froides ont commencé à couler sur mes tempes alors que j'observais le nombre total de nœuds dans l'arborescence de la scène - plus de 20 000, au bas mot. Sur une carte de 500 mètres de large, quand la version finale en fera au moins deux fois plus.
J'ai compris qu'on allait avoir un léger problème.
Le premier, c'est le nombre absolu de nœuds dans l'arborescence. Même s'ils n'ont pas de process actif, et qu'il n'y a rien qui itère particulièrement dans leur liste, ça ne peut pas être bon pour la performance d'avoir autant d'objets 3D existants en jeu.
Le second, plus concret, c'est que cela représentait entre deux et six MeshInstance3D par ressource à afficher, PLUS leur collider à gérer comme corps physique indépendant. Même avec des modèles extrêmement simples, l'occlusion et les matériaux partagés, la charge est trop lourde à porter pour le processeur et la carte graphique. Alors je ne voulais pas imaginer ce que ça donnerait avec des vrais visuels texturés plus tard.
Après m'être roulée en boule quelques heures dans un coin de la pièce, j'ai décidé que ce résultat était inacceptable, et je suis repartie à la reconquête de tout ma gestion des ressources, la bave aux lèvre. Je me suis armée des deux leviers massifs d'optimisation rendus disponibles par Godot : j'ai nommé les MultimeshInstancers3D, et le PhysicsServer3D.
Car voyez-vous, les nœuds dans l'arborescence de la scène sont une façon très pratique et commode pour gérer des objets ponctuels (notamment nos créatures). Mais en ce qui concerne nos six-cent buissons de baies sauvages, ces nœuds deviennent très superflus et inefficaces. J'ai donc dissous tous mes Node3D et mes ressources sont devenues intégralement "immatérielles", et plus spécifiquement, orphelines.
- Leurs meshs sont gérés par des MultiMeshInstancers, répartis en grille de 3x3 sur la carte. Chacun d'entre eux s'occupe d'afficher les X instances de la ressources aux positions, rotations et échelles demandées. Les MultiMeshInstancers sont de petites merveilles d'optimisation qui permettent de regrouper des milliers de modèles en un seul appel de rendu au lieu d'un par instance (comme c'est le cas avec un MeshInstance3D classique). Et en activant l'occlusion sur ces objets, les carrés de ressources trop éloignés deviennent invisibles pour économiser du temps de rendu.
- Leurs colliders sont directement créés et stockés par le PhysicsServer3D - une classe plus bas niveau que les CollisionObjects, et qui permet de manipuler les corps physiques et leurs formes in-game. Sans la couche logicielle des nœuds de scène, cette gestion est rapide comme l'éclair. Lorsqu'une créature se heurte au tronc d'un arbre, ou que votre personnage détecte une ressource récoltable sous votre souris, c'est l'une des forme de ce corps physique de ressource qui est reconnue sans passer par l'arborescence in-game.
Chaque instance de ressource est associée au même index pour ses meshs, son collider, ou ses données. Donc, quand je demande le saule n°32 du carré haut-centre de la carte, mon système sait exactement où sont stockés ses visuels, son collider, et ses informations de plante. Il n'y a plus aucun Node3D qui existe dans mon arborescence.
Le système n'est au final pas le plus complexe qui soit - mais j'ai du repartir de zéro pour gérer mes ressources et je n'avais jamais manipulé autant d'objets sous forme abstraite. Il y a certes une couche de complexité supplémentaire par rapport aux nœuds, mais le gain de performance est inouï. J'ai retrouvé mes 140 fps fluides et ma récolte de ressources fonctionne parfaitement. Je sais désormais que si j'ai besoin d'optimiser l'affichage des objets tombés au sol, ou de mes zones de collisions de linceul, je sais exactement vers où me tourner.
4) Ce n'est pas fini mon bro: Me voilà à genoux, pleurant des larmes de joie et remerciant tous mes dieux funestes d'avoir résolu mon épineux problème de performance. Mais vous savez ce qu'il se passe lorsque vous abattez un arbre, et que son tronc tombe à terre ?
IL EST SENSÉ BLOQUER LE PASSAGE, voilà ce qu'il se passe §§ Et de toute façon, vu qu'au segment prochain on va commencer à placer des bâtiments et des objets sur le chemin, il fallait bien qu'on gère le rafraichissement du pathfinding, qui, comme le reste, n'est pas "automatique olololz".
Si mon IA est codée à la main, j'utilise le système natif de navigation de Godot pour faire trouver leur chemin à mes créatures. Il fonctionne très bien sur terrain plat, mais jusqu'ici, je n'avais pas vraiment à me soucier du mesh de navigation (les polygones qu'utilise le système pour déterminer quelles aires sont accessibles et quel est le plus court chemin vers un point donné). Il était calculé au lancement du jeu pour correspondre aux extrémités du carré de la map, et ensuite, tout le monde l'oubliait.
J'ai donc demandé au code de mettre à jour le navigation mesh à chaque chute d'arbre pour que mes entités évitent correctement l'obstacle, et, SURPRISE, LE JEU FREEZE ALORS PENDANT DEUX SECONDES COMPLÈTES.
Le NavigationServer3D parcoure toute l'arborescence de la scène pour collecter les points et les faces des meshes, et les positionner sur 500x500 unités de carte. Le processus verrouille le thread de calcul principal, et le jeu est complètement bloqué le temps que l'opération se termine. Aïe.
Après une nouvelle heure en position latérale de sécurité dans le bureau, et une bonne goulée de la documentation du NavigationServer3D, j'ai décidé, comme pour l'affichage des meshes, de fractionner mes régions de navigation en de nombreux chunks connectés, pour qu'ils puissent être mis à jour indépendamment les uns des autres. J'ai passé Godot en version 4.3 profiter d'une connexion fluide entre ces chunks, en jouant avec leurs limites de baking et leurs bordures.
Le résultat, est le suivant:
- Je calcule séparément mes données de navigation du sol, des ressources, et des obstacles instanciés, et je cache ces données abstraites sur un nœud manager (sous forme de NavigationMeshSourceGeometryData3D). Je vais directement récupérer toutes mes informations de géométrie via le script et pas par l'arborescence de scène. Le gain de performance est déjà considérable.
- Ces données sont réunies, à volonté et virtuellement sans coût, en UN SEUL mesh pour toutes les régions de navigations. Elles réalisent ainsi toutes leurs calculs sur le même ensemble de points et sont cohérentes entre elles.
- Lorsque j'ai un changement à effectuer, je ne recalcule mes données que pour le type d'obstacle concerné (une ressource virtuelle, ou un Node3D instancié), et je ne demande un rebake QUE pour les régions locales, concernées par la mise à jour (4, tout au plus, si l'obstacle traverse la frontière entre plusieurs régions).
Leur taille était faible (et plus précisément, leurs BAKING BOUNDS étant restreintes), la mise à jour prend en moyenne 30 ms et tout est fluide. Les cheminements de mes créatures changent en temps réel si quelque chose se met en travers de leur route.
5) Les détails bonus: En prime, nous avons corrigé plusieurs bugs en pagaille, assuré la bonne reconnaissance de tous les colliders dans le navigation mesh (certains troncs, trop peu larges, ne bloquaient pas le passage des créatures), amélioré la visibilité des FX provisoires d'attaque, et préparé toute la base de notre arbre de ressources.
Le commentaire
Ce bloc de développement est, de loin, celui qui m'a le plus appris à propos de Godot. MultiMeshInstancer3D, PhysicsServer3D ou NavigationServer3D - les trois sont des classes exceptionnelles qui devraient être les outils primaires d'un développeur souhaitant maîtriser de larges quantités d'informations et des cartes de grande taille.
Bien sûr, les nœuds resteront indispensables - pour mes vyrrlins, mes bâtiments, mon soleil ou mon UI - qui se comptent sur les doigts d'une main. Mais je suis impressionnée des gains de performance et du contrôle accru procuré par ces classes qui, au final, n'ont rien de très sorcier à utiliser. J'avais vu des exemples de bullet hells ou des animations procédurales de bancs de poissons, où les nœuds deviennent impossibles à optimiser - mais je ne pensais pas qu'on aurait besoin de systèmes abstraits si tôt dans la construction de Lampyre.
Jusqu'à preuve du contraire, afficher toutes ces ressources sur une aussi grande carte, est pour l'instant mon plus grand défi d'optimisation - et on l'a franchi - DEUX FOIS, si on compte mes embrouilles avec le navigation mesh. Il faut bien comprendre que si je n'avais pas trouvé de solution à ces embûches, il aurait été quasiment impossible de continuer à développer le jeu tel que je l'imagine aujourd'hui.
Je ne me fais pas d'illusions sur le fait que nous tomberons sur d'autres joyeusetés imprévues, comme l'implémentation du système de sauvegarde (il arrive bientôt lui o__o j'ai peur), ou quand il sera temps d'optimiser les shaders de végétation et les assauts des ombres. Mais pour l'instant, on a vaincu avec brio ces murs qui sont apparus devant nos nez, et j'en suis très contente.
Quand j'aurais un peu sorti la tête du guidon des systèmes de récolte, je pense que je pourrais commencer à réellement apprécier nos mécanismes d'instanciation et d'extraction des ressources, qui, à l'heure actuelle, me permettent d'ajouter rapidement n'importe quelle plante, gisement, objet, à ma carte, et à mon jeu.
La suite
Maintenant qu'on est ressortis secoués (chokbars ?) mais plus forts de ce terrible bloc, nous pouvons enchaîner avec l'un des POURQUOI on extrait des ressources, j'ai nommé, la construction !
Un prémisse de base de Lampyre est la fondation de votre propre enclave pour abriter vos villageois et combattre les ombres. Et, oui, même si vous faites le margoulin et que vous essayez de jouer sans alliés, vous devrez toujours avoir un logis et des ateliers pour espérer détruire les autels de corruption. Alors, il me tarde d'implémenter ces mécaniques de placement et d'édification de structures.
Les mots d'ordre sont toujours les mêmes - simplicité, cohérence, satisfaction. Placer une structure doit être facile, choisir ses matériaux agréables, et progresser dans le chantier régalien. Et, évidemment, ce sera également un plaisir d'assigner vos villageois aux chantiers et de les voir emmener les matériaux pour bâtir votre village.
Attention, j'ai bien dit construction, et pas fabrication. Nous nous intéresserons au raffinage des ressources et à l'artisanat dans le bloc suivant - pour le moment, je vais instancier mes matériaux raffinés comme un gros rat dans mon environnement de test pour bâtir mes édifices (elle hackou le jeu pour son propre confort §§ woaow §).
Les fonctionnalités à implémenter pour ce bloc numéro six, sooooont...
- une interface, ou un menu de construction de structures - possiblement accessible via l'autel du village
- l'implémentation du concept de bâtiment et des presets principaux de structure (pas de construction de murs intégralement libre, il n'y a que peu d’intérêt à cela - la personnalisation sera en revanche très poussée sur les matériaux, les décorations, les sols, les objets...)
- le placement d'une structure (de son chantier) sur la carte, incompatible avec des obstacles, avec rotation et prévisualisation instantanée
- la progression organique et visuelle d'un chantier, avec l'apport de ses matériaux de base et leur travail
- les éléments des bâtiments doivent avoir une durabilité dépendante de leurs matériaux, et la possibilité de se dégrader, voir de se casser (une pièce ou un atelier n'étant plus fonctionnel si trop abimé)
- on doit pouvoir réparer les bâtiments et les structures
- si un bâtiment ou une structure est totalement détruit, on doit pouvoir récolter et recycler ses débris qui barrent la route
- je souhaite, à la fin de ce bloc, être en mesure de construire un preset entier de bâtiment avec des murs et du sol, une structure plus légère (e.g un atelier de taille de pierre), et des structures accessoires (lit, lanternes, pile de bois)
- il est possible qu'on ait alors besoin d'un shader custom pour s'assurer que le joueur soit visible, même derrière des murs ou sous un toit (il le faudra de toute façon pour voir ce qu'on fait au travers du feuillage dense qui sera présent dans les forêts)
Et nous verrons bien si d'autres besoins pointent le bout de leur nez ; en espérant ne pas reprendre le même mur d'embûches que pour les ressources (je vais leur optimiser leur uc aux bâtiments, il n'y aura aucun nœud superflu C'EST MOI QUI VOUS LE DIS).
Les visuels du schnaps
Et hop, compilation de nos GIF essaimés sur Twitter. Paradoxalement, il n'y en a pas tant que ça en comparaison du temps que nous a pris ce chapitre de développement, car le plus gros de la refonte de la gestion des ressources est littéralement... eh bien, invisible. C'était le but.
3) Nos première récolte de ressources tout simples: des fleurs ! Pour des petits éléments, nul besoin de serre-faux, et les objets vont directement dans votre inventaire si il y a de la place.
6) Le mini-jeu de récolte de gisement de pierre ! Casser les surfaces colorées remplit la barre de bonus (orange). Arrivée à 100%, le gisement éclate automatiquement et vous donne jusqu'à 30% de matériaux bonus en plus. Vous pouvez aussi ignorer le jeu, et attendre que la barre de vie (verte) descende à zéro.
L’intérêt de ce système c'est que ce QTE peut largement être customisé et enrichi de fonctionnalités (combo streaks, vitesse, forme du curseur et des zones à casser, points chauds à éviter, variations selon la qualité de votre outil ou vos points de compétences). Les gisements les plus simples seront une formalité à briser, tandis que vous pourrez tenter votre chance avec les plus solides si vous jouez le jeu.
Quand ce seront vos PNJ qui récolteront les gisements, ils essaieront aussi de remplir la barre bonus, en fonction de leurs propres compétences ! Les meilleurs mineurs et bucherons seront fort productifs.
7) Mes modèles de ressources provisoires. On ne peut pas faire plus "placeholder" - mais avec l'affichage de plusieurs meshes pour faire une seule ressource, je me suis assurée que mon système sache gérer des instances complexes dans le futur.
8) La période post-PLS, où j'ai décidé de refondre tout mon système d'affichage vers les MeshInstancer3D et le PhysicsServer3D. Je testais ici la reconnaissance des arbres par notre personnage-joueur. Ce système de détection a par la même occasion été optimisée, que ce soit pour les ressources, les objets au sol, les ennemis...
9) Ballade matinale dans mes biomes optimisés. Les plaisirs simples.
10) J'ai aussi dû adapter l'affichage des barres de vie des ressources orphelines ! Il est important de pouvoir voir si un arbre ou un gisement a déjà perdu des points de vie, ou si sa barre de récolte bonus a commencé à se remplir. Ces barres de vie, auparavant fixes pour chaque nœud, sont maintenant éphémères, et se détruisent une fois leur durée d'affichage expirée.
11) Le drame des NavigationMesh. Même en ayant enlevé mes ressources de l'arborescence, le rafraichissement bloque encore le jeu pendant plus d'une seconde. En en prime, le collider du tronc est ici trop petit pour "percer" le mesh, et être considéré comme un obstacle.
12) Mise à jour des régions de navigation, après refonte et cache des données. Ici je m'amusais à instancier aléatoirement des "cubes" géants d'obstacle sur la carte. L'ombrelgin qui me suit est là pour me confirmer que le pathfinding des créatures fonctionne correctement pendant les rafraichissements.
13) Une chute d'arbre fluide ! VICTOIRE ! Il fait un trou dans le navigation mesh, et les créatures éviteront de se coincer comme des débilus sur son tronc.
14) Le dernier test du bloc de développement. Une aube tranquille de récolte sous escorte armée.
Comme d'habitude, vous pouvez me suivre sur @Ariatowl pour avoir des nouvelles régulières du développement. Je code quotidiennement, et je n'hésite pas à ouvrir mon gros bec pour m'esbaudir de mes polygones qui s'agitent sous mon commandement.
Prenez soin de vous, on se voit quand tout cela est codé !