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.

Les mexicains vont pas aimer

Des murs !!! Ça y est, le système affiche proprement les murs définit, actuellement définit en code pour les besoins du POC (test). Vous voyez ici, dans l’ordre haut gauche vers bas droite, un N et un E, puis un S, ensuite un W et E, et enfin encore un W. Ainsi l’on constate que tout se met bien même côte à côte (EW).

Ça c’était la partie facile, au final, vu que tout était prêt avec GridBlock. La difficulté à présent est de modifier le pathfinder pour qu’il tienne compte des murs.

Premier constat, initialement nous tenons compte de l’élévation entre blocs de manière claire au sein du code, ce qui va à l’encontre d’une idée générique (entre l’idée de la librairie pathfinder et son usage en fonction du projet). Ceci dit c’était essentiel pour les POC précédents. C’est donc notre point d’entrée, il faut un moyen d’accepter ou non le fait de passer d’une case à une autre.

On retire donc tout ce qui touche à l’élévation et hauteur enregistrée jusque là, on crée une fonction que l’on envoie au pathfinder pour sa résolution, ainsi il ne connait rien de votre projet et cela vous permet de définir les règles d’acceptation, on le nommera le Validator. Pour un premier essai que tout continue de fonctionner comme avant, il renvoie toujours oui, et ça fonctionne, cette mécanique est prometteuse.

Ensuite vient le fait d’avoir connaissance des murs, car jusque là, tout ce que le préparateur de la grille du pathfinder sait c’est une coordonnée (x, y, vu qu’on a retiré l’élévation). Mais dans notre Validator nous n’avons aucun accès à la Map ou au subsetMap, nous avons juste la structure de coordonnées du pathfinder (précision importante ici).

Il faudra donc ajouter le GridBlock consulté lors de la préparation de la grille à la structure de coordonnée du pathfinder, pour garder un accès et le consulter plus tard au moment de la résolution du chemin et donc de la validation (passage d’une case à une autre) et modifier ci – et – là le fait d’utiliser cette structure au lieu de simple coordonées [x, y].

Ceci étant fait, nous avons donc un pathfinder, qui a une grille avec accès à la Map par référence, et une méthode de validation de passage le rendant générique. Il ne nous reste plus qu’à écrire ces fameuses règles autorisant ou non le passage d’une case à une autre, notre soucis : les murs.

Nouvelle question comment savoir quel murs comparer sur une case et sur l’autre ? Car oui, comme dans l’exemple initial en début d’article, on a deux murs collés EW, chaque sur leur case, il faut tenir compte des deux. Et ce n’est pas tout, comment savoir que c’est le N et le S qu’il faut comparer alors que l’on reçoit juste 2 coordonnées en entrée du Validator.

Nouveau défit, nouvelles idées. Nous avons au sein d’Element une fonction permettant de définir l’orientation. C’est grâce à cela que le caddie se tourne suivant son chemin. En divisant cette fonction en deux on a le getDirection() qui nous manquait. Dès lors la théorie à appliquer pour ce premier POC est assez simple, suivant l’orientation NESW, on compare les 2 murs des 2 cases, par exemple je vais vers le N, je compare mon mur N d’origine et le S de destination, et ainsi de suite selon toute logique :).

Et ça fonctionne !

Maintenant des portes et des interactions, car un mur avec une pancarte par exemple, sera lisible (interaction) mais : de quel côté ? condition(s) de mur mitoyen ? Etc. Beaucoup de questions qui vont demander de modifier les fonctions de l’IsoPlayer et du PlayerElement car, au final, ce ne sont que des détections ^^…

La grille, ultime frontière

Première étape cruciale dans l’évolution du système vers l’objectif HeroQuest : la grille (cf Tron Legacy ^^). L’évolution permettant l’ajout de murs et la gestion de toutes les conséquences induites, comme le pathfinder, le système de rendu, le raycasting, etc.

