Fusion de la chambre intermix

Suite à l’article précédent j’ai voulu faire un tour de nettoyage et je me suis rendu compte qu’un ancien démon revenait à la charge. Nous avons divisé le code par domaine et contexte de rendu, propre et héritant d’un parent commun, et dans un service je mets le code commun à ce qui concerne Grid, ou Iso etc., de manière à isoler les calculs de l’usage selon le contexte.

Schéma avant fusion

Plus simple avec un schéma, voici la découpe avant fusion. Ce qui nous intéresse c’est la séparation 2D et Gl, puis la découpe par usage/type à savoir Sprite, Grid ou Iso, puis les regroupements de type IsoElement/GridElement, ainsi que des services relatifs aux couches.

On a une sorte de matrice à 3 dimensions en ce qui concerne cette idée. Sauf que programmer ça, en TypeScript, ben c’est pas très évident. En PHP j’aurai pu utiliser des Traits, il existe des mixins en javascript mais non merci, je vous laisse vous faire votre avis mais ce n’est pas à la hauteur. L’héritage multiple n’existe pas (cf mixin), du coup il faut savoir se renouveler et faire preuve d’audace, d’expérimentation et de refontes inévitables. C’est ce que j’ai dû faire, non sans mal.

J’étais parti pour déplacer les fonctions de rendu dans les services par couche (Grid/Iso) et de fusionner Grid2DElement avec GridGlElement, vu que leur différence réside dans le contexte de rendu (2D/Gl). Mais je me suis aperçu que bien que je gagnais en clarté à tout regrouper, on augmentait d’autre part la difficulté de ce même code et des approches. Bref, bien, mais pas bien.

Du coup revirement de situation et revenons sur nos pas de plusieurs mois quand on a justement décidé de diviser par contexte de rendu, quand les lumières sont arrivées (la version solutionnée). Revenons donc à cette idée non divisée et sans emmerder les services déjà très bons tels quels.

C’est la fusion !

Fusionner Grid2D et GridGl, Iso2D et IsoGl, ok, mais on oublie Sprite2D et SpriteGl qui héritent de SpriteElement, il faut commencer par le commencement. C’est donc une refonte jusqu’à Element pour répartir les morceaux des différentes classes Sprite*. Ainsi disparaissent Sprite2DElement et SpriteGlElement au profit d’une nouvelle classe SpriteElement toute équipée.

Quand on parle de fusion on parle bien entendu de gérer le contexte de rendu au sein même de la classe. Ceci peut paraître étrange et contre certains bons principes, mais ces morceaux de code partagent parfois jusqu’à 90% du code, ce qui va contre le principe DRY (Don’t Repeat Yourself) et comme j’ai envie de bisous (KISS : Keep It Stupid Simple), j’ai tout regroupé et cela ne m’a demandé que quelque if peu coûteux, ce qui est très important car on appelle ces bouts de code des centaines, des milliers de fois par seconde (selon complexité de votre projet).

Fusion de Sprite*Element

J’en profite pour illustrer le service de rendu Gl et montrer à partir d’où on le connecte. J’en affiche un peu plus mais ainsi on voit les 2 types de regroupement GridElement et IsoElement. Ne prêtez pas attention à GridBlock, vous le connaissez déjà.

Nous sommes bien parti, continuons. On crée une nouvelle classe GridElement et on met ce que contient Grid2D et GridGl, hop tout dedans et on essaye de faire coller les morceaux. Là où ça se corse c’est de bien segmenter les parties communes et spécifiques, puis quand on arrive à IsoElement c’est encore pire car nous sommes basé sur l’héritage donc on ne réécrit que ce qui a besoin de l’être et là on observe des couacs, des oublis pour la 2D vu qu’on s’est concentré sur Gl depuis les lumières. Par exemple la table en 2D ne fonctionne pas, juste en Gl.

Fusion terminée

Sur papier ça parait plus beau, naturel, élégant, [mettre ici tous les beaux mots que vous désirez]… Mais dans la pratique ça demande pas mal d’efforts, de compréhension, évidemment c’est pour un mieux !

C’est quand même un impact de 33 fichiers dans le projet et son POC de démo, 8 dans TARS même, ainsi que 8 suppressions et 2 ajouts. Ça c’est pour les fichiers, mais en terme de lignes de code, même si je n’ai pas de compteur à cet instant, on a effectué une réduction notable, donc plus facile à maintenir, de par le regroupement aussi.

Preuve que ça fonctionne toujours, même si la table n’est pas gérée encore correctement en 2D.

Garder 2D et Gl ?

Pourquoi garder les 2 ? Car il est très difficile de débuger en Gl, vous ne pouvez pas dessiner aisément un repère ou une trace sans sortir les chars d’assaut et beaucoup d’heures de dev alors qu’en 2D vous êtes libre de manipuler le rendu en direct et ce rapidement.

C’est pour cela que le POC (démo) n’utilise plus les lumières en 2D, le but pour moi ici étant un debug rapide sans ajouter des soucis de performances, qui plus est, connus.

De plus, maintenant que la fusion est faite, on pourrait revoir tout le workflow d’utilisation pour ne plus devoir faire (à l’usage) une scène 2D ou une scène Gl. ainsi en changeant juste le mode, toutes les classes personnelles seraient utilisables, ce qui serait un chouette gain de temps et d’effort. De plus les 2 fonctions ajoutées is2DContext/isGlContext permettent d’agir spécifiquement si besoin était.

Et le fameux ensuite ?

Ah ben oui, ensuite quoi ? Corriger le pourquoi de la table en 2D et tenter de régler un conflit Grid/Iso sur un calcul de positionnement.

Il faut absolument faire un POC purement Grid et non Iso, genre un Mario (S)NES ou Duke Nukem 1-2, un truc tout carré pour voir que les calculs sont bons, juste une grille décorée, pas plus.

Ensuite, mes fameuses particules lumineuses et le miroir :p !

Mais bon, comme d’hab on verra ce qui me stitch comme on dit. Je vais déjà de ce pas fusionner les branches du repo et repartir sur une base saine :).

Edit

Pour ne pas refaire un article juste pour ça, j’ai également fusionné les classes du POC (treasure, door, player), leur code était identique. 3 classes de moins sur les 6 initiales (3x2D et 3xGl). Une bonne chose de faites qui va nous simplifier la vie ultérieurement. Les scènes restent dissociées car différentes, la Gl, plus complète, gère la lumière par exemple, les fusionner ne donnerait rien d’intéressant une fois en release. Au moins le debug (2D) peut se faire sans gêner le résultat final (Gl). Nous voilà dans un état propre, quelques corrections de bugs ou de manques seraient à faire pour solidifier avant d’avancer plus.

Masque splendide

Tel Stanley Ipkiss, TARS joue maintenant avec un masque lors des rendus d’objets occupant plusieurs cases dans notre contexte isométrique. Mais pourquoi ?

La table passe sur le Héro car elle est rendue après.

