Tractopelle de cerveau

Seconde partie de la capillotractorisation pour résumer le second mois écoulé. Une fois le déplacement et ses différentes techniques en place, un nouvel objectif a été décidé : l’écran de chargement. Ça parait con, mais pas du tout !

En gros, il faut charger une scène de chargement avant le reste et l’utiliser quand les autres ne sont pas prêtes. Jusque là, simple en théorie. On ajoute donc une scène dans le fichier de conf du jeu, on modifie Game pour le chargement et … écran blanc !

Mais cette fois il ne m’aura fallu que 5 jours de réflexion pour trouver que c’est le même problème que notre précédent soucis d’Observable vs EventEmitter. Ceci dans une autre mesure car cela touche toutes les couches : Game > … > Element.

Du coup, remplacement des fonctions load() , retrait de l’Observable, ajout de l’event, rebranchement, et toujours pas bon. La manière de charger Element manquait un cas, non détecté car un chargement se passe vite et cela n’a jamais déclenché de soucis. En gros, un Element sur demande est chargé et surveillé mais du moment qu’il est chargé l’event onLoaded (alerte de fin de chargement) n’est pas émit. Ça ne peut donc pas fonctionner, logique.

Une fois le tout rebranché, vérifié et contrôlé à nouveau, ça fonctionne. On a donc un système qui charge un loader sans rien afficher, affiche le loader et charge le reste. Le loader est retiré quand tout le monde qui doit être actif est prêt. Actuellement le loader est un IsoScene qui utilise les mêmes Sprites donc à peine il apparait qu’il disparaît. Du coup prochaine étape, un écran de titre pour tester plus avant le loader et installer un aiguilleur de scènes.


Autre point majeur qui a nécessité et nécessite encore des modifications, j’ai ajouté un éditeur, ce qui impacte 2 choses : un Component pour le routage et une Scene pour le comportement.

Ceci a demandé de diviser, pour mieux régner, le composant Iso en 2  : Editor et Player. Mais va demander de créer une couche Viewer pour parfaire les possibilités. En gros la différence se joue sur les éléments réactifs nécessaires, cursor, target, hero et la surcharge et comportements préparés au niveau des events souris (pour le moment).

C’est là qu’un petit défit apparaît, la fonction load signale la fin du load, mais dans le cas d’un héritage (Iso > IsoPlayer), la classe player aura aussi  une fonction load qui appellera celle du parent, du coup 2 fonctions qui signalent la même fin. Sauf que si vous êtes une classe player vous ne voulez pas que le parent prévienne, c’est votre rôle. Comme on a retiré les Observables au profit d’EventEmitter, on doit trouver une solution différente.

Et pourquoi pas un retour aux sources ? On passe à la fonction load une fonction de callback. Si présent on l’appel, si non présent on signal la fin du load. Allez pour une fois je vous montre un extrait de code.

// Classe mère Scene
public load(callback?: Function) {
  // Need to be overrided
  if (callback) {
    callback();
  } else {
    this.onLoaded.emit(true);
  }
}

// Classe fille Iso
public load(callback?: Function) {
  super.load(() => {
    ... du code ...
    ... une condition de fin ...
      if (callback) {
        callback();
      } else {
        // Alert that we are loaded
        this.onLoaded.emit(true);
      }
    ... fin de la condition de fin ...
  });
}

// L'appel dans Game
this.activeScenes.forEach((s: Scene) => {
  s.onLoaded.subscribe((sceneLoadResult) => {});
  s.load();
});

En gros si on appel s.load() sans passer de call back, la classe appelée est le dernier niveau, et elle appellera ses parents dans l’ordre avec la même logique. Du coup player se charge (sans callback), se passe à son parent (Iso) sous forme de callback, le parent (Scene) fait de même. Du coup Scene appelle le callback de Iso et l’exécute, au moment de vérifier la fin, il y a le callback de player a exécuter, qui à son tour se termine et au moment de vérifier la fin, pas de callback donc on peut enfin signaler la fin du load() via l’event onLoaded.

Et ça fonctionne ! Plutôt pas mal même, tout en maîtrisant le chaînage des appels et en prévoyant à chaque niveau la surcharge possible, ce qui rend l’ensemble flexible et extensible. Il ne faut pas oublier que TARS est un framework et que Nahyan étendra les classes proposées pour ajouter les comportements désirés spécifiques. Player par rapport est déjà une surcouche peut-être trop spécifique mais rien n’empêche le développeur de partir sur base de Iso et non de player s’il se sent l’âme de coder ce qui lui faut.

On a donc un ensemble fonctionnel de structures à finir (viewer) et il reste à progresser sur l’éditeur.


La suite ?

L’éditeur pour manipuler map ainsi qu’un écran titre pour l’aiguillage entre scènes. Ensuite il faudra sûrement revoir la structure pour séparer framework d’implémentation.

On pourra aussi poursuivre côté interaction du héro à destination (arbre) en faisant attention à la séparation framework/usage, sûrement avec un POC, et donc un sytème d’interaction (fenêtre. écran conditionnel, …).

