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 !

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. 🙂

L’antithèse d’Oppenheimer

Ça y est, l’éditeur a été débuté ! Il s’agit donc d’un outil propre au projet HeroQuest, car un éditeur est spécifique, du moins pour le moment. Nous ne gérons que des IsoScenes au travers d’un IsoEditor.

L’interface est pour le moment très rudimentaire mais fonctionnelle (sauf le bouton « nouveau » à côté du titre). Une zone centrale de vue et de travail, un menu supérieur barre d’outils, un pied qui sera je pense une liste d’indicateurs et enfin la partie latérale à droite, un systèmes d’onglets.

Ce dernier contient actuellement 2 choses : une liste des scènes disponibles et la liste des éléments à mettre sur celle-ci. Un double clic sur un Element transforme votre curseur en l’objet et vous permettra de le placer.

Évidemment il vous vient directement à l’esprit qu’il y a plusieurs zones et donc un simple clic ne suffira pas, il faudra définir la zone impactée. Là j’imagine dans la barre d’outil, à l’image des alignements dans Photoshop, un moyen de préciser la zone via des icônes. On peut toujours aller plus loin car l’objet affiché, un mur par exemple est affiché au nord par défaut, mais est-ce que le sens du mur affecte la zone d’impacte ou est-ce que la zone sélectionnée affecte l’orientation. Des comme ça on en aura surement un paquet et il faudra être bien cohérent sur la définition de l’expérience utilisateur, heureusement à force d’usage je verrais vite ce qui fonctionne le mieux.

Prochaine étape donc, ce placement sur la map. Il faut s’avoir que le composant Editor doit discuter avec l’IsoEditor, ils travaillent de concert et doivent chacun gérer leur histoire, et à moi de ne pas tout mélanger ou mal placer. À venir par exemple il faudra que je divise les onglets en composant pour respecter les idées Angular (mieux que tout dans un seul ^^).

Il faudra également penser au chargement/sauvegarde (fake), déplacement de ce qui est sur la carte, sélection d’un Element et modification de ses propriétés (comme l’élévation) et même accéder au GridBlock. Enfin, ce n’est que le haut de l’iceberg actuel comme toujours :).

Squellettore mon Héro

Le caddie ayant bien roulé, tellement qu’il en a les roues carrées, j’ai décidé de le remplacer par un héro ayant un peu plus de capacité d’animation. C’est tout naturellement que j’ai pensé à mon Héro de toujours : Squellettore.

Faisant directement suite au coffre, il m’aura juste fallu l’encoder dans la bibliothèque et ajouter 2 lignes de code pour activer les 2 animations.

Le coffre de la tourmente

Comme vous l’aurez compris j’ai ajouté un coffre au trésor dans le set d’objet actuellement disponible. Mais pourquoi ?

  1. Nous n’avons rien qui teste le système d’animation depuis qu’il a été créé, et le « target » en vague bleue était l’animation principale.
  2. Rien n’a été mis en place pour le changement d’animation.
  3. Un problème de boucle se soulève en imaginant l’ouverture du coffre, actuellement toutes les animations sont bouclées.

Mais d’abord il faut le dessiner, dans les 4 directions, l’animer, ce qui est une autre sinécure et enfin en faire des Sprites NESW. Là il n’y a pas d’outils actuellement dans mes mains, rien de facile, gratuit pour les POC. Cependant j’ai trouvé un script (Layer2SpriteSheet) un peu bancale mais qui m’a bien aidé. C’est pour Adobe Photoshop et ça utilise vos calques pour générer votre Sprite final.

Et voilà un coffre que je vous anime ci-dessus. Ça c’était la partie facile, même si ça m’a déjà pris quelques heures de chipotages. Maintenant il faut l’intégrer au POC HeroQuest/TARS. Donc comme d’habitude on manipule la bibliothèque en précisant les images NESW, les 4 animations disponibles et les dimensions des images, on édite la liste des Elements disponibles, on ajoute une DTD (définition) pour ce qu’est un « Treasure », donc la coordonnée d’interaction, les interactions disponible comme pour la porte et pas de référence de Sprite spécifique, celui par défaut convient très bien.