La solution est qu’une coordonnée de la grille, qui était un tableau d’Elements, devienne un GridBlock et c’est lui qui contient les Elements dans un de ses espaces. En gros GridBlock est lui même un tableau d’Element, un multi-tableau d’Element même, youhou bienvenue dans la 4ème dimension de la grille !

Évidemment ce n’est pas aussi basique ni aussi simple. GridBlock est volontairement une classe et non un simple tableau de plus. Ceci nous permet de prendre la partie du rendu globales des zones dans un ordre logique, de donner sur demande le contenu d’une zone et de prendre de son côté les fonctions d’insert et de retrait d’Element. Donc on rassemble ce qui peut l’être au lieu d’être au sein des méthodes ci-et-là.

On parle de zones, mais qu’est-ce donc ? En gros, nous voulons améliorer la gestion de l’empilement, et avec HeroQuest nous avons besoin de murs et portes, et pourquoi pas plus tard des indications au dessus des objets ou du héro. L’objet GridBlock a donc pour vocation de gérer des espaces virtuels pour faciliter le montage finale de la coordonnées. Il y a NESW, Center (centre), Top (dessus) et Bottom (dessous), ce qui correspond à un Ground en Bottom, un personnage ou arbre en Center, les murs et portes en NESW, etc.

Ensuite, il y a l’usage, car, dans un premier temps, j’avais tout mis dans la zone centrale, et fait en sorte que les méthodes liées utilisent la zone centrale par défaut. Ainsi, étape par étape, ça continue de fonctionner et ça permet d’isoler chaque étape de modifications.

En premier j’ai divisé la méthode qui construit actuellement la carte dynamiquement pour le POC, une grille de 10×10 qui ajoute aléatoirement des arbres. Et j’ai donc poussé le héro en zone centrale avec les arbres et le sol dans la zone du dessous. Rien de compliqué là dedans. Mais du coup le système de rendu n’affichait plus le sol (vu que c’est central par défaut).

Comme dit plus haut, l’avantage d’avoir GridBlock en tant qu’objet et non que comme simple conteneur, j’ai pu lui écrire une méthode draw() pour orchestrer le rendu par zone et donc déplacer le code initialement dans Iso vers GridBlock et ce pour chaque zone dans l’ordre désiré. Le tout en gardant les notions Z importante pour l’empilement. « Les » car Top se base sur Center, NESW et Center sur Bottom.

On a donc un caddie sur sol avec des arbres. Retour case départ, mais le caddie ne bouge plus.

C’est normal, le pathFinder se base sur un objet de type Ground et il cherche dans la zone centrale alors qu’on a déplacé dans bottom. Jusque là c’est simple de lui dire quoi faire pour corriger ça, mais est-ce au service de préparation de la grille du pathFinder de se tracasser de ça ? Non ! J’ai donc remplacé excludedTypes (types d’Element exclus du calcul) par une fonction que l’appelant implémente avec sa logique de tri.

Ainsi, un coup je peu être un personnage marchant sur des zones centrales inoccupée, autant une autre fois je peux être un oiseau parcourant les zones top. C’est bête mais important ! Du coup on a une fonction plus générique, une séparation logique propre et un résultat fonctionnel. Enfin, quand on oublie pas que le caddie occupe une place centrale et donc invalide sa position et l’empêche de bouger ^^ Bug connu précédemment revenu suite au changement de façons de procéder, excludedTypes était la solution précédente, on lui envoyait TreeElement à exclure ce qui incluait donc le PlayerElement, CQFD. Et grâce au fait que maintenant c’est géré côté appelant, donc dans IsoPlayer, il peut clairement indiquer ce qu’il veut ou non, et exclure de manière forte notre fameux PlayerElement.

Dernier détail, ce qui soulève en fait une optimisation, c’est le raycasting, donc le fait de trouver sur la carte ce que l’on survole avec le curseur de la souris. Le but est de déterminer la coordonnée survolée, et si un objet est sur la zone (et qu’il peut avoir une forme variable et donc un contour) le raycasting détermine si on est dessus ou sur une autre coordonnée, en gros.