C’est là qu’il faut vous expliquer comment ça fonctionne, ce qui n’est pas une mince affaire. La table occupe 2 cases, mais comme tout Element, il a une coordonnée qui ici nous dit {x: 8, y: 2} (rappel on commence à zéro) et notre Héro, lui, arrive sur la case {x: 9, y: 1}, ce qui de part notre mécanique de rendu Isométrique dessine le Héro avant la table et donc la table écrase le dessin du Héro. Vous suivez jusque là ?

Ceci illustre bien l’ordre de rendu (dessin), de haut gauche, vers la droite jusqu’en fin de ligne, on passe à la ligne suivante et on repart de gauche à droite.

1 problème ?

On a donc un problème, le Héro devrait être dessiné devant la table, ce qui correspond à notre logique visuelle. Même si on inverse le sens de dessin, en colonne au lieu de ligne, on aura le même soucis avec l’autre table.

Après de longues recherches et essais, une seule solution : la manifestation ! découper l’image en 2 pour occuper chacune une case. Car le problème n’est pas que cette erreur de dessin mais aussi le pathfinder qui passe au travers de la case qui n’est pas affectée (en mémoire), elle ne l’est que par vos yeux; et enfin la lumière qui voit la table tantôt d’un bout (loin), tantôt de l’autre bout (proche) et donc l’éclaire différemment.

3 problèmes ?

Le pathfinder se base sur la présence d’un Element sur la grille que l’on fabrique sur base du subsetMap, et la lumière, chacun ayant son algorithme. Le rendu se base directement sur la subsetMap. On serait tenté de dire qu’il faut solutionner les 3 individuellement et c’est ainsi que j’avais commencé.

J’ai donc ajouté une notion d’additionnalCoords (coordonnées additionnelles) dans la définition de l’élément table et ce pour les 4 orientations NESW, décrivant, dans le cas de notre table orientée vers le Nord et dont la base se trouve en bas gauche de l’image, une case additionnelle en x: 0, y: -1.

Le choix de la coordonnées {0,0} vient du sens de rendu et du sens de détection du raycasting quand on clic droit sur un Element (coffre, porte). La dernière case dominera les précédentes comme démontré par le problème. Le raycasting parcours l’inverse du rendu pour trouver l’élément le plus devant. Notre {0,0} sera donc cette dernière case a être rendue et correspondra à la coordonnée de l’Element, les additionalCoords représente l’ensemble des autres cases en mode relatif (ex: {x: -1, y: 0} pour la table orientée vers l’Est).

Raycasting ? 4 problèmes donc ?!

Maintenant que vous comprenez la structure, le pourquoi du comment et la mécanique céleste, nous allons tenter de corriger les problèmes un à un.

Le plus facile est le pathfinder, qui se base sur des cases occupées pour dire que l’on ne peut s’y rendre. Actuellement le Héro se déplace sur la seconde case de la table « dessous » car la table se dessine après. Il faut donc arriver à lui dire « hey tiens voilà des coordonnées additionnelles à retirer !« .

Aussitôt dit, aussitôt fait, quand on prépare le pathfinder sur base de la subsetMap, on demande à tous les Elements s’ils ont des coordonnées additionnelles et ensuite on les retire du résultat final.

Fin ! 8D

Ah ah ah Oui mais non ! Ça fonctionne, certes, le Héro ne traverse plus les tables, ce n’est plus un drôle de fantôme. Mais ! La lumière n’est pas bonne, le raycasting reste un problème et évidemment notre problème de rendu reste identique, la table passe devant le Héro.

J’ai tenté une théorie visant à dire au GridBlock de prendre en compte des pointeurs, une forme de référence entre cases, mais en vain. Un GridBlock est un multi-ensemble d’Elements, point.

Je vous épargne toute la frustration et brisage de méninges, il m’aura fallu une bonne semaine pour trouver la seule piste envisageable.

Un système de masque

Dit comme ça, ça semble être la solution ultime, et c’est pas loin, mais incomplet. On garde les additionnalCoords et on s’en sert à l’ajout de l’Element sur la Map lors du chargement pour l’ajouter à chacune des cases, le même élément (la même instance, pas une copie). Notre table est donc physiquement présente en mémoire sur 2 cases.

Cela résout le problème de pathfinder et on peut retirer ce que nous avons fait précédemment. Le raycasting est aidé par cette approche mais il faudra l’aider (cf le rendu), nous verrons ça en fin d’article. Il nous reste la lumière et le rendu.

Table ajoutée 2 fois sur la coordonnée de l’Element au lieu des cases spécifiques.
Chacune des 2 cases de la table rend la même image, donc elle « fusionnent » visuellement.

L’idée du masque est de dessiner une partie de l’image sur chaque case correspondante, évitant de demander au graphiste de préparer un grand nombre d’images individuelles et de devoir se battre avec son éditeur de map, ce qui n’est pas gérable.

Chaque case correspond à un rendu à faire, la zone bleue donne B et la zone orange donne A.

Modification du système de rendu

Dans le cas où notre Element a des additionnalCoords il faut appliquer le masque, sinon le rendu classique qui va bien. Pour y arriver, il nous manque quelque chose, comment savoir quelle partie dessiner ? Actuellement nous dessinons selon la coordonnée de l’Element, du coup dans ce cas on dessine une table entière selon ses coordonnées ce qui donne l’effet de fusion illustré ci-avant.

Pour changer ça, il faut dire à notre système de rendu que ce n’est pas la coordonnée de l’Element qu’il faut utiliser, grosso modo. On va commencer par ajouter la notion de coordonnées au gridBlock, qui n’en avait pas besoin jusque là. Et qu’ils passent cette coordonnée à la fonction draw() pour que l’information arrive au système de rendu.

Et après on fait comment ? On se questionne, a-t-on des additionnalCoords ? Si oui, à laquelle correspond le x,y donné par le rendu ? De la on calcul un masque prenant en compte le déplacement dans l’image pour n’afficher que ce qui nous intéresse dans notre cas. Illustrons ça avec la conquête des erreurs de rendu.

Il y a bien un rendu par case, en ayant forcé la largeur de la source à une largeur de case, mais en ayant oublié la déformation de destination.
Correction de la destination, tout le monde à une case de largeur.
Premier résultat concluant.

Le Héro ne passe plus derrière la table ! Mais ?! Qu’est-ce-que c’est que cette drôle de table coupée ? On a un soucis, ok mais lequel ? On dirait que la « fenêtre » du masque n’est pas au bon endroit, mais pourquoi que dans la table orientée à l’Est (à gauche) ?

Pour déterminer ce qu’il se passe, j’ai essayé de dessiner le premier morceau, et on voit que le même problème apparait.

Tentons de voir à quoi ressemble chaque morceau sans considérer la case originale. Nous n’avons pas le même résultat, en vert on a une fenêtre aux bonnes dimensions et bien positionnée, en rouge non.

Pour tenter de comprendre, j’ai modifié la table Est en inversant son origine (en haut gauche au lieu de bas droite) et en modifiant son rendu. J’ai également ajouté un fond à l’image pour comprendre les dimensions gérées.

Inversons le rendu et affichons un fond à la transparence.