Jusque là c’est nickel, j’ai juste dû corriger quelques typos dans mes modifications et ça roule, mais comme on a un objet avec lequel on peut interagir et même si c’est IsoPlayer qui fait la réaction des interactions pour le moment, il va nous falloir un objet spécifique : TreasureElement. En gros c’est la copie d’une porte pour la partie interaction et on retire la gestion multi-sprite au profit de l’usage du sprite par défaut.

Pour rappel, le Sprite par défaut, est l’image ou la somme d’image composant l’ensemble des possibilités d’animations de l’objet. Avoir plusieurs Sprites au sein d’un élément est une construction dynamique, une volonté spécifique pour obtenir un résultat dynamique qu’un Sprite ne peut offrir.

Nous voilà donc avec notre classe TreasureElement à laquelle on va déplacer la gestion des interactions en son sein au lieu de IsoPlayer, c’est quand même IsoPlayer qui l’appelle, mais la partie spécifique est du côté spécifique, meilleure séparation et clarté du code. Nous avons donc une méthode « doInteraction », comme dans IsoPlayer, avec les mêmes paramètres etc. qui va nous permettre de faire les tests de cas en fonction du verbe reçu.

C’est bien joli, mais l’animation dans tout ça ? On y arrive enfin, on a 4 animations : ouvert, fermé, s’ouvre, se ferme; et voici le schéma qu’on aimerait mettre en place :

  1. Le coffre s’initialise avec l’état fermé et l’animation « fermé ».
  2. Je clique pour interagir avec le coffre et choisi de l’ouvrir : on lance l’animation « s’ouvre » et on aimerait qu’après ça passe en « ouvert » et que ça reste ainsi.
  3. Je clique pour interagir avec le coffre et choisi de le fermer : on lance l’animation « se ferme » et on aimerait qu’après ça passe en « fermé » et que ça reste ainsi.

Je ne revient pas sur le fonctionnement de l’interaction, il vous suffit de lire l’article précédent. Donc nous en sommes à recevoir l’action « ouvrir » et on souhaite changer l’animation courante. BIIIIP faux départ ! Cette fonction n’existe pas.

Premier problème rencontré, rien n’a été prévu pour changer l’animation. Je vous passe toutes les réflexions, j’ai ajouté dans Element une fonction setAnimation dont le but est de manipuler la collection des animations et mettre les paramètres dans les bonnes conditions (nom de l’animation, timing, frame courante).

J’ajoute directement ici qu’après réflexions sur les améliorations utiles, le fait de pouvoir attendre la fin d’une animation est très intéressante. Que ce soit d’être prévenu tout simplement par un événement auquel on peut s’accrocher, mais également en interne, initier le changement d’animation en attendant la fin de la précédente, on crée alors en interne l’attente de l’événement et on change alors les mêmes paramètres comme dit avant.

Maintenant que la possibilité existe on peut demander notre changement d’animation suite à l’interaction. Il nous reste à utiliser également l’événement de fin d’animation pour appliquer le changement d’animation vers les « animations » fixes « ouvert » ou « fermé ».

À cela, j’ajouterai que j’ai prévu un paramètre d’animation « loop » qu’il me reste à implémenter pour définir qu’une animation ne boucle pas forcément. On arrêterait ainsi l’animation avec un « flag » (drapeau d’état) et ainsi ne pas boucler sur la dernière image et envoyer X alertes d’événement de fin.

Pour simplifier, parmi les 4 animations citées, nous pourrions supprimer « ouvert » et modifier l’animation « s’ouvre » avec le « flag » « loop » à « false » (non), elle resterait ainsi sur sa dernière image, sans chercher à poursuivre et nous n’aurions qu’une seule fois l’alerte de fin d’animation. Ceci nous économise une définition d’animation et plus de facilités pour l’avenir.