Mais maintenant nous avons plusieurs zones, donc un travail plus long. Il faut donc prendre les zones peuplées et ne pas considérer Bottom. On simplifie tout en complexifiant le travail ^^.

En résumé, on a amélioré le système de grille avec une gestion spatiale structurée, adapté le système de rendu, de pathfinding et de raycasting, rassemblé du code et éclairci d’autres. Tout fonctionne à nouveau comme si de rien n’était mais nous ouvre à présent de nouvelles portes pour les objectifs désirés.

HeroQuest

HeroQuest est un jeu de plateau de chez MB de 1989 qui se joue de 2 à 5 joueurs. L’idée est simple, des héros affrontent des donjons, gagnent des trésors, combattent des monstres et obtiennent la gloire !

Plateau de jeu v1

Pourquoi est-ce qu’on parle de HeroQuest ? Pourquoi est-ce que je vous montre ça ? Tout simplement car le projet Nahyan stagne du fait de la distance entre le niveau actuel du moteur et ce que j’essaie d’atteindre. Et plutôt que de ne pas bouger et d’attendre la solution théorique optimale, je me penche sur un jeu simple que j’ai adoré et qui représente « l’étape d’après » de l’état actuel. Cela fait quand même quasi un mois que je n’ai plus bougé réellement.

Nous avons : une carte, des mouvements de caméra, un pathfinder, la possibilité d’interaction de base, une résolution de position au clic sur objet, un chef d’orchestre et une orientation lors du déplacement.

Il ne nous manque plus qu’une gestion de murs et de portes, un pathfinder adapté, un système de combat au tour par tour, une gestion de héro multiple (si on prend cette option), une mécanique de jeu sur des règles déjà existantes et éprouvées et un système de suivis de quêtes. On est pas loin :). Et je ne vous parle pas totalement du jeu en lui-même à intégrer…

La première étape va donc être de créer quelques éléments de design HeroQuest, de modifier le système de carte pour intégrer les murs et d’adapter le pathfinder. La découverte du donjon en se déplaçant pourrait être une seconde étape de départ.

Ensuite viendra le moment de gérer une carte complète (un donjon) et d’y ajouter, étape par étape, des objets, des monstres et des mécaniques de jeu.

Quand cela fonctionnera pour un niveau test, on pourra imaginer une mécanique de jeu globale avec choix du/des personnages avec leur fiche et inventaire et un scénario à suivre.

Certains me diront que le jeu a été réédité, que des extensions ont été ajoutées, que ça va tout compliquer, etc. c’est vrai. Mais il faut bien commencer quelque part :).

Mais avant de commencer cette première étape, nous allons faire un éditeur de donjon :).

Réflexion sur le sol et l’empilement

Une réflexion me taraude l’esprit et je vais tenter de l’exprimer ici. Il s’agit du sol de Nahyan, et donc de la mécanique TARS/Nahyan à mettre en place pour que cela fonctionne et ait du sens.

TARS empile les éléments et des méthodes, actuellement pas parfaites, gèrent l’insertion d’un objet ou son retrait sur base de son type. Suite aux observations faite lors des POC Caméra, on a noté qu’il fallait faire attention aux additifs, ces Elements décoratifs comme un curseur etc., qui disparaissent lors du reinit du subset n’étant pas sur la carte.

Ce point étant fait, dans Nahyan nous aimerions avoir un sol, ou de l’eau, et la possibilité de les parcourir. D’y avoir des objets, traversable ou non, comme de l’herbe ou une caisse, que l’on peut escalader ou non si on peut. Le seul facteur étant le temps de l’animation et du passage à la case suivante peut-être etc. Ne pas oublier que cela dépend des gestes et compétences connus. Avec cela il faudra ajouter les additifs.