En rendu inversé pour la table Est ça fonctionne ! Mais pourquoi ? Qu’est-ce qui change ? Et là une théorie survient, le fait que la base soit au delà d’une distance de case dans l’image quand on calcule le masque, provoque ce décalage. Je vais vous passer les calculs et les correctifs spécifiques au masque, mais en résumé, on doit déplacer la coordonnée source dans l’image selon la théorie isométrique (par demi case en X/Y) mais aussi corriger le résultat par cette même théorie, car il faut rationaliser ce décalage au delà d’une distance d’une case. De plus ce dernier correctif doit être appliqué à l’inverse au positionnement de destination. J’ai tenté un dessin, mais même pour moi il n’a pas été simple de le schématiser pour le coder.

Résultat corrigé du rendu.

On a donc notre Héro devant la table, des tables bien positionnées (celle de l’Est a été déplacée pour les tests) et on a en même temps solutionné la lumière qui éclaire équitablement les 2 morceaux. Ceci est un effet de bord qui tombe à point et qui se base sur le fait que c’est la même instance du même Element qui est référencé dans les 2 cases et donc quand la lumière s’applique, c’est le plus proche qui est choisi et appliqué, par effet de propagation. Enfin, c’est ma déduction car je ne me suis pas amusé à le démontrer.

Bon ok j’ai regardé un petit peu en écrivant ces lignes, il se pourrait que la table aie une addition de quantité d’éclairage par case, il faudra donc vérifier ça et décider du comportement. Vu les formes découpées, je ne suis pas sûr qu’un éclairage non uni soit une bonne idée.

Et le raycasting ?

On y vient, comme dit plus haut, c’est ici que ça se passe. On est vite tenté de dire qu’on a fini car nos problèmes visuels s’en sont allés, mais que nenni, il nous en reste un beaucoup moins visible : le raycasting. Pour ceux qui n’ont pas suivi, le raycasting (lancé de rayon), permet de déterminer dans notre cas ce sur quoi on clic (quand on clic droit sur le coffre, la porte, ou même un arbre).

Donc ici, pour chaque case, il se croit être à l’origine et m’est avis que ça va pas nous aider. Il va falloir, car ce n’est point encore fait, lui expliquer à lui aussi le système de masque. Par extension, on pourra peut-être globaliser et « simplifier ».

Prochaines étapes

En me relisant, je remarque que je parle souvent de l’éditeur mais pas cette fois; ni du jeu que nous allons démarrer comme projet vivant de l’usage de notre moteur TARS, et oui toujours dans le monde de Nahyan, nous y reviendrons prochainement; Mais plutôt vous parler de système de particules et de miroirs qui me sont venu à l’esprit et tant qu’à faire, une démo technique avec une petite explosion dont chaque particule est luminescente et se reflète dans des miroirs…

Smooth crimilight

Hier nous parlions de lumière, de beaux éclairages et de couleurs. Aujourd’hui, nous allons parler de douceur et de déplacement, mais parlons aussi de Disco et d’épilepsie !

Actuellement, la lumière et ses effets sont actualisés à certains moments, nous évitons ainsi les calculs perpétuels inutiles. Mais nous aimerions que la lumière suive notre héro Squellettore quand il se déplace et non pas que quand il traverse une case, ce qui nous donne un clignotement de refresh global quand on change de case. Ça nous fait un effet rétro mais on peut mieux faire.

L’idée est de diviser la lumière en 2 dès que l’on commence à bouger. Une lumière à X% de là où on part et une seconde à Y% là où on va (la case d’à côté dans le sens actuel). La somme donnera un résultat équivalent mais permet de graduellement faire transiter la lumière d’une case à une autre en douceur.

En regardant l’image ci-dessus, en prenant les 2 points vert comme étant le centre de chaque case et en considérant 100% la distance entre les 2, on peut facilement se représenter un delta qui représente la quantité de déplacement au départ de l’origine. Ainsi, tant qu’on est sur la case d’origine on aura X% = 100% – delta et Y% = delta. Je vous épargne les quelques complexités et conditions dues à mon implémentation initiale.

C’est là que le Disco est né au fond d’un donjon… Ça c’est quand modifie la force de la lumière, entre autres… et ce n’est pas ce que l’on veut exactement. Il faudrait altérer la lumière par case, pas sa force.

Pour arriver à cela, j’ai dû améliorer les informations de lumière en ajoutant la notion d’intensité qui altère le résultat sur la force de la lumière. Vous me direz « c’est quoi la différence ? » et je vous dirais que la force donnera la distance à laquelle la lumière éclaire (5 cases par exemple pour Squellettore et 3 pour le coffre) tandis que l’intensité altère la valeur par case uniformément. C’est ce qui a permis de faire glisser la lumière d’une case à l’autre.

Oh douceur du déplacement progressif remplaçant le changement saccadé brutal. Joie !

À cela on ajoute un code refactorisé, simplifié, plus facile à suivre et documenté. Ahhh TODO que j’aime tant… te retirer. C’est bon, on la garde !

Illumineux

Ça y est, j’ai pris à bras le corps le sujet des lumières qui étaient encore à l’essai, ramasser les TODOs (pas tous) et figer dans le système et son POC, ces mécaniques.

Ainsi Squellettore notre héro et le coffre sont détectés dans la subsetMap comme éléments lumineux. Ils contiennent une ébauche d’informations de lumière à savoir une couleur, une force et un état allumé/éteint.

Là où nous appliquons une modification de luminosité (brightness) basé sur la distance de la source de lumière, dorénavant nous appliquons la lumière, c’est à dire sa couleur selon sa distance. Ainsi, notre magnifique shader qui permet cette opération est enfin utilisé par le système et non plus via mes essais manuels.

Évidemment cela ne s’est pas fait en 2 tours de cuillères à pot… Il a fallu définir une information de lumière, faire les fonctions d’usage, les modificateurs de variables, faire la différence entre être une lumière et illuminer, ce qui est fort important je vous l’assure pour le bon nommage de vos éléments; et comme dit ci-avant, la détection de ces lumières dans la map automatiquement. Ceci a engendré des correctifs bien utiles et des améliorations en retirant la redondance et simplifiant certains déroulements.

Ensuite nous avions des murs mal illuminés, c’est à dire que les murs, ou éléments de la zone, Sud et Est appartiennent à un gridBloc (carré sur le sol) mais en fait concernent leur case voisine, car vous les observez depuis une autre case, donc leur illumination ne doit pas se faire depuis la case les possédant mais bien par leur case voisine respective.

En bas à droite de l’image ci-dessus vous verrez un mur quasi noir derrière un mur éclairé. C’est du fait que l’élément est masqué virtuellement par un élément non traversable. En conséquence il ne reçoit aucune lumière et ne subit en fait que l’éclairage global. Cela relève du choix personnel lors de votre implémentation en utilisant le moteur, j’ai fait ce choix pour ma démo.

Un autre point que cet article va traiter est la rotation de Squellettore, notre intrépide héro, qui, tel un Derek Zoolander, peut enfin illustrer et parfaire son mouvement de rotation gauche ET droite !