Petite anecdote, j’avais défini tous mes changements d’animation en demandant d’attendre que l’animation courante se termine avant de changer, mais du coup mon coffre s’ouvrait ou se fermait 2 fois avant de rester dans son état ouvert ou fermé. Mais pourquoi ? En fait, en faisant ça, au moment où l’animation se termine, l’événement nous alerte et on lui demande de changer d’animation quand elle sera finie, mais l’événement vient de passer, du coup il relance l’animation une seconde fois. Vu que nous sommes déjà dans l’état de fin, il ne fallait donc pas utiliser « l’attente de fin » encore une fois. Ça vous évitera beaucoup de recherches inutiles.

Tout ceci commença une dizaine d’heures plus tôt en dessinant un personnage pour remplacer notre ami le caddie dans le même objectif de manipuler les animations. Ce que je vais faire suite à ceci ainsi que coder le paramètre « loop ».

Intergalacticulaire

C’est l’histoire d’un clic, qui n’avait rien de particulier, mais était destiné à de grandes choses. Il ne le savait pas encore, mais il allait bientôt vivre un grand moment et constater les conséquences de ses actes.

C’est l’histoire d’un clic, droit comme un pique, sur une porte, droite elle aussi, qui pouvait en un même instant faire bouger un caddie, un peu bancale lui, et demander une interaction qui elle allait dans tous les sens.

Cette histoire chers amis, c’est une conclusion, en terme de preuve de concept (POC), qui dure maintenant depuis plusieurs semaines. On y est enfin, la boucle est bouclée et voici l’explication finale.

Pour vous donner cette explication je vais procéder de manière chronologique en séquences. Ceci dit, je ne vais pas me priver de quelques détails. À cela j’ajoute que j’ai opté pour la solution HTML du menu et que ça nous a réservé quelques surprises.

Tout commence par un clic droit, notre isoPlayer (de type isoScene) reçoit l’événement, on détecte le bouton droit et on tente de déterminer sur quoi on a cliqué (le fameux Raycasting). On trouve un Element (ou rien et ça s’arrête là), on lui demande ses coordonnées (oulala…), on dit au Hero que c’est en fonction de cet Element qu’il va s’orienter, puis on attaque la partie déplacement en fonction des coordonnées reçues. On termine par préparer une surveillance de quand il arrivera à destination.

Excepté si le Hero est déjà sur place, nous patientons un instant et on nous rappelleras quand il y sera. La fonction de surveillance sera appelée et dedans, tout simplement, on informe les intéressés, qu’un événement de menu d’interactions a été déclenché, en lui passant les informations recueillies de notre Element cible : coordonnées du bloc à l’écran et la liste des interactions disponibles.

Nous venons de parler des intéressés, c’est à dire ceux qui se sont abonnés à l’événement du menu d’interactions. Petite modification dans notre composant Player (celui qui englobe le rendu et le point de départ de Game), nous avons créé et ajouté un composant InteractionsMenu, dont le but est d’afficher une liste de verbes et de prévenir quand on a cliqué dessus. Notre composant Player s’est abonné et va donc recevoir l’événement dont on a parlé fin du paragraphe précédent.

Du coup que se passe-t-il ensuite ? IsoPlayer est prévenu et peut à son tour préparer une variable à destination du composant InteractionsMenu, qui à son tour va pouvoir s’afficher selon les coordonnées écrans avec la liste données. Jusque là, tout est comme annoncé.

Petit commentaire sur la liste des verbes, bien sur il y a 2 verbes pour une porte, « open » et « close » (actuellement dans notre POC), mais selon l’état actuel de la porte le menu n’affiche que ce qui est disponible.

Vient ensuite le choix du menu, ici il n’y en a plus qu’un vu le filtre cité juste avant. Le composant InteractionsMenu a une sortie sous forme d’événement à son tour. Vous cliquez, il informe ses abonnés, dans le cas présent : le composant Player, qui à son tour, quand déclenché, masquera le menu et demandera à IsoPlayer de gérer l’action.

Pour ce POC, et vu que nous n’avons qu’une porte, la gestion se fait au cas par cas et selon l’état. Donc, de quel type est l’Element ciblé, et en fonction du verbe reçus on contacte la bonne méthode pour lui demander de changer d’état. En interne, c’est à lui de se gérer et mettre sa liste d’interactions à jour.