J’imagine également que l’objet sol est interactif, par exemple on peut creuser le sol, pêcher dans l’eau, plonger/chercher dans l’eau etc. On ne parle pas encore ici de modification de terrain, bien que ça reste une idée intéressante mais difficile à tenir vu le mode graphique. Cependant, un minimum de terrassement me semble tout à fait possible. Enfin, ici on parlait surtout du côté interactif et conteneur de l’objet sol.

J’imagine donc que l’on garderait un type « Ground », qui resterait le plus bas niveau et l’unique Element de ce type de l’empilement et forcément en base de pile. viendrait par dessus les Elements décoratif traversable (herbes/buisson de cueillette) et additifs de sol (curseur). Enfin viendrait les objets qui s’empileraient.

En y pensant, si on dépose une caisse sur de l’eau, il faudra prévoir que l’eau détecte ce qu’il y a sur elle, et, fonction du poids et d’une sorte de flottabilité, l’engouffre (effet conteneur).

On imagine au final qu’il faudrait une méthode d’insert qui naturellement, en fonction du type de l’Element, l’ajouterait au bon endroit selon ces règles. Dans le cas de la caisse sur l’eau il faudra une animation pour symboliser la chute dans son conteneur.

Un mois de passé…

Une idée est apparue quand on a évoqué un autre projet intermédiaire : HeroQuest (voir article dédié). L’idée serait qu’en fait le sol soit en fait la coordonnée et que cet élément soit une division spatiale.

Il vous suffit d’imaginer un cube dont chaque face touche un autre cube. Ce qui vous fait 7 espaces exploitables, sans vouloir pousser le bouchon à manipuler des coins.

Ainsi vous avez le sol, l’espace exploitable sur-sol (là où le personnage se déplace), un espace au dessus ainsi que 4 espaces NESW à l’intérieur de la zone définie par le sol. Pour voir ça plus simplement imaginez une zone avec des murs appartenant à la coordonnées (sur le sol).

Voilà donc qui règle beaucoup de soucis. On garde les bonnes idées, empilement, calcul de la hauteur, placement de curseur et indicateurs, mais on réparti. Cela rend le système de rendu plus complexe, la gestion plus poussée, mais le tout me semble rendre plus de possibilités claire et moins de bricolage.

La technique n’est pas encore définie, seul le principe et ses tests théoriques sont validés, le reste ne saurait tarder.

Bon, sur ce, il est temps de sortir cet article.

Souriez vous êtes suivi

Il y a une chose que nous avons oublié avec nos tests miniatures, c’est le suivi de la caméra, son déplacement en fonction de la position du joueur. Le but étant de pouvoir se déplacer dans le monde et de garder la zone de visibilité du joueur à l’écran.

Il y a un tas de possibilités au sujet des caméras dans les jeux. J’en ai exploré deux. Suivre le joueur de manière centrée et déplacer la caméra si le joueur sort d’un cadre. Nous retiendrons que la seconde est beaucoup plus intéressante et agréable dans le cadre de Nahyan. On ajoutera à cela prochainement un centrage automatique sur joueur au bout de x secondes sans mouvement, pour être sur d’avoir la visibilité optimale.

Premier cas : suivi du joueur au centre

Il faut se rappeler que lors de l’initialisation de la Scene, une caméra est créée et que la vue est centrée sur la position du référentielle de la caméra. En gros on dit à la caméra que sa position de référence est x,y et on en déduit les offsets nécessaires pour le rendu.

Bouger le joueur/héro (mon caddie), ne déplace pas la caméra. Le lien entre les deux dépend du développeur de son projet utilisant TARS. On se positionnera donc dans l’événement d’update de la Scene, là où les Elements peuvent utiliser la méthode de déplacement (passée en paramètre pour rappel).

Là, l’idée est simple, si on change les coordonnées du joueur, on en profite pour calculer la différence entre la position initiale et la destination et on l’impacte aux offsets de la Caméra. De but en blanc ça donne un déplacement bloc par bloc très brutal à regarder.