Son déplacement, suite au résultat du pathfinder, est une suite de points à suivre tel Hansel et Gretel. Quand un changement de direction survient actuellement on applique la réorientation du personnage sans autre cérémonie. C’est là que ça commence, il faut s’insérer temporairement dans le mouvement du déplacement le temps d’animer une rotation puis de relancer le déplacement, le tout sans se planter.

Je vous passe la construction d’une technique de détection de direction de rotation, et les emmerdes dues aux croisements d’informations sur les animations en cours et les modifications de l’animation courante qui se chevauchaient, annulant ainsi ce qu’on essaye de faire.

Du coup, en s’abonnant à l’événement de fin d’animation et en détectant que l’on change de direction, l’on peut intercéder, couper le déplacement, lancer l’animation de rotation dans le bon sens, attendre la fin de celle-ci et une fois fait relancer la prochaine étape du déplacement. Aussi simple, même s’il m’aura fallu 6h pour affronter tous les soucis desquels je vous épargne un tantinet.

Il reste cependant à mieux gérer la rotation pour l’utiliser également quand on cible un objet à côté de nous pour interagir avec. Si on tourne en se déplaçant, pourquoi regarder d’un coup un objet à côté de nous.

Dans la gamme du « reste à faire », j’aimerai tenter d’adoucir le changement d’éclairage au déplacement, plus gourmand, complexe, mais pas forcément une mauvaise idée. À suivre donc.

On a donc ici, une amélioration, et non des moindres, de l’atmosphère de notre rendu, de la gestion générale des lumières et un essai d’animation intermédiaire qui convainc plus encore la démarche de notre héro.

Il y a toujours de quoi faire en éclairage, ombres et ombrages et effets visuels variés, on peut citer le normal mapping, des particules, des shaders d’altérations, et bien plus encore, mais tout cela relève surtout de ce que vous voulez faire dans votre projet. Le POC ici arrivent tout doucement à sa fin et un projet devrait débuter sous peu dès que j’aurais revu l’architecture globale et tenter d’éliminer les TODOs restant.

TARS n’est pas parfait, loin de là, mais il faut le mettre à l’épreuve et, au travers d’un projet, corriger et améliorer ce qui sera nécessaire.

C’est en pavant…

J’ai eu l’occasion de montrer une démo de TARS à un de mes professeurs et cela m’a amené à tenter de produire un nouveau type de rendu. Moi qui vente les mérites de la flexibilité de mon système, c’est l’occasion de le prouver… et de me prendre une petite baffe au moral en passant.

Reprendre un projet plus de 6 mois plus tard, même documenté, s’il a été pensé mais trop orienté (scène ISO), ben ça n’aide pas. Ici, il s’agit de faire une vue à 45° de face, une forme de 2.5D, à la Zelda (de l’époque) plus ou moins.

La baffe étant prise et acceptée, une petite demi journée de perdue, je rattaque l’exercice et produit un petit damier, un truc un peu dégueulasse que j’ai affiné avec Boudine en lui montrant. Je vous passe le détail esthétique. Parlons technique !

Côté technique, il s’agit de constater que la hiérarchie : Sprite > Sprite3D > Iso3D est trop spécifique et doit être éclatée. C’est un soucis qu’on a déjà eu et qui maintiens le fait que c’est là le nerf de la guerre structurelle avec GridBlock dont on parlera dans un second temps.

Si je souhaite afficher des bloc non isométrique, je vais partir dès lors sur un Sprite3D (qui doit être renommé en SpriteGl car on ne fait qu’utiliser l’accélération matérielle au final), mais de ce fait je n’ai pas accès à tout ce qu’Iso3D (> IsoGl) contient côté fonctionnalités qui restent valable pour mon histoire de bloc non isométrique.

Le bloc non isométrique dont on parle

Il faut donc se poser la question : est-ce qu’un bloc isométrique est un bloc non isométrique qui surcharge une partie des fonctionnalités ? Force de constater que la seule différence entre les 2 est un service de calcul de position, mais que tout le reste concorde. Donc je dois trouver une astuce.

Ensuite, comme dit plus haut, il y a les GridBlock, qui, initialement, représente une case de la grille en proposant plusieurs espace de stockage, fonction de l’usage comme avec IsoGridBlock, ce qui permet d’aider au rendu. Là encore il y a matière à ce demander si Iso et non iso n’ont pas quelque chose en commun et là aussi revoir qui va avec qui et comment.

Mais ce n’est pas tout, j’ai constaté une erreur de transparence d’image qui m’a replongé dans les shaders d’il y a un an, si vous vous rappelez… Nous avons Alpha et Brightness qui au final touche la même chose, d’une manière bornée différemment, en gros Alpha ne sert à rien, mais du coup il manque Opacity que j’ai ajouté et fait fonctionner et testé avec le title3D daaboo. Il faudra faire du nettoyage pour enlever Alpha.

Comme de bien entendu, je fais un POC dans un POC existant, donc c’est le bordel et c’est parfois sale, mais ça permet de voir ce qui devrait être généralisé et/ou corrigé. Par exemple on a un ElementProvider qui est une sorte de factory personnalisé fournissant les Element correctement typé (DoorElement, TreasureElement, …), mais plus basiquement, ça donne les Grid3DElement ou Iso3DElement et ça m’a permis de tester le fait d’avoir plusieurs providers et le comment du pourquoi ça fonctionne etc.

Ou encore, le fameux catalogue de références de contenu (les sprites), après autant de temps et vu le mélange du POC dans mon POC, cela relève de nouvelles questions, est-ce que le dataProvider est bien pensé, est-ce qu’on ne devrait pas être plus flexible, etc. Il y a le catalogue, la DTD que j’avais oublié et une liste d’éléments utilisable, une sorte de helper pour le chargement dans une scène. Si dans un même projet vous mélangé deux « trucs » il serait intéressant de permettre une meilleure séparation des fichiers, bien que techniquement vous feriez 2 projets séparés, donc je vais peut-être pas m’en occuper tout de suite.

Ceci dit, au final, il y a eu besoin de peu pour réussir à sortir quelque chose. Une scène représentant un niveau, avec des éléments de décors, une flèche animée (mouvement haut bas) et une brillance au dessus du trou qui oscille dans son opacité et son brightness, autant mettre le truc à l’épreuve ah ah.

J’avais prévenu c’est pas super joli, et vu le temps que je perd à essayer de faire le minimum syndical du joli, je vais rester concentrer sur mon code.

L’objectif actuel est atteint, ça tient la route fonctionnellement et « il n’y pas plus qu’à » corriger et améliorer ce dont j’ai parlé dans cet article.

On pourrait ajouter un sprite pour parfaire la démo mais aussi revoir le système de dessin de la grille en partant sur des compositions comme les anciens jeux, permettant ainsi de faire des bords aux blocs dessinés, ce qui « finirait » proprement le dessin, masquant la grille carrée au profit de fioritures esthétique. L’avantage est que c’est plus joli, mais plus compliqué pour un rendu et encore moins dynamique par rapport à ce qu’on essaye de faire avec TARS, on est dans un beau niveau de technicité, mais si on fait une carte vraiment plate en vue du haut à la Zelda ou Graveyard Keeper et autres alors on pourrait penser à ça. Peut-être un défi à faire pour le fun. Bref c’est pas le même challenge ni le même objectif, mais pourquoi pas…