Nous voilà donc avec un boucle bouclée, un clic est à l’origine du ciblage et nous affiche un menu d’interactions disponibles, on choisi et c’est reparti, on remonte la chaîne pour agir sur l’Element.

Ceci est bien sûr un premier jet, fonctionnel certes, du côté implémentation de TARS (usage de la librairie), mais on peut se poser quelques questions et y voir déjà des choses à régler ou à faire évoluer. Enfin, ceci sera surtout vu au cas par cas tout au long de notre chemin pour nous rapprocher du projet HeroQuest.

Mais du coup, maintenant que ce grand challenge a été relevé, c’est quoi le suivant ? Bonne question, mais je pense que l’éditeur reste la suite logique. C’est à dire, l’édition de la carte, une gestion différente de la souris et de la caméra, un menu interactif pour la gestion du catalogue et interagir avec un Element sélectionné. Il me reste à découper ça en phase et à trouver par où attaquer ^^.

Ours à poil, sens du détail

Hier encore la détection se faisait à l’aide de « bounds », de zones définies par rapport au Sprite pour définir ce qui est susceptible d’être le contenu de ce qui ne l’est pas. Nous avions un arbre, un sol, des murs, jusque là c’était évident, 5 à 6 points suffisent pour les définir grosso-modo. Mais, depuis nous nous sommes mis en tête de mettre une porte, qui plus est ouverte, et là, c’est un besoin de 5 zones et ce par direction, soit 20 repérages pour un seul objet et encore, sans animation.

Vous l’aurez compris, le but n’est pas de se compliquer la vie, alors développons une solution : la détection par transparence de l’objet.

Nos images sont des PNG, donc un format préservant la transparence, une fois dans un « canvas » (l’objet HTML), nous pouvons demander à celui-ci de nous donner la valeur d’un pixel. Il nous suffit, dans notre méthode de détection existante, au lieu d’utiliser les « bounds », mais en gardant tous les calculs préalables, d’utiliser notre « canvas » afin de tester la valeur du pixel sous notre souris.

Je schématise bien sur, cela demande un environnement vierge, une remise à zéro des valeurs pour comparaison correcte, le dessin de l’image, la capture du point, son analyse, enfin bref, 4-5 lignes et c’est joué.

Pour vous ça revient à tester si la souris touche du rose (qui sera la transparence) ou l’objet, ici notre porte. Ce qui nous rappelle les belles années des jeux « point & click », comme Monkey Island, Loom, Sam & Max etc, basé sur ce genre de sprite en fond rose ou vert une fois décortiqué.

Ceci dit, ça n’a pas été aussi simple, il a fallu modifier les interfaces de configuration des animations, détecter si on veut travailler en Alpha ou en Bounds, rendre ça « sexy » et pratique.

Ce qui nous a également amené à découvrir des erreurs planquées, car ce sont des cas qui n’ont pas encore été testés. Il s’agit de notre porte, ben tiens, encore elle. Elle est définie par 2 Sprites et n’en affiche qu’un à la fois selon son état. MAIS le calcul de taille est basé sur la somme des 2 et la détection les parcoure également. Imaginez une seconde le bordel quand vous ne voyez la porte que normalement, alors que le système manipule tout à fait autre chose.

Après de longues recherches, j’ai introduit la notion de Structure, permettant à un Element défini par X Sprites (un tronc et un feuillage), de les manipuler sur conditions (3 blocs de tronc puis le feuillage) et ainsi de travailler sur une base connue et manipulable (par une classe d’extension). Ainsi notre porte répond à la question « Quelle est ta structure ? » et les méthodes en ayant besoin ne sont plus en erreur, et correspondent à vos attentes.

Maintenant que nous avons la détection sur la porte, et les « verbes », que j’ai ajouté dans la foulée. Nous allons pouvoir « demander » à l’objet trouvé, non plus sa nature comme vu précédemment, mais les interactions possibles ! À nous de les afficher comme bon nous semble et de définir comment nous allons gérer ces interactions.

Mouse to be alive