Le déplacement se faisant, une chose oubliée alors sera le refresh du subset de la map, car sinon rien ne change visuellement, on ne fait que déplacer la map dans la direction du joueur, sans masquer/ajouter les éléments qui devraient apparaître ou disparaître. Pour se faire il faut changer le référentiel de la caméra en pointant la position du joueur et ensuite de demander le réinit du subset de la map. Ceci recrée également le subset du pathfinder (je vous laisse deviner l’importance de tout ceci).

Second cas : suivi du joueur hors cadre

On parlait d’initialisation de la caméra dans le premier cas, il faut aussi garder en mémoire le redimensionnement de l’écran (navigateur), qui change les données pour le rendu, les subsets etc. On va avoir besoin de s’accrocher à cet événement dans IsoPlayer pour fabriquer un cadre virtuel à l’intérieur de la zone de rendu. Un cadre dans un cadre.

Grâce à cet événement on récupère la taille à jour de l’écran, on peut donc créer 2 points A et B, symbolisant le point haut gauche et bas droite du cadre. Idéalement ça serait du 25% de tous côtés, mais on a découvert que l’élévation en Z des blocs change l’impression visuelle, on a donc corrigé en haut 40%, gauche 30%, droite 30%, bas 30%. En gros le point A commence à 30% du bord gauche et 40% du bord supérieur.

Représentation factice du cadre

Ensuite, on en revient à l’événement d’update de la Scene, mais au lieu de se placer dans la fonction de déplacement du joueur on se place sous sa fonction d’update, ainsi on récupère les données à jour du joueur. L’idée est de détecter s’il est hors du cadre, et de combien et d’affecter les offsets de la Caméra de ces valeurs.

Le fait d’être hors de la fonction de déplacement nous permet de demander la position du jouer AVEC son delta de déplacement, ce qui va nous permettre d’avoir une glissage d’écran fluide, et non saccadée comme le premier cas.

Il nous reste ensuite à faire le rafraîchissement du subset. MAIS, comme on a changé l’idée du déplacement, avec le cadre, garder le joueur comme point référentiel est une erreur qui va vous faire de sales emmerdes si vous ne faites pas attention au pourquoi du comment. Le référentiel est en fait maintenant le centre de l’écran.

Réfléxions globales

Les valeurs modifiant les offsets et le point référentiel de la Caméra sont donc les 2 piliers du suivi caméra.

Les calculs nécessaires se font à chaque passage d’update, il faut donc porter une attention particulière à l’optimisation; ne recréer le subset que si nous avons bougé; n’inspecter le déplacement que si nous sommes le joueur (dans notre cas); ne pas tester des directions de déplacement si son opposée est déjà validée; etc.

Une autre observation globale est que le curseur et target (anim01) sont ajouté avec le joueur après l’init du subset. Car ils ne font pas partie de la map, ce sont des additifs. Le fait de déplacer la caméra efface donc le curseur et target du rendu. Cette observation sera valable pour tous les additifs visuels, il faudra donc les prendre en considération au moment du réinit du subset par IsoPlayer, qui ajoute déjà le joueur.

L’aiguillage de ton titre

Je me suis recréé un poste de travail sur mon PC principal, ce qui m’a demandé de réinstaller toute la panoplie de dépendances, logiciel, etc. De plus j’ai mis à jour le package.json ce qui a engendré quelques soucis, du coup nettoyage, un tslint réveillé et quelques commits plus tard : une solution fonctionnelle, à nouveau ^^.

Et donc, quelles sont les nouvelles ? J’ai écris l’écran titre, basé directement sur Scene, bien suffisant, et sur un Sprite (et non un Element), c’est là que certaines choses apparaissent comme « au mauvais endroit », trop spécifiques pour être dans TARS, à diviser ou à revoir, la liste n’est pas petite mais ce sont les aléas des POC continus j’imagine.