Il y a aussi un point qui est embêtant pour le moment c’est la capture vidéo du canvas, mais des personnes ont développé des solutions d’extraction à partir du canvas pour écrire un gif, le tout via le browser, ce qui est assez costaud. C’est assez complexe et j’aimerai faire quelque chose de simple, donc on verra si usage ou non de lib externe et possible solution simple interne. Sinon en parallèle, je pensai mettre un espace démo qui serait mis à jour en fonction des articles pour l’illustrer.

Évidemment il y a aussi la partie serveur qui reste fort obscure pour le moment et dont un des points est la capacité de faire le temps réel et donc du push ou un socket, ce qui semble préférable. Je me base pas mal sur l’expérience de BrowserQuest, qui se base sur un back nodeJs pour WebSockets, mais je n’ai aucune expérience de node et que de la théorie sur webSocket, ça risque d’être chaud. À la base, je voulais faire le back en PHP, ma référence précédente, il y a Ratchet, à voir qui me conviendra le mieux.

Au final, le socket n’est là que pour rapatrier l’information à jour et la plus légère possible. C’est l’action de l’utilisateur qui envoie la mise à jour et un serveur quelconque qui s’occupe des impacts.

Imaginer un système ou les joueurs parlent et bougent est simple, mais du moment qu’il y a projectiles, déplacement en combat, déplacement d’objet ou altération de la carte, là ça devient plus compliqué. Mais ce n’est qu’un challenge de plus à affronter après tout :). Le tout est de bien comprendre comment découper l’ensemble et que chacun fasse son travail.

Affaire à suivre donc 🙂

Capillotractorisation

Nous voilà quasi 2 mois plus tard, et non je n’ai pas abandonné ! Non je ne suis pas passé à autre chose (pour changer…) ! Je suis resté coincer +2 semaines sur un soucis et 5 jours sur un autre. Détaillons ça ensemble.

Premièrement, j’ai suivi le plan annoncé, mais pour ce faire j’ai dû introduire la notion de direction à Element et donc comment dessiner un même objet dans une direction différente ? Après plusieurs tergiversations, l’idée est de faire 4 images arrangées de la même manière pour préserver le mappage d’animation et on les nommera par leur direction.

Ensuite il faut une technique pour dire à Sprite l’usage et la manière dont il peut s’en servir. Pour faire générique j’ai appelé ça des Layers (calques), ainsi on a la longue chaîne :

Game > Scene > Element > Sprite > Layer > Image

Ceci donc rajoute une couche d’Observable dans la longue chaîne et pouf… écran blanc, chargement complet sans erreur et rien de plus…

Il m’aura fallu +2 semaines d’expérimentation pour déterminer que les Observables étaient la cause et que la solution était de les supprimer et remplacer par des EventEmitter. Et besoin de rien changer d’autre ! Car compatible, excepté la division de l’action appelée et du listener en gros (oh zut, une ligne de code en plus…).

Ceci fait tout fonctionne comme avant, à ceci près qu’on a des Layers en plus. Du coup j’ai pu avancer sur la partie pathfinder et déplacement de notre héro le caddie.

Caddie, ce héro

Ce caddie, récupéré d’un POC Auchan raconté dans un article précédent, a déjà les 4 images nécessaires donc allons y. J’ai modifié les fichiers de configurations, les interfaces, les chargements et tout le nécessaire. Il ne reste plus qu’à le faire bouger !

Commençons par le pathfinder, le calcul entre la coordonnée du clic et la coordonnée actuelle du héro. Car sans chemin à suivre, pas de déplacement ! Ainsi en ayant décortiqué la mécanique des différents pathfinder j’ai développé une petite solution sympa qui nous donne en un temps record directement le chemin final (si trouvé) et grâce à un petit raccourci (en mode throw exception à travers des boucles) on gagne pas mal de temps.

Pour les curieux, un pathfinder simple s’occupe de partir de la coordonnée de destination et de se propager de case en case jusqu’à ce qu’on trouve le héro. Il suffit de remonter le chemin à l’envers. Si on parcours tout sans succès c’est donc injoignable, ou on est hors carte. Ceci fonctionne avec tous systèmes de grille. Il ne faut pas oublier de préparer la grille de coordonnées utilisables, car on ne peut pas se déplacer sur un arbre ;).

Ensuite, ça devient « comique ». Nous avions déjà une animation avec le Target (vagues bleues), mais là il s’agit de déplacer physiquement un objet d’une case à une autre. Cependant, nous avons comme coordonnée un nombre entier ([1,1], [2,5], etc.). J’ai donc ajouté un delta lors du draw() qui déplace le point de référence et donne l’impression qu’il se déplace. Évidemment j’ai ajouté dans l’update() un petit calcul qui détermine un déplacement dans un laps de temps défini et par une règle de trois on sait à quel pourcentage du chemin (entre deux cases) il se trouve.

