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 !

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.