Cet écran titre est donc la Scene de démarrage, fait apparaître ‘daaboo’ progressivement, attend 2 secondes puis fondu au noir. Il restait ensuite à définir ce qui se passe ensuite, le fameux aiguillage dont j’ai déjà parlé.

L’aiguillage a pris la forme d’un service chef d’orchestre (Conductor) et reçoit les demandes de changements de Scene. Actuellement, on démet et on ajoute à la suite une Scene nommée. Ce n’est pas assez, mal nommé et à revoir, mais après avoir tergiversé une heure sans trouver un angle d’attaque à ce sujet complexe, j’ai décidé de démarrer quelque part et ça fonctionne. Cependant il faut rester clair : c’est à revoir.

C’est la Scene titre qui déclare, une fois arrivé au fondu au noir, la demande de changement de Scene et c’est elle qui sait vers qui elle veut nous rediriger. Ceci démontre bien que cette séquence fait partie du « jeu » et non de TARS. En gros, Conductor reçoit la demande et émet un événement que Game reçoit en s’étant abonné dès le début, ainsi il peut modifier les Scenes actives.

Ceci permet aussi à d’autres Scene qui en auraient l’usage, de s’abonner et de réagir selon les changements. On imagine facilement un HUD se modifier en fonction de la Scene de rendu ou un ensemble de Scene composant un rendu en couche qui auraient besoin de savoir ce qui se passe. Ceci n’est pas un canal de communication (développement spécifique au projet) mais une gestion propre à TARS concernant les Scenes.

Pour étayer ce qui ne va pas dans Conductor il faut s’imaginer une Scene de départ et une Scene à venir, le tout en gardant en tête que chaque Scene a une transition d’arrivée et de départ. Du coup si vous voulez quitter la Scene de départ pour aller vers la suivante, vous déclenchez en fait sa transition de sortie, elle n’est donc pas encore inactive et à retirer de la liste des Scenes actives. C’est cette Scene de départ qui doit en fait se gérer et quand elle a fini, lancer la transition vers la Scene suivante.

Il faut aussi garder en tête que le loader s’affichera si la Scene suivante n’est pas chargée au moment de l’activer et ce, sans transition. Donc pour faire de belles transitions, il faut que les Scenes soient chargées à l’avance ou en tâche de fond, c’est à dire charger la Scene sans la mettre dans les actives et garder le lien quelque part, le récupérer dans une Scene peut-être (ce qui n’existe pas, mais pas bête de le prévoir).

Enfin, une Scene peut s’ajouter par devant ou par dessous une autre, permettant telle ou telle transition ou effet de passage d’une à l’autre. S’ajouter à plusieurs peut-être et retirer de la même manière. Et est-ce utile de penser un remplacement ou déplacement de Scene(s) ? Beaucoup de questions et de gymnastique de code pour gérer l’ensemble.


La suite ?

Conductor sera modifié au cas par cas, jusqu’à remplir tous ses objectifs au mieux. Rien ne sert de se concentrer dessus absolument vu sa complexité.

TARS en lui-même a une belle liste de TODOs (~22) et comme dit ci-avant, certains morceaux doivent en sortir, ce qui ne va pas se faire sans mal.

Du coup, le meilleur moyen d’avancer, même sans serveur/back actuellement, est d’avancer sur le POC d’usage et donc « Nahyan », enfin, un semblant. Je pense continuer d’explorer l’interaction qui sera quand même la base du projet. J’ai émis quelques idées sur le comment mais je vais en faire un article à part entier.

Il reste également l’éditeur comme sujet assez imposant. Là encore, est-ce du TARS ou un outil de jeu. Les types d’Element étant un problème soulevé à sortir de TARS, c’est encore un beau débat.

Il y a de quoi faire c’est certain. Affaires à suivre… 🙂

Le solitaire