Case départ, et plus si affinités

Nous y sommes, nous sommes revenu à la case départ, au même niveau qu’au moment du constat sur les performances avec les lumières. Ça compile, ça fonctionne, soupir de soulagement, joie intérieur et stress futur quand on voit ce qu’il a fallu pour en arriver là, et donc qu’il faudra arranger. Mais profitons de l’instant présent, du succès immédiat, de ce haut-fait personnel, de cette joie temporaire mais précieuse.

Profitons en pour rire un peu avec quelques captures et commentaires utiles.

Au commencement : ça fonctionne, d’apparence, bon positionnement, chaque chose à sa place, le curseur bouge, bref idéal. À savoir que les objets complexe comme le coffre ou la porte sont arrivés plus tard car il a fallu traduire vers la 3D objet par objet, et au final se rendre compte que les abstractions ne nous oblige qu’à changer la classe de laquelle on hérite, ce qui est parfait mais pose un problème de logique de conception, en sachant que le TypeScript ne permet pas les Traits, mais veut faire du Mixin auquel je m’y refuse (pas beau, pas maintenable, …).

Comme nous n’avons pas de Traits, j’ai donc été obligé de garder l’héritage, ainsi Element (en tête), devient un SpriteElement, qui à son tour sera décliné en Sprite2DElement et Sprite3DElement et chacun d’eux aura sa version Iso (Iso2DElement et Iso3DElement). La difficulté première est justement ce dupliqua de code obligé par la technique limitée du langage.

Nous avons donc un rendu, complet, chaque objets dessinés et à sa place.

Oh ? Mais que ce passe-t-il ? Ben ça c’est quand on clique sur la porte pour la fermer et que … ben ça plante. Ce qui m’a fait découvrir l’erreur total sur le principe de création des textures (sprites transformé pour WebGL) pour le rendu. En gros, la boucle JE confondais la liste des sprites et les calques de ces sprites, ce qui, vous l’aurez compris, ne fonctionne pas.

La solution n’était pas pour autant aussi trivial que cette définition de l’erreur. L’objet Texture a été transformé en héritage de Sprite, le code de création a été inclus dans cette classe, et l’usage se fait comme Iso2DElement, ce qui est assez comique, mais juste. Faire, défaire, déplacer et recommencer…

Là c’est du grand n’importe quoi ! Non seulement le Héro Squellettore ne tourne plus MAIS en plus son animation fonctionne ! Il ne s’agit donc pas d’un soucis de cache (il n’y a pas de cache en plus). Mais en plus de cela quand on clique pour interagir avec le coffre celui-ci se rend à un arbre, plus ou moins un en bas gauche (variable en plus).

Premier cas : la rotation du héro. Celui-ci est résolu par le cas précédent, la gestion des calques des Sprites, le fait d’inclure la Texture à se niveau, d’y avoir accès et de pouvoir l’appeler au moment du rendu en précisant la rotation manquante jusqu’à lors.

Le second est plus ardu : le clic droit d’interaction fait quelque chose d’incohérent et variable, et c’est surtout cette dernière qui est bizarre. Un code ne change pas tout seul, donc on a quelque chose qui ne va pas et reste silencieux. Et c’est le cas, un try-catch attrapait une erreur dans la fonction de détection de contact avec l’objet (le raycasting), il tombait en erreur sur le cache qui n’existe pas en 3D, donc dur d’évaluer si on touche ou non l’objet si notre référentiel n’existe même pas…

Là, on attaque un chantier, devoir utiliser le cache 2D pour les éléments 3D (sous-entendu les Sprites 2D dans un contexte 3D). Là encore, du fait du langage, obligé de dupliquer un bout de code pour créer le cache à dimension et dessiner, en 2D comme avant, notre objet au complet. On peut se permettre de simplifier car ni la transparence ou la luminosité ne sont utiles pour savoir si on est dessus ou non, autant optimiser. Évidemment on ne fera cette opération que si on veut tester le contact avec notre objet, ce qui ne devrait pas poser de soucis de performances, et de toutes façons on a pas trop le choix sur la technique.

L’optimisation sur la transparence a directement été mise à la racine de la méthode isTouch (détection du contact avec l’objet) pour éviter tant que possible les tests inutiles.

De plus ailleurs, au moment du rendu, on testait la transparence ET la luminosité, mais en rien la luminosité n’affecte le fait d’être affiché ou nous, du coup, j’ai changé en ne gardant que la transparence.

Le contexte du cache et l’initialisation de ce dernier ont été factorisés pour éviter le plus intelligemment possible la répétition de codes et ainsi faciliter la maintenance tout en restant flexible sur la partie variable.

Enfin, ceci ne concerne que le POC, ce que vous voyez actuellement dans les différentes captures, le coffre, qui est une source de lumière pour le moment, était inclus de base. Mais quand je fais mes rendus, pour vous présenter les résultats, je réduis ma fenêtre et donc la taille du subsetMap, et pour une fois, j’ai vu qu’il fonctionnait, en faisant aller mon Héro tout en bas, le coffre est sorti de l’écran, et… pouf ! Freeze complet, je me suis fait engueuler par le navigateur qui me demandait comment utiliser quelque chose de non disponible… :p Oups désolé, on va corriger ça et ajouter une condition ^^. Évidemment on est en POC et tout ce qui concerne les lumières est en chantier, mais ça n’empêche pas de penser à ça. Maintenant c’est fait.

Je peux vous présenter enfin la comparaison avant/après.

À gauche la version précédente en 2D, qui continue de fonctionner en tous points, et à droite la version actuelle en 2D via 3D qui a exactement le même comportement, les performances en plus.

Vous remarquerez qu’en 2D (à gauche) je n’ai pas mis le coffre en source de lumière. C’est la seule différence entre les 2 images. Les arbres sont aléatoires, ça vous l’aviez deviné si vous suivez les articles, donc différence aussi en bas au centre.

Nous voilà donc tel que nous étions il y a plus d’un mois durant les recherches de performances sur les lumières. Nous voilà donc revenu dans une situation « stable » et ainsi pouvoir explorer les 14 millions de suites probables.

Ma première idée, car il manque peu de choses, serait l’impacte couleur de la lumière. Par exemple le coffre émettrait du bleu. Il faudra faire la fusion des couleurs, gérer les impactes individuels et on profiterait pour revoir et finir la propagation des lumières.

Ensuite, il y a bien sur l’éditeur, il faut l’adapter à la 3D et avancer sur l’insertion d’objets sur la carte.

Et puis, enfin, on verra bien l’inspiration du moment. Le but reste le même : le projet HeroQuest; tout en développant le moteur TARS.

Fade to grey(worm)

Outre une petite boutade au Visage de chèvre (GOaT), il sera question ici d’un premier résultat, un retour au gris !