C’est là qu’on oublie un détail, il est toujours attaché à sa coordonnée, seul le delta change. Et donc il passe sous les blocs dessinés ensuite… logique… :(. Il ne faut donc pas oublier de changer de « parent » l’objet qui se déplace au moment d’atteindre sa destination. Mais ce n’est pas suffisant. On a un caddie qui disparaît (si coordonnées supérieures), sous le bloc durant son déplacement puis réapparaît soudainement, on a un clignotement désagréable.

Pour solutionner ce clignotement et parce qu’on a décidé d’avoir une 3ème dimension dans le sol (l’élévation), la solution a été de changer de parent à mi-chemin, ainsi on réduit la gêne et l’effet de clignotement. Le tout en bénéficiant d’un effet favorable puisque l’objet en déplacement s’adapte en Z lors du changement de « parent », ce qui ne se faisait pas avant. On a donc l’impression que le caddie monte/descend bien les « marches ».

Ensuite vient la rotation, ce qui a été rapide à solutionner. Prenez la coordonnée actuelle et la suivante et vous pouvez calculer l’orientation. Là on adapte alors la valeur d’orientation de l’objet et le caddie tourne.

Encore plus fort et sans les mains, si je clique sur l’arbre, il ne se passe rien, mais si c’était un coffre j’aurais envie que le caddie aille devant, bien orienté et interagisse avec. Pour ce faire on a besoin d’une notion de zone d’accès. J’utilise le système d’orientation NESW car l’objet peut pivoter aussi. Donc si mon arbre est orienté à l’Est, L’Est devient son Nord. Là ça devient compliqué dans la tête.

Du coup, quand je clique sur l’arbre, la coordonnée est refusée, on cherche le premier élément sur le sol et on lui demande ses zones d’accès s’il en a, sans oublier de réorienter en fonction de l’orientation de l’objet lui même. Ensuite on les teste toutes, et si elles ne sont pas occupées, on comparera la distance entre chaque et on gardera la plus rapide. Évidement, cas réel, si on est déjà dessus, on zappe directement et on passe à l’interaction.

On a donc un système de déplacement agréable et fonctionnel. Une kyrielle de bugs a été corrigée en même temps, comme la duplication du caddie pendant son déplacement, le resize qui faisait de même (mais autre raison), la perte du caddie en déplacement etc… Je ne me rappelle plus de tous les cas corrigés. Le code a bien été peaufiné. Même si, au final, vous ne voyez pas beaucoup de nouveauté.

Pour ne pas vous prendre trop la tête en une fois je vais diviser ce compte rendu en deux. 🙂 La suite n’en est pas moins capillotractée.

Carrêmisation

Qu’est-ce que la « carrêmisation » ?

Et ben rien du tout 🙂 C’est l’optimisation qui fait faire un régime sec à notre système de raycasting, en évitant dès que possible le travail inutile. Ceci se fait de plusieurs façons.

La première consiste à commencer par le haut. Car les chances sont plus grande qu’un objet sur un autre recouvre celui-ci. Là aussi il y a deux points à regarder, d’un côté la Scene qui contacte ses Elements un par un dans son sens inverse de rendu (dans le cas présent de la Scene Iso – isomérique) et d’un autre l’Element lui même qui possède potentiellement plusieurs Sprites, dès lors Element générique les empiles et donc le contrôle se fait en commençant par le haut.

Ensuite, même si déjà fait dans mon code précédent, on peut épargner des contrôles de bounds si au moins un a été touché par le point.

Vous remarquerez que le contour du tronc n’apparaît plus en noir.

La seconde, consiste lors du test d’un Sprite de détecter s’il a des bounds (bords pour la détection) et ensuite, s’il en a, une fois les calculs de repositionnement effectué, on test si le curseur est au minimum dans le cadre virtuel de l’image du Sprite.

Le tout combiné, entre les optimisations côté Iso et Element, nous donne ceci :

On remarque que seuls les Elements qui sont susceptibles d’être en contact avec le point ont été marqués, ce qui signifie que le calcul n’a été fait complètement que pour ceux-là. Vous imaginez donc, entre la première photo de cet article et cette dernière, l’avantage directe de cette optimisation.

Premier contact, et plus si affinité

Le curseur

Dans la continuité, je me suis attelé à m’occuper des interrogations précédentes, car sans un pointage efficace il sera difficile de bien comprendre ce qui se passe, surtout en cas d’erreur. Du coup, aujourd’hui, c’est la révolution du curseur !

Premièrement, il a fallu ajouter une méthode pour savoir si on est sur un Element ou non, quelque soit l’algo, et ainsi le dire à la Scene pour qu’elle s’arrête au plus tôt (économie) et puisse déterminer la position, ou ne rien trouver.

On part du fait que la souris donne sa position, je vous passe le trafic des événements, et on pose le curseur à la coordonnée reçue.

Premier point : le curseur se met au niveau zéro et c’est chiant, car avec l’élévation, l’œil humain se perd et à du mal à coller la grille sans repère. Donc on va s’occuper de ça directement.

Il s’agit d’insérer l’Element curseur sur le dernier Element de type Ground de la coordonnées, donc après si on regarde la liste des Elements de cette coordonée. Il ne faudra pas oublier de le retirer au prochain déplacement significatif de la souris (x ou y différent).

Ensuite, on peut revenir sur la détection du curseur, et là on fait du raycasting (lancé derayon), car la position de la souris sur base de la grille n’est pas suffisant, la souris touche quelque chose et pas forcément le niveau zéro d’une case de la grille. J’ai donc opté, pour commencer quelque part, par le cadre de l’image au complet.

En gros on va prendre le chemin inverse du rendu. Au lieu de commencer au plus haut on part du plus bas, en remontant inversement les Elements dessinés.

Les images finales en fin d’article démontrent clairement ce que j’exprime ici, vous verrez.

On se base sur le point de repère de l’image, on ajuste en fonction du point de pivot (basex, basey), le tout en prenant la bonne frame du sprite et ses caractéristiques et surtout, on oublie pas de ramener tout aux coordonnées 0, 0 de l’écran (client du navigateur). Sinon comment comparer une position de souris avec une image quelque part à l’écran, les 2 avec un système de coordonnées différentes.

On a donc un premier jet fonctionnel, mais vous remarquerez directement que ce n’est pas idéal, on compare des rectangles théoriques avec des losanges visuels, l’utilisateur ne comprendra pas, et c’est normal.

On peut améliorer ça si on se donne un peu plus de mal, et là j’ai cherché comment détecter un point dans un polygone, mais entre Math complexes et obscures, librairie fermée et autres, les possibilités sont nombreuses et pas toutes idéales (vitesse, facilité, …). J’ai opté pour une ligne dans un triangle et là j’ai découvert qu’en fait c’est 3 fois une comparaison du point avec une droite, de quel côté le point est. En sens horlogique, les valeurs négatives sont à « l’extérieur » de la droite si on considère le triangle comme 3 droites qui ferment le polygone. Du coup on peut augmenter le nombre de droites, tant que le tout reste un polygone convexe. Ce qui nous donne après plusieurs encodages des données de points :

C’est beaucoup mieux n’est-ce pas ? Plus précis surtout. Là il ne faut pas se tromper, on démultiplie les tests, donc il ne faut pas oublier de couper court dès qu’on sait si on touche au moins une fois l’Element.

Suite aux expérimentations, tout fonctionne, mais on perd le curseur dans la zone grise quand on a pas de Ground présent sur la grille. Cela vient du fait que le rayon n’a percuté personne donc ne rend rien, du coup il faut revenir à l’ancien système de coordonnée de souris vers coordonnée du monde, tout simplement, comme avant.

Ceci dit, ce monde démarre en 0, 0 et les valeurs négatives sont refusées par une des méthodes, je dois encore regarder à ça, mais pensez-y.

Notez également un problème avec le resize, le curseur se démultiplie bizarrement visuellement, mais rien de clair actuellement sur la raison. Il faudra inspecter l’état de map ou subsetmap, et ce n’est pas du tout une mince affaire…

Le chargement chaîné

Depuis le début, une des problématiques est d’avoir les infos quand elles sont disponibles et nous sommes dépendant de chargement de Sprite. La Scene s’occupe du chargement de ses Elements mais aussi des Sprites via le service Resources. J’ai modifié ça, et je pense améliorer l’idée et la mécanique en mettant le chargement de l’image dans l’Element et en modifiant fortement la manière dont Resources s’en occupe. Désormais, le premier Element qui veut charger un Sprite, va créer un Observable dédié et s’y accorcher. Le second Element qui a besoin du même Sprite ne va pas recréer mais s’accrocher tout simplement à l’Observable déjà créé. Ainsi, quand le Sprite a fini, il prévient tous ceux accrochés. Je rajoute que si le Sprite a déjà fini de se charger quand on le demande, on renvoie un Observable auto clôturé, ce qui donnera l’info directement que c’est bon, c’est chargé.

L’avantage direct est l’initialisation de l’Element sur base des infos des Sprites dont il a besoin. Pensez animation et c’est direct le bordel dans la tête. Un Sprite contient les définitions du possible, l’Element contient l’état actuel. Ainsi l’Element, lors de son update(), cherchera les infos de l’animation courante qu’il a décidé de jouer dans le Sprite. Chacun son rôle, mais si Sprite n’est pas chargé quand vous voulez initialiser Element ça ne fonctionnera pas car pas encore disponible.

Initialement, je faisais cet initialisation au premier update de l’Element, car là on savait que tout était chargé. Il ne peut y avoir d’update d’une Scene que si celle-ci a obtenu le feu vert du chargement des ressources.

Maintenant, au lieu d’un « if » perpétuel, valable qu’une seule fois, dans la fonction update, on a une initialisation propre et un update tout aussi propre.

Plusieurs Sprites pour un Element

Un sujet qui traîne depuis une refonte, c’est le fait qu’un Element peut avoir plusieurs références de Sprite, ceci pour un objet composé comme un arbre, un personnage etc.

Mais que faire au niveau de base de Element, toutes les méthodes actuellement prennent le premier de la liste et ne s’occupent pas du reste. Pire il n’y a pas de contrôle ou d’adéquation entre la configuration envoyée à l’Element et sa nature (je suis une fougère…).

Du coup, j’ai complété une idée précédente, encore un POC, mais qui tient quelque chose. J’ai créé un service DTD (document type definition) et créé 2 listes de références pour déterminer le chaînage entre des Elements et une autre pour déterminer de quoi est fait un Element typé. En plus clair, si je suis un Tree (arbre), j’aurais besoin d’une définition pour Trunk (tronc) et une pour Foliage (feuillage), sinon comment l’Element de type Tree pourra savoir qu’il a bien reçu les Sprites dont il a besoin s’ils ne sont pas identifiés ?

Nous avons donc maintenant un Element (générique), qui reçoit son type par la configuration et une suite de références de Sprites, on passe cette liste par une moulinette de contrôle grâce à la DTD de types et nous avons là une liste identifiée de références propres au type de notre Element.

Là de suite ça n’apporte pas grand chose, juste un contrôle de ce qu’on reçoit, ne prendre que ce qui est possible pour notre Element typé. Un Tree ne va pas s’occuper d’une référence de roché par exemple, il l’ignorera. Nous sommes d’accord que ce cas de figure n’a pas lieu d’être, sauf si édité à la main avec erreur.

L’avantage de ceci est que si nous créons un éditeur pour TARS/Nahyan, en cliquant sur un Element, on recevra sa cartographie via son type et donc on pourra créer un formulaire pour choisir les Sprites attendu par le composant. Prenez un arbre tronc et feuillage, sélectionnez le, modifiez le tronc de chêne par un tronc de bouleau et un feuillage d’automne plutôt que d’arbre mort, un choix de texture typée pour un Element ayant une définition venant de la DTD.

S’il n’y a pas de définition, un type par défaut englobe tous les cas où il n’y a qu’une seule image pour l’Element, ce qui facilite les encodages, les réduits et n’oblige pas de faire une sous classe par type si la seule différence est cette notion. On a donc un Element générique capable de beaucoup de chose à lui tout seul.

Dans notre cas d’arbre, l’Element Tree doit dessiner un tronc et un feuillage. si vous vous rappelez un article précédent sur l’empilement, il suffisait de faire X Element et de les mettre dans l’ordre à une coordonnée, l’empilement aurait fait le travail, mais vous n’auriez pas la possibilité de donner un comportement global et programmé à votre arbre. Vous voulez une interaction, et peut-être que le tronc fasse 4 hauteurs de bloc avant de mettre le feuillage, il vous faut donc une extension de la classe Element, en étant générique, elle ne traite pas les particularités, c’est un peu le but.

On créera donc une nouvelle classe Element pour notre arbre, on l’appellera TreeElement et on surchargera uniquement la fonction de dessin et de contact pour le raycasting. En fonction de paramètres spécifiques, prévus dans la définition de la configuration possible des Elements, nous pourrons dire la taille de notre arbre, qui sera le multiplicateur du tronc par exemple (si on considère un tronc comme un bloc de tronc).

C’est là que la définition par le type de nos références de Sprite a de l’importance, car notre TreeElement pourra explicitement faire une référence aux Sprites dont il a besoin par leur nom prévu. Il sait qu’il peut manipuler un Sprite nommé Trunk, et un autre nommé Foliage. Ainsi il saura quoi faire lors du dessin et lors de la détection, vu que les 2 dépendent de comment le dessin final est monté.

C’est complexe a expliquer, mais une fois que l’on comprend le rôle de chacun, la nécessité de séparer proprement les concepts, la manière dont c’est utilisé etc. alors cela devient simple pour vous. Mais je vous accorde que c’est un exercice de pensée qui n’est pas aisé surtout avec juste des explications et quelques illustrations sommaires.

La suite ?

Toujours le pathfinding et l’objet NESW, qui restent la suite logique de ce qui a été fait jusqu’à présent. Ainsi que les quelques bugs décelés : valeurs négatives refusées sur la grille, considérer le haut d’abord dans la détection de contact et le soucis de curseur lors du resize, entre autres.

Ça avance 🙂

Une pause curseur, c’est un événement !

Ce titre racoleur (:p) ne fait qu’annoncer 3 choses : le retour du curseur (qui m’a fait paniquer un instant), le retour des événements et le système de pause.

Commençons par le système de pause, même si sujet technique, si vous changez de fenêtre ou d’onglet de votre navigateur, le système (canvas) se met en pause technique, et à votre retour il recommence. Jusque là ok, sauf qu’on boucle en déterminant le temps qui passe entre 2 images pour faire avancer de manière fluides les animations et autres. Du coup quand on revient c’est le bordel, tout s’excite et essaye de rattraper le temps perdu, mais vous n’étiez pas là et pour vous ça aurait juste dû s’arrêter et reprendre à votre retour.

Autre phénomène d’exemple, une vidéo, vous arrivez, on joue un son ou vidéo, vous changez d’onglet, ça serait souvent préférable de stopper l’ensemble. Évidemment ceci est soumis à une somme de variables dépend de ce que vous en faites. Un jeu se met en pause c’est sympa, ça vous préserve, mais un MMO ne se met pas en pause, même s’il faut stopper techniquement le rendu et peut-être les sons. Il y a matière à réflexion, et c’est pas demain que j’aurais les réponses, et encore moins de manière générique.

Ensuite nous avons le retour des événements, pris par le service Navigator, qui fait le pont entre le composant Angular et le programme/jeu et ses besoins. Et donc le curseur !

Pour le curseur c’est devenu très simple, il suffit d’avoir un Element, le charger et le dessiner aux coordonnées reçues par l’événement mouseMove (quand on passe la souris sur l’écran). Le truc c’est que rien ne charge le curseur, ses images, ni ne définit son animation par défaut (même si une seule image c’est une animation figée). Il y a encore de quoi améliorer bien sur. Du coup là on charge le curseur définit dans la conf de la scène, ne serait-ce que pour le Sprite et son type, sait-on jamais, ça ne coûte pas grand chose actuellement, on verra sur la durée.

Là où j’ai paniqué c’est au rendu… car j’avais pas tilté directement qu’on avait l’empilement et donc la gestion de hauteur et donc un sol au dessus du niveau 0. du coup notre curseur est dessous, mais dessiné par dessus.

Avant

Encore plus de question, où devrait apparaître le curseur, ou est-ce que le sol devrait être descendu pour rendre l’idée d’origine ? La seconde option me plait, mais il faudra voir en fonction des élévation, et là on retombe sur l’idée de déplacer le curseur en fonction de la position du Ground (l’Element sol, qui n’existe pas encore comme tel). Mais dès lors, vous risque de voir votre curseur bouger verticalement alors que votre souris fait juste un passage de gauche à droit simple, car le curseur voudra s’adapter au terrain. Dilemme donc !

Il existe aussi la méthode du Raycasting (lancé de rayon), qui consiste à partir de la position du curseur de la souris à déterminer quel élément on touche. On parcoure les Elements du plus près au plus lointain (l’inverse exacte du rendu) et si la hit box 2D (= on touche le dessin ?) de l’Element attrape le curseur alors on s’arrête et on dit qu’on est là. On part donc du rendu visuel pour déterminer où on est, sans suivre la grille réelle technique. À peu de chose près.

Pour les détails, l’événement clavier a été raccordé pour désactiver la pause automatique quand on change d’onglet. Même si le débat demeure sur cette gestion de pause. De plus toutes les scènes n’ont pas besoin de l’infos de pause non plus (ex.: menu).

La suite ?

Comme dit dans le précédent article, un Element capable des 4 directions NESW et le pathfinding. Et peut-être des réponses à mes interrogations précédentes. Le nombre de TODOs ne cesse d’augmenter… ^^.

J’aimerais également sauver le code sur un git maison pour préserver tout ça, même si on enchaîne les POCs.

Et ainsi font, font, font, les petites barres


Tadaaa.. l’animation fonctionne !

Merci POC Sonic 🙂 ! Ceci dit, cela démontre également l’importance d’un Sprite Packer, ce n’est pas rien, mais tant qu’on est pas plus avancé je ne vais pas investir, et continuer de bricoler un peu. Quand on sera plus avant, il sera toujours temps de regarder les options (shoebox, texturepacker, …).

Comme vous pouvez le voir dans ce gif tout pourri (pas merci OBS et convertio), tout est rendu avec la nouvelle technique utilisant l’animation. Il reste a optimiser update() et draw() des objets sans animation, mais quand on y pense, ils auront tous au minimum les 4 directions NESW, mais on a pas encore décidé si Sprite divisé ou non, imaginez la composition du truc…

Quand on parle d’animation on oublie qu’il y a plusieurs genres, ici c’est l’animation des Sprites, mais il y a également le mouvement (déplacement en Z par exemple ou variance de x,y), ainsi que des particules ou effets par exemple, qui combineraient les 2.

La suite ?

Ayant récemment mis au point sur papier le pathfinding (trouver son chemin suite à un clic sur la carte), c’est la direction suivante que je vais prendre. Ceci comprendra l’orientation NESW, faire un élément Hero, regard suivant la souris, réactiver le clic, le curseur et les événements avec propagation, le déplacement en évitant les obstacles, du coup le zoom aussi tant qu’à faire (même si c’est réservé à l’éditeur, donc à voir).

Allez encore 25 TODOs sans cette liste précédente ^^…

Empilement de dessins

Petite victoire suite à l’article précédent, j’ai réussi à déplacer la fonction de dessin au sein d’un Element, proprement en supprimant la dépendance cyclique, tout en affinant les objets de bout en bout. Ce qui est amusant c’est que j’ai supprimé 50-80 lignes de code pour un résultat amélioré et identique au rendu, c’est toujours agréable.

Notez qu’on avait déjà refait le subset (portion de la carte à rendre à l’écran – optimisation), en considérant la somme du Tile suivit des Items à la même coordonnée ([Tile, Element, Element, …]) et ainsi faciliter le rendu tout en corrigeant la notion de couverture d’un Tile sur un Element : un morceau de terre recouvre l’ombre du second arbre dans l’image ci-dessous, sans ça l’arbre était dessiné après et donc l’ombre recouvrait le bloc de terre qui n’a rien à voir. Ainsi c’est plus juste visuellement tout en ayant une boucle plus facile à traiter.

Du coup map (contenant tout le monde chargé) a subit le même lifting que subset (le schéma de données d’entrées aussi), soit la somme directe de tous les Elements et on supprime Tile qui perd de sens si on donne Z à Element (c’était la seule différence). Du coup on pourra étendre Element pour les objets composés.

Ensuite, étant lancé, on (avec Boudine) a continué sur l’empilement et donc la notion d’animation, toujours dans la fonction draw de Element. Le résultat comique quand oublie qu’un objet a une taille, c’est qu’ils s’écrasent au niveau 0. Ensuite, si vous oubliez que initialement vous considériez le haut du sol comme point de base, et qu’ensuite vous mélangez la nouvelle notion de taille avec les ancienne valeur de base, vous avez un sol à l’ancienne place et des arbres qui volent ^^, car eux ont suivit l’élévation incrémentée et le sol non.

En résumé un Sprite contient la notion de taille de l’objet représentée, selon la frame d’animation (ou la seule image par défaut), et le système de rendu sur base d’une hauteur de base donnée, incrémentera de la taille de l’objet rendu, additionné de son élévation Z. Ainsi vous pouvez avoir un arbre sur un arbre comme dans l’image ci-avant. Et pour illustrer l’élévation vous avez les blocs de terre qui montent ou descendent, ce qui est dessus suivra naturellement.

Tout fini bien quand les petits correctifs sont appliqués. On a donc un rendu avec code mieux réparti (le bon endroit), une mécanique d’initialisation et de rendu améliorée, des lignes et procédures inutiles retirées et le sentiment de progresser dans le bon sens.

La suite ?

On a ouvert la porte à la structure d’animation, le système de rendu de Element doit encore être amélioré pour traiter ça complètement, car Element n’a pas encore de gestion des ses animations. Pour cela le code du POC Sonic servira de base car il couvrait pas mal de possibilités variables. Il restera à faire un cas de test. Notez que je sais qu’un logiciel libre existe pour composer lui-même sur base d’image un Sprite, me reste à le trouver et voir s’il rend en sortie également les références de positionnements, ça aiderait grandement.

Sinon, on peut également repartir sur l’intégration des events, qui doivent transiter par Game ou directement aux Scene actives, à voire, et ainsi reprendre le dessin du curseur et le zoom. Reste à savoir où mettre tout ça et comment raccorder.

Nahyan, 18 ans plus tard

Nahyan, ça fait un bout de temps dis donc et en en reparlant avec certains membres de l’équipe originale, ça n’a pas prit une ride (Merci K-you). Ça tombe bien j’ai toujours pas abandonné !

Reprenons la situation

Reprenons quelques instant un mini résumé de la situation : « Seul je ne sais pas faire le jeu tel que pensé et il faut arrêter de croire que je serai aidé ».

Ensuite, dans l’optique « j’y arriverais malgré tout », il faut reprendre ce qu’on a comme acquis permettant de démarrer : je suis développeur web et pas illustrateur. Il faut donc que j’amoindrisse mon ambition, sans casser le projet ou perdre l’envie de le faire, rester motivé et prendre du plaisir à le faire.

Du coup on repars un an en arrière, j’étais développeur web en mission pour Auchan et j’ai développé, entre autre, un éditeur de plan de masse (en gros la vue du haut des meubles d’un étage de magasin). Ceci a été fait sous Angular en Canvas et c’était bien amusant de le faire. Du coup quand c’est sympa on explore et on chipote, on fait ça encore mieux, etc. Ce qui donne ceci :

Vue partielle de l’éditeur

En gros on a comme possibilité de sélectionner, drag/droper d’une liste vers le plan ou du plan vers le plan (déplacer un meuble), molette de souris pour faire une rotation du meuble ou zoom du plan (min-max), visibilité de la sélection dans une liste sur plan ou inversement au survol, etc… Le tout avec les optimisation possible pour ne dessiner que ce qui est nécessaire tout en se faisant plaisir avec les petites ombres par exemple.

Exemple de rendu d’objets : 3 meubles de différents type, une caisse et un escalator

Ici on a un exemple de certains éléments graphiques créés directement en code.

Tant qu’on y est…

J’étais lancé et je ne pouvais pas explorer plus avant les possibilités de cet éditeur, du coup je me suis créé un petit projet perso pour tester les animations en sprite. En fouillant un peu j’ai retrouvé ce bon vieux Sonic.

Ainsi j’ai pu chercher comment découper, afficher, animer, retourner, etc. notre petit héro bleu. Ensuite, me casser les dents sur la physique de déplacement du personnage mais ça on va zapper…

Oui mais ensuite ?

Ensuite la mission s’est terminée et j’ai eu quelques jours au bench. Du coup je me suis occupé en tentant de renforcer mes compétences Angular, surtout sur la partie mise en place d’un nouveau projet. Car c’est facile d’utiliser ce qui a été mis en place par d’autres, mais le faire soit même c’est de la découverte casse-gueule…

Coder c’est joli, mais un rendu c’est mieux, Canvas c’était sympa et comme j’avais déjà fait un POC (preuve de concept) isométrique en canvas à la maison, je me suis dit que mélanger les deux rendrait la chose plus amusant, Angular et Canvas pour un moteur isométrique. Il manquait un truc : les éléments graphiques, et c’est pas le plus simple.

En fouillant je suis tombé sur l’excellent site opengameart.org, fait par un développeur, Clint Bellanger, d’un jeu gratuit et open-source nommé Flare, de lien en lien je suis également tombé sur son blog partageant ses études du sujet isométrique et de son jeu et moteur graphique.

C’est dans la même suite de recherches que je suis tombé sur le magnifique logiciel MagicaVoxel. Ce truc est terrible, pas évident au début mais très agréable. C’est là que je me suis dit :  » tu n’es peut-être pas graphiste/illustrateur, mais t’as les notions et la créativité pour faire de ce logiciel l’instrument qui te manquait « .

Du coup j’ai fait ça :

Un morceau de terrain isométrique

Oui c’est un arbre

Ok je me suis bien amusé, j’ai chipoté et fait un rendu isométrique par le logiciel et puis ?

Ensuite j’ai transposé mes précédents essais et morceaux de code du projet Auchan, Sonic et compagnies en un POC de rendu isométrique. Ce qui donne, après de nombreux chipotages et réflexions sur les calculs de rapports entre écran et monde, monde et écran, en incluant les notions de zoom, d’offset et d’élévation :

Tadaaa… ok ça ressemble à rien mais en une matinée c’était pas mal (une demi journée de recherche à tourner en rond sur le net et un début de rendu). Évidemment ce n’est pas suffisant, il nous faut un objet et un curseur pour contrôler et vérifier les formules de positionnement, de zoom etc.

Et avec un objet pour vérifier la superposition, l’ombrage et l’élévation :

Du coup ça fonctionne, un POC de +650 lignes, données incluses, ça fait plaisir, et on est à 1 jour et demi. Puis en chipotant sur des dessins et en repensant au plan de masse d’Auchan, je me suis dit, qu’est-ce que ça donnerait en isométrique ? Il m’a suffit de dessiner un meuble au l’autre, changer la carte de données, corriger des superpositions et ordonnancement pour avoir ce petit magasin :

Vue isométrique test d’un plan de masse

Ça a fait beaucoup rire les collègues 🙂 .

On s’arrête au POC ?

Bien entendu non, j’ai réussi une étape, c’est bien, je l’avais déjà fait mais là c’est plus abouti. La base est là et vérifiée, on peut continuer ! Cependant c’est un POC tout dans un seul fichier, c’est pas propre et continuer là dedans c’est pas une bonne idée. Du coup, on décompose le code en morceaux, on crée des classes, on structure son code, on découvre les joies des inclusions cycliques, des prises de têtes de  » Où mettre ça ??? « , etc… Il m’aura fallu presque une semaine pour transposer le code et en arriver presque au même point qu’au POC :

Test de rendu d’un terrain variable avec objets

Là on a une version complète et fortement améliorée pour son côté structuré et flexible. Manque le curseur et le zoom qui n’ont pas encore été recâblé pour la simple et bonne raison que je me suis concentré sur le système de rendu, la structuration des données, simplification/factorisation du code et correctifs divers.

En parallèle

À coté de ça, j’ai tenté de m’installer un espace de travail propre, un petit GitLab sur le serveur daaboo, mais quand on a la moitié de RAM que nécessaire… et pas de redmine encore car GitLab pourrait peut-être suffire. Mais on est loin du compte encore. Le top serait d’avoir le moteur graphique séparé de l’usage Nahyan, on lui a trouvé un nom  » TARS « , je vous laisse chercher pourquoi.

La suite ?

Là ça fait une semaine que je m’échine à refondre la structuration Sprite et Element, suite de ce que je disais avant, fondre et refondre pour trouver le bon équilibre de séparation des concepts, stockage des données, etc. J’y suis presque, l’idée est d’avoir un Sprite qui s’occupe de stocker l’image et les infos descriptives de cette image (points de référence, hauteur/largeur, frames d’animations, …) et de l’autre un Element faisant référence à un Sprite tout en ayant ses valeurs propres. Ainsi 2 Elements « brin d’herbe » utilisant le même Sprite pourront être animé séparément et ne pas avoir cet effet ringard de tous les mêmes élément animés en même temps.

Dans cette refonte j’essaye d’inclure directement la notion d’animation et d’objet composé.

Il y a encore pas mal de travail avant d’avoir à nouveau une base propre, stable et bien pensée, même si on sait d’avance que ce n’est pas le dernier refactoring du code de base.

Pour détails du jour

Pour ceux que ça intéresse ou pour le souvenir :

  • Usage de la classe Bloc au lieu de l’interface périmée
  • Suppression de Tile au profit de Element contenant une propriété Z (élévation).
  • Nettoyage et fixes suite à la disparition tragique de Tile
  • La grande question est… (non pas celle là) comment transférer Draw de Scene dans Element vu qu’on a un Camera, un incrément de bloc et des coordonnées d’écran
  • Stacking mis en place au niveau du subset
  • Hero est un set de données x, y en dur
  • Cursor désactivé, mouse event aussi
  • Autre question est comment mélanger Iso et rendu standard au sein d’une scène, mélange de méthode et de services

Pour info il y a 30 TODOs qui traînent dont 19 rien que dans Iso…