Ceci est l’histoire d’un programme qui hantait mes pensées et mes rêves depuis presque 20 ans, tenté par plusieurs langages et labourant mes méninges de mille façons… Son nom résonnera longuement dans tous les méandres d’Internet… Écoutez ! Il se nomme Nahyan !

Derrière ce titre se cache un clin d’œil à la saga Axle Munshine, le Vagabon des Limbes, découvert grâce à mon père. Plus particulièrement au numéro 22, éponyme de cet article. Car ce que vous ne savez pas, c’est ce qui est arrivé à TARS !

Mais quel est le foutu rapport ? Tout simplement une division, la première entre ce qu’est TARS : un framework; et Nahyan : le projet. Destinés à vivre en osmose mais clairement séparés. D’où le clin d’œil à « Le solitaire », où 2 personnes s’aimant fusionne un peu trop…

C’est le constat de départ en voulant ajouter un écran titre, éprouver le loader et ainsi suivre le plan de l’article précédent. Les 2 ont fusionné un peu trop : POC et framework.

« Bah c’est simple suffit de les séparer… » Et une baffe, t’en veux une ?

89 modifications, des jours et quelques fixes plus tard, l’ensemble fonctionne à nouveau, mais non sans mal. Voyons en gros ce qui a changé.

D’abord le code de TARS est déplacé dans un répertoire à lui tout seul au sein de l’app, tel une dépendance. Ainsi quelques classes ont été sorties comme DataProvider et les implémentations d’IsoScene.

Ensuite il a fallu rebrancher ce qui ne fonctionnait plus, ou ne devrait plus de la manière actuelle, au vu de la séparation des mondes. Clairement le DataProvider, une variable d’environnement et quelques liens et broutilles annexes.

Concernant le DataProvider c’est intéressant d’en parler un peu plus, car avant on étendait la classe de TARS comme modèle à suivre pour être compris du framework, ainsi le système d’injection savait la récupérer, mais vous ne pouvez pas le faire si le framework ne connait pas votre classe externe (logique) du coup il a fallu trouver une autre solution qui ne casse pas tout le code et reste logique.

Ainsi le DataProvider dans TARS devient un conteneur, le modèle devient une interface (ça reste une obligation à suivre) donc le conteneur ne peut recevoir qu’une classe qui suit le minimum requis par l’interface sans vous empêcher de faire des rajoutes pour votre développement. Ainsi on accède au DataProvider réel en le demandant avant de le toucher.

this.dataProvider.loadCursors()

Devient :

this.dataProvider.get().loadCursors()

Ce n’est peut-être pas la bonne manière ou la manière définitive, mais ça fonctionne et le POC va pouvoir se poursuivre. Ainsi on repart sur le plan précédent avec un écran titre et le système d’aiguillage, développé et testé en même temps grâce à cette division.


Autre point important, les Scenes qui étaient injectées dans Game via un fichier de configuration. Et ben ça c’est du passé, en tout cas dans TARS. C’est à l’utilisateur du framework de faire un appel à Game.load() en lui passant les scènes avec le nom de celle qui démarre et de celle qui représente le chargement. Ceci sera à voir quand l’aiguillage apparaîtra.

Cette injection manuelle se fait dans le component, ce qui réaffirme que le component fait partie du projet et non de TARS lui-même. Ceci permet aussi d’injecter ses propre classes héritant de telle ou telle déclinaison de Scene. Tars s’occupera de ce qu’il connait et vous pourrez ajouter et gérer votre niveau à votre aise sans vous tracasser d’avantage.

Rien ne vous empêche de continuer à utiliser la factory des scènes qu’on a retiré ou d’utiliser un fichier de configuration pour votre projet si vous le jugez nécessaire, mais ceci n’entre pas dans TARS.


Tout ceci me ramène à mes développements de Badawok. Cette séparation ramène de très bon souvenirs de challenges de construction de framework et relance les réflexions et les possibilités du projet TARS. Petite bouffée d’oxygène une fois accompli, un Mars (erk) et ça repart !