Tel un tube disco et des déhanchés sur le « dance floor », c’est l’euphorie du clic à présent ! La séparation clic gauche pour le déplacement et le droit pour l’interaction (avec déplacement si besoin et possible) est fait, et ce n’était pas une sinécure !

Dis comme ça, vu qu’on avait déjà les 2 fonctionnalités, rien de neuf sous le soleil mais cela a soulevé des questions, qui ont soulevées la mousse des rochers du début de projet et créé un vrai bordel et remises en question.

Nous en revenons au Raycasting (détecter un objet quand on clique dessus), car désormais nous avons un GridBlock à gérer et ses 7 zones. Nous ne parlerons pas du besoin de séparer le côté spécifique des 7 zones avec un GridBlock générique et dont les IsoScene devront prendre la gestion à l’usage, c’est connu et ça attendra une prochaine version une fois que la poussière du champs de bataille sera retombée.

Le cycle est le suivant :

  • Cliquez (quel bouton ?) droit sur un arbre ou un mur.
  • L’IsoPlayer prend l’événement, détecte le clic droit et demande quel est l’élément « Raycasté » (on dira ça ainsi, ou touché par le lancé du rayon sacré venant de la sainte souris…).
  • Celui-ci demande alors au service Iso de s’en occuper et plus vite que ça !
  • Le service parcours le subsetMap, et demande à chaque GridBlock de faire de même pour les Elements.
  • Du coup le GridBlock demande à ses zones de consulter chaque Element et de voir s’il y a contact.
  • Oui / Non : l’histoire nous le dira !

Première remarque, la manière dont on parcours le subsetMap, on partait de la coordonnées du joueurs et on recalculait ses dimensions pour faire une boucle, je simplifie, mais pas bien du tout ! Après 2 efforts, et surtout l’idée que la grille n’est pas carrée mais un losange, on retiendra que pour chaque colonne et ligne de cette grille on cherche le minimum et maximum afin de la parcourir efficacement et sans artifice inutile.

Notez également qu’un tableau ne peut avoir d’index négatif, ceux-ci seront considérés comme des propriétés (‘-‘ étant un caractère au final) et du coup un forEach, length, etc. ne sont d’aucune aide, privilégiez le « for … in » alors. Ceci dit, nous avons décidé de ne pas les gérer, ce qui permettra une forme d’optimisation.

Ensuite, nous avons le niveau de GridBlock qui doit orchestrer le parcours des zones dans le sens inverse de l’ordre de dessins pour tester le Raycasting. Encore une fois, il faut faire attention à ne pas se répéter inutilement et surtout mettre le bon code au bon endroit, GridBlock ne doit que gérer ses zones. Ainsi, après un petit « refactoring » (refonte), on a une fonction de calcul de hauteur d’une zone (car le Raycasting travaille à l’envers du dessin), une fonction de détection par zone (en demandant à chaque Element si ça touche :p) et la fonction d’orchestration principale pour gérer l’ordre des zones et l’ensemble de l’opération. C’est ce qui m’a pris du temps à rendre joli.

Un code laid n’est pas un bon code

À cet instant, chaque zone est bien ordonnée, testée et aucun code n’est dupliqué. C’est élégant, fonctionnel (surtout) et avec cette refonte, un horizon de généricité se dessine.

La méthode a été reportée sur la fonction de dessin qui souffrait des mêmes manques. On peut donc imaginer avec plus de facilité sa prochaine version, son évolution, comme dit en début d’article.

Après une, deux, semaines, nous revoilà au début de notre schéma pour obtenir l’interaction, point 1) le clic sur l’objet concerné.

Notre but reste un clic sur la porte, faire apparaître un menu quelconque et modifier son état ouvert/fermé. À partir de là on ouvre beaucoup de possibilités à explorer et en découvrir les limites, à repousser toujours plus loin.

En parlant de menu, une question se pose, profiter du HTML à notre disposition ou définir une interface dessinée. La première existe déjà et aura ses avantages / inconvénients, la seconde est a créer de toutes pièces et permet donc une maîtrise complète (événements, animation, éléments inexistant en HTML, etc.) mais demandera beaucoup plus de temps à concevoir.