Mais pourquoi donc ? Du gris ? Mais où ça ? Ci-dessous :

Ceci ne vous dit peut-être plus grand chose mais il s’agit du ‘loader’, l’écran de chargement en attendant que la scène courante à venir soit prête. Et oui, il est sur fond gris :).

En fait, l’emphase sur le fond gris représente la finition d’un long processus qui vient de nous occuper ces dernières semaines, derniers mois.

Petit retour en arrière avec les performances au niveau de l’éclairage qui nous a déjà bien embêté et limité notre créativité. J’avais pas envie, mais au final c’est une vraie bonne solution : passer au WebGL.

Évidemment cela ne se fait pas tout seul, ni facilement. Si vous avez lu l’article précédent, vous savez déjà un peu, mais pour résumer : merci au site webglfundamentals.org qui m’a permis de démarrer. On attaque pas ça n’importe comment et le code actuel que je vais vous présenter reste un gros POC non-affiné.

Pour commencer il faut se rendre compte que le contexte WebGL ne travaille pas comme le contexte 2D, du coup, au niveau des classes de TARS et du POC il faut penser une division quelque part, en usage ou en héritage, mais quelque part. Là on se casse déjà les dents.

Le premier gros chantier a été de renommer pour identifier la 2D et séparer là où c’était nécessaire, avec, en bonus, le fait que ça continue de fonctionner…

Ensuite on sort l’huile de coude et on ajoute et duplique les composants pour gérer la partie 3D. Il a fallu donner du mou au Renderer pour lui envoyer des options spécifiques (premultipliedAlpha, alpha) et créer un service (pseudo-temporaire-je-POC-laissez-moi-tranquille) de gestion du contexte 3D avec ses shaders, car ici on y passe directement. Du coup, on ne peut pas démarrer sans que ça soit chargé et prêt, donc un événement à insérer dans le workflow.

De plus, pendant les tests, il a fallu désactiver les scènes ‘world’ et ‘loader’ qui sont IsoScene (isométrique) alors qu’on a pas encore affiché une seule image plate (scène ‘title’). Donc on va peut-être commencer par là.

Après, ça part dans tous les sens, le but est de produire une fonction drawImage tel que celle du contexte 2D, mais à ma sauce permettant son faux polymorphisme ainsi que l’injection d’additifs à destination des shaders tel que la gestion de la transparence, de la luminosité, la modification de couleur, etc. Et quand on a écrit la première version, et corrigée, on a le titre daaboo tel que nous l’avions avant ! Yeah !

Mais bon, pour les IsoElement ce n’est pas aussi simple. En 2D ils ont besoin du contexte 2D, il y a un cache et certaines limitations. En 3D, il n’y a pas de cache, on dessine en temps réel et on a des options via les shaders que la 2D n’a pas. Du coup, on y revient, la découpe 2D/3D et du code spécifique pour chaque.

Au final, tout se passe à l’appel de la fonction drawImage de mon service Sprite3D tout en tenant compte du positionnement XY que 2D fait en 2 temps et en un seul en 3D. Une fois l’appel vers drawImage fait, en gros on a fini et le reste se fait tout seul grâce à l’abstraction mise en place.

Ça dessine mais sans utiliser les attributs de la fonction toujours en développement
On a notre assemblage et correctement positionné !

S’en suit la fameuse finition du fond gris où encore une fois les techniques 2D/3D sont différentes, et donc le ‘clear’ de l’écran avant de faire le rendu. Petit jeu d’héritage et de conditions et hop, on passe la couleur ou une valeur par défaut, en restant générique, et on obtient le résultat tel que montré au début sur fond gris !

La suite se fait impatiente ! Réactiver la scène ‘world’, l’interaction et enfin : la lumière.

Bonus : j’ai eu une idée pour faire varier la lumière en tenant compte du delta du déplacement du personnage et ainsi rendre plus doux le changement de lumière sur chaque Tile. Ceci dit il faudra voir la lourdeur du processus, mais on peut déjà imaginer que nous n’avons besoin de refaire le pathfinding qu’au passage de Tile, et durant le delta l’usage suffit (à préserver donc), mais ça demande un reset de chaque Element quand même, enfin, en WebGL cela n’a plus du tout le même sens, ce qui nous arrange justement !

L’obscurité naît de la lumière

Le rendu actuel est bien joli, mais ça manque d’effet de lumière et donc d’obscurité. Du coup j’ai tenté l’expérience, pleuré, cassé, et réussi à avoir un premier résultat.

D’abord c’est quoi cette notion de lumière/obscurité ? C’est tout simplement le fait d’avoir une image lumineuse ou assombrie sur base d’une source supposée de lumière et d’un calcul pour définir l’effet.

Une source ? N’importe quel Element peut être une source de lumière, j’ai créé un interface Illuminate pour qu’il se prenne pour un truc radioactif.

Un calcul ? J’ai déclaré,pour le test, que la lumière allait à 6 cases de distance en décroissant de 20% de manière arbitraire. Cependant j’ai prévu une interface (pas finie) dont le but sera de définir des paramètres pour l’éclairage, permettant ainsi des variables d’affichage (couleur, force, …).

Il s’agit donc de définir pour chaque Element de chaque Zones de tous les GridBlocks du subsetMap une valeur de « Brightness » (luminosité) sur base du calcul et des sources. Le tout sans faire de calculs inutiles.

J’ai opté pour le moment de la création du subsetMap pour agir. Ceci arrive uniquement lors d’un changement d’offset de la caméra (déplacement du héro en bord d’écran), on peut alors profiter de la boucle de création pour trouver les lumières de la Scene. Ensuite il faut parcourir à nouveau le subsetMap pour mettre le « Brightness » de tout le monde à 0 : le noir total. Oui deux fois, car nous agissons dans PlayerScene basé sur un IsoScene et la création du subsetMap est dans IsoScene.

Ensuite pour savoir ce que la lumière va affecter je me suis dit que le PathFinder ferait un bon système de propagation, respectant les murs et les objets. Ce n’est pas au point, et cette idée doit être poussée plus loin, mais le départ est pas mal. On imagine une source de 100% qui perd 20% à chaque changement de case, et on ajoute ceci à l’éclairage déjà présent.

La théorie est pas mal, on est en 2D isométrique quand même, on ne peut pas faire trop de folies et on est limité par l’architecture de TARS et des possibilités de « Canvas » (notre support HTML5 de dessin). En pratique : les performances s’effondre…

Mais que se passe-t-il ? Il faut savoir que pour avoir cet effet j’utilise le « filter » « brightness » de « Canvas », en gros une propriété au moment du dessin permettant d’affecter la luminosité de l’image que j’ajout pour composer la Scene. Je le fais déjà avec l’alpha sans soucis (cf titre daaboo). Bref, ça fonctionne mais prend un temps de dingue, ce qui rend le résultat non réactif et donne du mal au navigateur pour répondre.

Il n’y a pas d’autre façon, l’accès au données de l’image n’est permis qu’au niveau du rendu final et non à l’image que l’on va ajouter et on ne peut pas travailler sur l’image stockée en mémoire, d’une part car c’est pas permis et de l’autre car il nous faut l’original pour modifier les effets.