Enfin, avant de penser à ça, nous allons devoir définir ce qu’est une interaction, lesquelles sont possibles, comment les récupérer et les définir. Un beau challenge encore. 🙂

Je suis Traversable

Je suis … Traversable !!! Comme un fantôme ! Ou du moins comme une porte :). Exactement, une porte, ce sujet qui nous tracasse depuis qu’on a des murs.

Mais qu’est-ce qu’une porte ? C’est un Element, qui se situe au niveau d’une zone de mur et à qui le pathfinder doit poser la question si oui ou non on peut passer ce « mur ». Il doit donc demander s’il est Traversable.

Côté technique il y a 2 aspects : étendre Element au travers d’une classe Door et définir une interface Traversable ainsi qu’une astuce typescript, une fonction dédiée instanceOfTraversable (car nativement on ne peut tester si une classe étend ou non une interface).

Ne pas oublier que pour ce faire nous avons également ajouté les images, les définitions dans les fichiers de ressources et modifié le générateur de map pour notre POC comme précédemment, petit rappel.

Pour les codeurs qui chercheraient de l’aide sur le sujet des interfaces voici comment s’y prendre :

export interface Traversable {
  isTraversable(): boolean;
}

export function instanceOfTraversable(object: any): object is Traversable {
  return 'isTraversable' in object;
}

Ainsi notre DoorElement basé sur Element nous permet d’utiliser cette interface et de répondre à la question. Notez que la question n’est pas posée par le pathfinder lui-même mais par la fonction de validation qu’on lui donne. Ceci reste donc côté implémentation comme désiré.

Comme vous pouvez le remarquer on a bien notre porte… enfin 2 !

Mais qu’est-ce qui a bien pu se passer ? Ben en fait c’est tout con, première règle élémentaire de la Robotique de notre moteur TARS, tout est empilé ! Et quand Element reçoit des spritesRefs multiples, il stack naturellement, jusqu’à ce qu’on surcharge sa classe et sa méthode draw() pour changer tout ça :). Et c’était prévu, mais, car il y a un mais, il y avait moyen d’améliorer l’accès à la fonction de dessin individuel.

Dans notre cas nous avons 2 sprites chargé pour un DoorElement, ouvert et fermé (ah ben bravo !). Nous ajoutons un statut à la porte, on en profite pour faire une interface surchargeant SceneElement pour les config et un « enum » pour structurer Open et Closed comme valeur possible du statut de la porte, le tout rendant l’objet porte bien structuré quand on veut en ajouter une.

Enfin, suivant le statut, un seul Sprite sera affiché et grâce à la fonction drawOne() qu’on a ajouté, le tout est rendu plus simple.

Actuellement il n’y a pas encore d’interaction, donc la vidéo ci-dessus est illustrée par 2 compilations distinctes.

Ce qui nous amène à une observation, l’eau ça mouille… et les arbres cachent la case derrière eux (en NE), du coup le raycasting actuellement utilisé au déplacement de la souris, ne permet pas aisément de sélectionner la case visée, il faut aller sur le bord de l’arbre sur 1-2 pixels.

Après réflexion, vu qu’on tend à implémenter les interactions, à revoir cette gestion de la souris. Ceci vous étonnera surement, mais elle a plusieurs boutons, la souris. On pourrait donc en utiliser un pour se déplacer, sans raycasting, juste un survol des cases, et un autre pour tenter d’interagir, si la cible le permet.

Il y aurait également la réflexion sur le curseur qui serait caché, car son rendu se fait au moment de sa case, et si un mur ou un arbre le masque, vous ne savez pas vraiment où il est si vous n’avez pas suivi. On pourrait imaginer un postDraw (un rendu d’après), permettant alors de se redessiner après tout, sous une forme allégée, comme un contour en transparence ou surbrillance. Il faudra également savoir qui a besoin de ce postDraw, pour éviter de parcourir à nouveau l’ensemble du subsetMap. Soit par préenregistrement, au moment de l’init du subset par exemple vu qu’on parcours la map, ou au moment du rendu vu qu’on passe dessus également. C’est peut-être cette dernière piste que je regarderais.