Une seule solution : la protestation ! À chanter comme une manif de métallo un jour de pont entre deux grèves…

Comment améliorer les performances ? Comment faire quand les outils manquent, les accès limités, les possibilités réduites… ? Il y a bien quelques solutions, mais l’idée n’est pas de demander plus de travail aux utilisateurs de TARS, enfin, le moins possible.

Je suis parti sur l’idée d’un cache, le soucis étant le temps de rendu, et on sait que les mises à jour ne sont que ponctuelles par comparaison. Mais comment ? Nous allons créer un « Canvas » par IsoElement (on va surement le descendre en SpriteElement par la suite (c’est fait :p) et on fait le rendu dans ce « Canvas » invisible. Au moment de composer la scène on copie le contenu à sa destination comme on le faisait avant, et on ne fait plus que ça jusqu’à ce que l’on doive régénérer le cache.

Pour vérifier que ça fonctionne bien avant de péter tous mes calculs, j’ai simulé un rendu dans mon cache, un carré rouge et au lieu de dessiner comme l’image ci-avant, on utilise le contenu du cache.

Je vous épargne le retour du bug de hauteur, le curseur qui fait bouger les Elements, les soucis liés à la division du calcul en 2 étapes, etc. On a déjà connu, déjà fixé, et j’ai dû le refaire encore une fois…

Une anecdote cependant, si vous ne dite pas à votre cache de se régénérer sur base des paramètres de votre Element (PlayerElement, TreasureElement, etc.), il restera tel quel depuis son premier rendu, en gros Squellettore ne marchait plus, il glissait, droit comme un pique sans changer d’orientation. Logique si vous avez lu. Du coup on doit penser à invalider le cache lors de nos mises à jour.

Vu qu’on parle d’animation, il faudra permettre de savoir quand une animation change afin de régénérer le cache à point nommé. Et hop un TODO de plus…

Une fois tout cela fait, on a un rendu avec des performances bien meilleures, cela demande un peu plus de manipulations car il faut penser au cache mais globalement c’est une nette différence.

Concernant le cache, certains automatismes pourront être mis en place en interne au lieu de demander à l’utilisateur de TARS d’y penser. Comme invalider le cache si vous changez d’orientation, l’alpha ou la luminosité. Ce qui est fait en même temps qu’écrire cet article :p et ajouter 3 TODOs… comme si j’en avais pas assez… ^^

Revenons à nos lumières. On a un rendu correct sur base du calcul déclaré plus avant, simple, efficace, même si pas fini (murs opposés, murs d’une case supérieure à la distance prévue, …). Du coup je décide d’en ajouter une seconde pour voir si le cumul fonctionne bien et c’est la fête du slip… Je n’ai pas fait de photo, mais en gros le point d’origine est assombri et génère de l’obscurité, les cases éclairées sont plus loin, bref ça ne va pas.

Mais pourquoi ? C’est une sacrée boucle à investiguer. Ce n’est pas la fonction d’action sur les Elements, ce n’est pas les préparatifs, ni les accès aux variables. C’est le propagateur (par extension le Pathfinder). En fait, par un malheureux hasard, il se peut que vous passiez plusieurs fois sur la même case au cours d’une boucle de propagation (ce qui peut arriver). Le truc c’est que je pensais que je les avais bien exclues, et c’est vrai, mais il faut aussi vérifier en préventif.

Mes tests montraient une quantité astronomique de calculs pour ma petite zone de 10×10, ce qui ne me semblait pas vraisemblable. C’est ce qui m’a mit la puce à l’oreille en gros. Un « If » de vérification et d’un coup la folie s’arrête, les lumières apparaissent et se cumulent proprement. En plus dans l’histoire on a augmenté les performances du Pathfinder et du propagateur pour les lumières !


Voici un résultat avec 2 lumières, le coffre statique et le héro mobile.

Il ne faut pas perdre de vue que ce n’est pas fini. Il y a un gros soucis de performance à la mise en place des lumières quand on s’occupe du subsetMap. Il faut considérer les lumières animées et s’occuper également des occlusions (ça serait top) et de l’orientations des objets, par exemple les murs, opposés ou de la case d’à côté nous tournant le dos. Là il va falloir creuser les théories les meilleures pour augmenter la qualité de l’effet sans défoncer les performances.

Une autre chose aussi, vu que nous travaillons en 2D isométrique, serait d’ombrer selon le caractère cubique de nos objets et de prévoir ainsi, soit une image South, une image East et pourquoi pas une image Top pour ombrer selon des calculs d’angle (les normales) et de distance. Le tout serait de fournir à ces objets des informations plus complète qu’actuellement.

Ceci dit, en l’état, c’est déjà chouette comme rendu !

Angular library – how to – snippet

Suite à mon article précédent j’ai souhaité investiguer, attiré par la curiosité maladive et la difficulté jusqu’à lors de compréhension de cette mécanique. Je vous liste ci dessous quelques articles qui m’ont guidé tant pour démarrer que pour me mélanger les pinceaux.

Pour résumer, si comme moi, vous êtes perdu voici un snippet de démarrage et une copie d’un conseil donné dans le dernier lien.

ng new tars --directory . --create-application=false
ng generate library tars-lib --prefix=tars
ng generate application tars-test

La dernière ligne est utile plus tard quand il faudra tester via une app angular la librairie que l’on souhaite publier.

À noter que mon but n’est pas de passer par NPM pour la publication ni d’aller sur GitHub pour distribuer mon code.

Le conseil du dernier article proposait les lignes ci-dessous dans votre package.json (le premier en root) dans l’espace des scripts.

    "build_lib": "ng build tars-lib",
    "npm_pack": "cd dist/tars-lib && npm pack",
    "package": "npm run build_lib && npm run npm_pack",

Ma prochaine étape est le déplacement du code TARS au sein de l’espace tars-lib créé et de refaire les liens vers le fichier public_api.ts. Ensuite de créer, comme actuellement, une app démo/test, sans inclure l’éditeur, juste une scène et un rendu fonctionnel, simple et expressif.

Une fois validé, je pense que l’on peut alors faire le build de la version de la librairie TARS et créé un nouveau projet dédié pour HeroQuest et là, voir si l’éditeur et la base du jeu prenne bien la lib’.

Réorientation des Sprites

Il y a un point gênant qui traînait depuis un moment du fait que ce monstre était difficile à cerner. Le problème est que chaque POC (preuve de concept) doit être intégré proprement une fois validé, mais pour certains d’entre eux vous ne voyez pas toujours la portée de ce vous venez de faire, les impactes et incidences, ça vous arrive souvent que quand vous y êtes confronté, ce qui a été mon cas.

Premièrement je dois vous informer que j’ai divisé Element en Element et SpriteElement, car si on pense 3D, Element tel qu’il était ne convenait pas, et il doit rester la base, du coup nous avions pas mal de contenu spécifique aux POC et monde 2D (pour le POC iso), donc en résumé j’ai sorti tout le spécifique 2D dans SpriteElement, sur lequel vient s’accrocher à son tour IsoElement qui l’étend plus encore. Element quant à lui est une identité multi-dimensionnel correspondant à la fois à la 2D et à la 3D, soit XYZ (z = précédemment elevationFactor en 2D), le delta de chaque pour les animations notamment et les interactions qui ne dépendent pas de la dimension.

SpriteElement va alors se compléter avec la gestion des spriteRefs, l’orientation (on va y revenir), l’alpha que j’ai ajouté en refondant le titre pour maximiser l’usage de la librairie TARS, le getSize, les positions d’interactions, des get/set Coord adaptés au monde 2D, ce qui concerne les animations, l’update et le draw du/des sprites (spriteRefs encore) du SpriteElement et le isTouch. Pensez 2D plate, pas encore iso, donc on a des méthodes que l’IsoElement va remplacer car il induit une notion de représentation que SpriteElement n’a pas encore. Ainsi on a sous forme d’étage une complétion structurelle cohérente.

Pour les curieux, IsoElement contient des méthodes comme le draw, isTouch et l’update comme cités avant mais remplacées, ainsi qu’une fonction de rotation, c’est tout. 🙂 Simple, basique, toussa.

Pour revenir à l’orientation, la problématique est simple, le POC se déroule en isométrique avec 4 directions, résumées sous la forme NESW (North, East, South, West), vous aurez compris 😉 (j’espère). Du coup, vu que nous traitons en interne des caractères ‘n’, ‘e’, ‘s’ et ‘w’, comment savoir qui est à l’est du nord de l’objet ? Comment connaître la coordonnée au nord de l’objet ? Au final 2 questions qui deviendront 5 réponses, on en reparle plus tard. En gros, pendant plusieurs mois, suite au développement du déplacement et de la réorientation, le code était dans l’actuel SpriteElement et anciennement dans Element.

Si maintenant je vous demande, avec TARS, de faire un jeu de plateforme 2D latéral ce que je viens de vous dire déconnera plein tube car vous avez un 2D isométrique en dur dans Element, hors à la base c’est l’idée, « être capable de tout ».

« Être capable de tout »

TARS

On a donc un soucis connu, le système d’orientation n’a pas sa place à cet endroit. On peut même imaginer un personnage se déplaçant en 8 directions sur un environnement isométrique en 4 directions, tout à fait réaliste. Donc on se trouve au niveau de Element comme dit, on sait qu’on est au bon endroit mais pas de la bonne manière, ni de manière flexible/variable.

Je vous passe la réflexion interminable, la solution est un service sur mesure, donc une Factory (une classe qui vous donne la bonne selon critère). J’ai donc crée SpriteFactoryService qui donne accès à SpriteOrientationSystem (lol) basé sur un critère simple : combien de direction veux-tu pour cet Element, que j’ai nommé SpriteOrientationType et vous donne accès à une liste du style SO0, SO2, SO4, etc. En gros, une image plate, une image plate qui va à gauche ou droite, une image ayant 4 directions quelque soit le système de rendu, 2D haut, 2D iso, etc. Le résultat nous donnera une classe SpriteOrientation0, SpriteOrientation4, etc.

On a donc l’actuel SpriteElement qui demande ce fameux service et selon sa configuration passée au type de l’Element, on a la DTD qui donne des infos générales et s’initialise. On peut désormais avoir un SpriteElement en 2 directions à côté d’un autre en 4 directions etc. C’est la même classe, avec un service sur mesure. Le tout est cadenassé par une interface bien sur (modèle à respecter), ce qui garanti que SpriteElement peut travailler avec n’importe quel système de SpriteOrientation.

Je vous ai épargné beaucoup de détails car au final c’est +17 fichiers qui ont été impactés, adaptés, corrigés et nettoyés. Une aventure difficile mais dont je suis content d’être sorti avec une certaine élégance dans le résultat.

Après ces développements et tentatives de diminuer les TODOs présents, de +40 on est passé à 31, ce qui permet de respirer un peu et de voir de l’avancement, même si rien de nouveau en visuel.

À propos de visuel, l’éditeur avait un peu avancé avant tout ça, voici une capture. En gros, le chargement de scène fonctionne, la suppression d’un Element aussi, la rotation selon molette de souris et si l’Element le permet, fonctionne. J’ai également ajouté la notion des Zones du gridBlock que vous voyez sous la forme BCTNESW avec le C de ‘center’ sélectionné et j’ai ajouté une sélection, que vous voyez en fuchsia pointillé, fonctionnelle mais ne rend pas encore la sélection sur base du filtre de Zones.

Évidemment, tout ceci a permis de découvrir des problèmes, d’amender certains points pour les rendre accessible ou carrément meilleur.

J’avais tenté une sélection multiple d’objets du catalogue pour préparer un pinceau dynamique mais la manière n’était pas optimale. L’idée sera plutôt d’avoir un onglet spécifique aux outils permettant en leur sein d’avoir des options à choisir, via des listes et autres. Par exemple, vu qu’on est un éditeur pour HeroQuest, on peut imaginer un outil « création de pièce » avec en option, « quel mur et sol voulez vous« , on peut sélectionner plusieurs sols et jouer sur un aléatoire de style et de rotation si coché par exemple. Je pense que c’est une meilleure approche.

Je vais déjà tenter de faire un ajout simple puis on verra les outils plus avancés. Quand je dis « tenter », le truc n’est pas compliqué, mais il faut penser à beaucoup de chose avant de se lancer et de devoir recommencer. Ce qui a donné tous ces développements depuis la dernières fois en gros, même si parfois très indirect.

L’idée futur proche reste la diminution des TODOs pour être stable et propre et l’ajout dans l’éditeur. Vu que nous sommes en DEV nous utilisons un dataProvider Fake (faux) qui renvoie des données brutes stockées localement en JSON (et non venant d’un serveur comme cela sera à terme), ce qui veut aussi dire que la partie sauvegarde du résultat de l’éditeur ne peut pas se faire. J’imagine passer par la console pour avoir une sortie texte brute à mettre moi-même dans un fichier local, ce n’est que temporaire et pour contrôler de toutes façons. On sait ce que temporaire signifie…

On est de plus en plus proche d’un résultat exploitable pour commencer le POC du jeu HeroQuest comme dit, il faudra penser la mécanique manquante des actions au sein d’une Scene déplaçant le joueur d’une Scene à une autre, maîtriser d’un point de vue macro l’ensemble du processus de jeu au delà des limites des Scenes. Typiquement le joueur arrive en début de donjon et doit aller à la fin, le fait de toucher la case ou d’interagir avec le fait quitter le donjon pour aller en zone neutre avant d’attaquer un nouveau donjon. Le tout sans perdre l’état du joueur, ses découvertes et équipements.

Et là je n’ai pas encore parlé de la problématique organisationnelle que ceci est un POC dans un POC, la librairie TARS se doit d’être séparée dans un module spécifique, versionné et récupérable au travers d’un projet NPM propre. Ce qui n’a pas l’air évident du tout.

Que ça soit en mode « player » ou en « editor » on a encore du boulot. 🙂