Radiation de pixels, quand la 2d est 3D et vice versa

Suite des articles visant à améliorer l’illumination de la scène, après une couleur diffuse correctement éclairée grâce aux normales et aux spéculaires, il restait, invariablement, que l’image entière était éclairée uniformément concernant l’intensité, pour une même direction de normales.

Mise à plat de la scène pour valider le dégradé de l’intensité.

Les calculs et les informations transmises étaient insuffisantes pour calculer la valeur de chaque pixel et, surtout, rien ne nous permet de savoir où le pixel se situe dans l’espace, nous permettant de faire varier son illumination sur base d’une distance altérée par sa position.

Je ne suis pas 3D

N’oubliez pas que ce que vous voyez n’est pas de la 3D, mais une succession d’images plates représentant « un quelque chose » en 3D, et c’est cet enchevêtrement savamment orchestré qui vous dit que l’image est 3D. Tout n’est qu’illusion Mr. Anderson ! Et votre cerveau vous trompe, et c’est le but !

Je l’ai déjà dit dans les précédents articles, mais c’est là tout le problème, comment donner une information 3D à une image plate ? Certes nous avons les normales pour nous donner la direction, mais qu’en est-il de la position ? Comment puis-je savoir que le haut de mon bloc est une surface au Z équivalant, variant sur X et Y ?

Explication du process depuis le début.

Reprenons depuis le Zébut

Un Tile est l’image représentant une zone, une unité de notre grille. Celle-ci a ses coordonnées X et Y. La distance entre leur position sera de 1 (1x, 1y ou 1x1y).

L’éclairage d’un Tile est soumis à la propagation de la lumière dans le subset de la map courante (cela nous rappelle de vieux souvenir [2018 !]). La propagation reste importante pour savoir quel Tile est impliqué dans l’éclairage (optimisation).

Un subset reprend une partie des éléments du monde concernés par le rendu à l’écran.

On parlera donc de la distance (parcourue ici) entre le Tile que l’on veut éclairer par rapport à la source lumineuse et sa force.

Une lumière peut donc éclairer sur une distance (d) un objet proportionnellement à sa force (S) à cette distance et selon l’intensité (i) de cette lumière. Nous obtenons alors le facteur (f) d’éclairage du pixel (rgb aux coordonnées uv).

f = (1 - (d * (1 / S))) * i

On obtient une valeur (float) allant de 0 à 1, qui sera notre facteur d’éclairage du Tile.

Ça c’était avant

Ce qu’il nous manquait jusque là c’est d’avoir toutes les coordonnées au niveau du shader pour effectuer ce calcul de distance au niveau du pixel.

On calculera ainsi notre distance autrement, c’est-a-dire sur base des coordonnées du Tile (t) et de la lumière (l), sans oublie l’intensité (i).

d = √((l.x - t.x)² + (l.y - t.y)²) * i

Donc on propage, on définit pour chaque Tile concernés les lumières impliquées, on ajoute les informations manquantes jusqu’alors et … ? Ben jusque là rien de nouveau sauf une distance plus correspondante à l’origine mais pas par rapport aux obstacles rencontrés entre la source et l’élément éclairé, car ça, je suis sur que vous les aviez oubliés.

On ne traverse pas un mur, la lumière « contourne » par rebond etc. Donc la propagation reste la bonne manière mais ne donnera pas le bon résultat en isométrique, et j’espère que vous comprenez pourquoi avec toute cette suite d’articles ahah. En bref, on a pas la possibilité de savoir comment la lumière est arrivée à chaque pixel, il faudrait calculer pour chaque objet un pixel de rebond (surement sa normale), balayer ainsi dans tous les sens et à chaque contact : éclairer ce pixel et modifier le rayon pour le faire repartir (couleur, angle, puissance, …), bref on réinventerai un rendu par projection de rayons en 2D sur base d’information 3D stockée dans les textures. Non !

Spatiale 3D dans de la 2D

Ce qu’il nous manque à cette étape est le moyen de faire évoluer notre formule de calcul de la distance pour inclure la position du pixel rendu de notre texture.

Nous avions vu plus haut que le Tile se définit par une position X et Y, ici en sont centre, sur la partie supérieur pour représenter le sol. Nous ne parlerons pas de Z ici mais l’idée sera la même une fois que j’aurais réussi à ramener l’information jusqu’au shader.

Si un Tile est à une distance de 1 de ses voisins alors il a à ses extrémités un delta variable de [-0.5, 0.5] en X et en Y. Z étant, lui, soumis à un facteur d’élévation définit par la scène, ici de 8 pixels, et donc aura une idée différente de sa distance, du moins c’est ainsi que l’élévation avait été définie au tout début.

Nous avions jusque là 3 textures : la diffuse (l’image elle-même), la normale (direction d’éclairage) et le spéculaire (carte de où appliquer le spéculaire). Et bien nous allons en ajouter une nouvelle.

Suivant la même idée que pour la texture de normales, nous réutiliserons notre définition RGB de nos repères : X en rouge, Y en vert et Z en bleu. Pour chaque pixel de l’image nous définirons ainsi sa position relative par une valeur RGB. Ne pas oublier que le centre ne sera pas 0, car nous avons besoin des deltas négatif, ainsi nous commencerons au centre à 128,128,128.

Un programme, un plugin, ou une théorie pour le faire de manière automatique n’existant pas, on va devoir y mettre de l’huile de coude.

Ainsi comme vous pouvez le voir, si on veut s’aider, nous pourrons compter sur les gradient, un bon dégradé bien réfléchi qui sera combiné à une couche supplémentaire. Nous commencerons par le XY et on surcouchera par le Z. Un total de 2h nécessaire avant d’arriver au résultat que voici.

Notre formule de distance peut donc être complétée par cette nouvelle information (pp).

d = √((l.x - (t.x + pp.x))² + (l.y - (t.y + pp.y))²) * i

Ce qui nous donnera sans plus attendre ce premier résultat :

Wow que se passe-t-il ? On a notre dégradé * fête * cependant nous avons un drôle d’aspect sur certain Tile. Notre algo utilise la force (S) pour se propager, mais comme vous l’avez vu, la propagation est plus courte que le segment calculable en ligne droite donc nous arrivons trop court, nous n’avons pas assez s’électionné de Tiles pour notre impact. L’idée serait surement de refaire un calcul sur base de la distance au lieu de calculer le nombre de saut, mais pour le workaround j’ai ajouté +2 à la force (S).

On en profite pour restaurer le calcul du spéculaire maintenant que nous avons de vraies informations et ce foutu éclair blanc au coffre s’en va !

Il y a bien évidemment des effets de bords, comme le fait que la distance parcourue n’étant plus impliquée, si vous fermez la porte, le Tile de l’autre côté sera éclairé comme si vous étiez à côté au lieu d’un faible éclairage (la lumière fait le tour du mur). Les tables clignotent et là je ne sais pas encore pourquoi. C’est plus joli mais moins cohérent. On en revient à l’idée des rebonds, du vecteur qui doit bouger selon l’idée mais rendre chaque Tile cohérent entre eux, là je ne vois pas comment.

J’ai quand même regardé comment ajouter la distance à la formule mais le résultat empirique n’est pas terrible.

Ajouter la distance assombrit la scène

Et en fait si on déplace la lumière selon la distance parcourue dans la même direction, on éloigne alors virtuellement notre Tile voisin. Et ainsi le résultat tiens compte de la distance. Ceci dit l’éclairage en a pris un coup, il y a surement quelque chose à faire avec ça.

lightPos += lightDirection * lights[i].distance;

Pour bien faire il faudra savoir si la distance correspond à un voisin, et ainsi différencier si notre voisin a une distance de 4 ou de 1 et appliquer l’éloignement que dans ce cas. Reste à trouver comment faire.

Et ensuite ?

Comme toujours, quelle est l’étape suivante et comment adresser les nouveaux problèmes, ou ceux mis de côté ? Plus j’avance dans TARS et plus les limites, personnelles et techniques se montrent. Il est grisant de se dire que j’ai relevé un défi qui, selon Google, n’a pas été adressé ou n’est actuellement plus référencé. Il faut aussi se rendre compte de l’effort considérable que cela demanderait si l’on voulait faire un jeu entier ainsi. Et là encore nous n’avons exploré que la lumière (par rapport à l’existant réalisé).

C’est le cœur serré que je me dis que j’ai clôturé cette étape, et qu’il faut, car le temps n’est pas infini, et que je n’ai pas que ce projet à faire, passer à autre chose… Quand je dis « passer à autre chose« , ce n’est certainement pas abandonner Nahyan, certes non ! Mais explorer (enfin) une autre piste que celle de tout faire moi-même, même si je n’ai pas fort le choix. Je ne vais pas en dire plus pour le moment, j’ai déjà commencé à explorer d’autres possibilités et vous raconterais le moment venu.

Cela fait 7 ans que TARS existe, avec une belle pause au milieu, et 2-3 belles phases de développement, comme le passage au WebGL pour adresser les performances sur l’éclairage première version, ou encore la refonte des classes communes pour mieux gérer les données et factoriser le code générique. Et enfin ici, le nouveau système d’éclairage, mais in fine rien de plus, nous n’avons toujours pas vu « Le temple du Prisme« , un HéroQuest ou un Liebesstein.

Qui sait que j’y toucherai par défi encore 😉

Et si on revenait un peu à TARS

Mélange des origines avec la map de test

Petit point de situation suite à la mise à jour Angular 17. J’ai donc repris le POC TARS et sa carte de départ, j’ai nettoyé quelques POCs (CodingPark et le début du système de particules) et … ça ne marche plus :/ ?

Plus d’une fois j’ai eu le malheur d’un craquage complet avec un beau message d’erreur… et là je valide le fait que le message d’erreur est effectivement amélioré. Alors merci chère mémoire de ne pas m’aider à me rappeler pourquoi j’ai coder ça ainsi et merci les 4 ans passés à évoluer et donc ne plus écrire ou penser de la même manière. Ceci dit j’étais quand même dans le pétrin.

Je dirais principalement que le fait de ne plus compter sur les effets de bords du typage aide pas mal à avoir des soucis, du coup merci le mode stricte ^^. Puis surtout, en terme de documentation c’est tellement plus confortable.

C’est là aussi que l’on voit que l’on code en « happy path » et sans gestion rigoureuse d’erreur sur projet perso. Le « je sais ce que je fais car je l’ai fait » c’est bien, mais 4 ans plus tard… comment dire, ça laisse à désirer. Du coup « oui », TARS a des lacunes. J’ai tenté de dire que le sol était un sol et il n’aimait pas, imaginez ma surprise… et il fallait lui dire que c’était une « image »… logique ! Et pourquoi ? Du fait que la description de l’élément ne répondait pas au standard ISO(métrique) demandé, mais sans gestion d’erreur ça explose ailleurs et tu peux chercher longtemps pourquoi.

Comme le fait de cibler un élément d’un tableau sur son index, de modifier le tableau et d’avoir une erreur d’accès à une méthode sur une instance… tellement évident… heureusement ça c’est le côté POC pour générer la map et tester le moteur, l’idée étant de passer par l’éditeur ensuite qui lui ne fera pas d’erreur. Mais son opérateur, ça…

Ah et en passant, tant qu’à faire de l’amélioration générale, n’oubliez pas que A || B n’est pas pareil que A ?? B (petit lien sympa). Et que les ternaires A ? A : B peuvent devenir A ?? B.

Mais aussi que les pipes RxJs peuvent s’agrémenter de code perso et ainsi pouvoir charger un Object { a: 1, b: 2 } vers un tableau [{ name: a, value: 1 }, {...}].

Ou encore qu’on ne peut pas retourner la déclaration d’un enum pour accéder à ses clefs (return SO0), mais qu’il faudra retourner ses clefs directement (return Object.values(SO0)).

Enfin voilà, en gros, tout est une bonne grosse question de type et de ce que l’on peut faire avec. Il y aurait encore à en dire, mais au final je n’ai pas retenu ces directions, et sans les notes il sera dur de vous en montrer plus.

Il y a bien sûr encore pas mal de boulot, je vais repasser progressivement, à temps perdu, dans l’ensemble du POC et de TARS lui-même, faire un coup de clean et de typage massif, gestion d’erreur(s), et surement de refactorisation plus ou moins importante, par exemple la suppression du mode 2D, ce qui aiderait quelques petite performances; mais aussi le fait rencontré que le validateur de chemin n’ait pas l’information disponible de la différence de hauteur/niveau/élévation entre 2 Coords en comparaison, et qu’il sera difficile en l’état d’y arriver. Du coup, on reparle de théorie du système, doit-il gérer Z ou pas, gros débat (ombre portée dynamique, lumière, changement de niveau (hauteur), …).

Quand on pense qu’au départ de ce moment je voulais juste reprendre le dev de l’éditeur…

Y replonger, bien que difficile, chronophage et casse-gueule avec le temps et la mentalité différente aujourd’hui, est amusant, un petit challenge sur le côté, et surtout : il fonctionne, pour ce qu’il est, ses limites et possibilités.

Je me tâte toujours, est-ce que Nahyan peut être exploré en isométrique ou doit-on absolument le voir en 3D ?

Angular : 4 ans, 9 versions

Début 2019 on a Angular 8, et en cette fin 2023 on a reçus la 17, soit 9 versions ! Et pourquoi je vous dis ça ? Tout simplement car j’ai voulu mettre à jour le POC de TARS, et ça ne s’est pas du tout passé comme prévu malgré l’usage du site de migration qui est bien fait.

Le truc c’est que ce n’est pas exempt de soucis qui se pose au fil du temps. Par exemple, certaines commandes vont exécuter l’install (npm i), mais il vous faudra reprendre à la main en précisant le flag --legacy-peer-devs ou encore des résolutions d’arbre de dépendances qui ne se font plus correctement.

J’ai réussi non sans mal à passer à la version 11 mais pas plus loin, la quantité de conflits ou de soucis a explosé, pourtant le projet n’a rien de particulier d’un point de vue Angular.

Du coup j’ai opté pour une autre approche. Le projet n’utilise Angular que comme une coquille de lancement avec l’usage de l’injecteur et du client HTTP. J’ai donc créé un projet frais et j’ai replacé le code en dedans et là : BOOM ! Fallait pas oublier qu’on était passé en mode stricte, ou que le côté standalone a changé la manière de monter un module ou de gérer les composants, bref il ne s’en sortait plus.

On sort l’huile de coude, on repasse partout, on s’arrache les cheveux sur le typage fort qui me manquait à l’époque et on solutionne avec quelques generic. On ajuste la nouvelle mouture standalone avec les imports et providers locaux et ça tourne ! Enfin, oui, mais ça nécessitera quelques heures avant que le builder arrête de péter une durite. À savoir que si vous avez un couac il peut perdre la compréhension de tout les tags Angular et votre erreur c’est 1 détails quelques part… Bonne chance.

J’en profite pour vous filer un tuyaux : penser à définir vos composant en standalone, rajouter ce qu’il vous faut localement comme CommonModule ou HttpClientModule.

Un truc qui continue de m’agacer, c’est ce double état undefined et null, car si vous spécifiez un attribut de classe comme optionnel vous utiliserez un ? à sa fin, mais si vous l’assignez suite à un find d’un tableau par exemple, ce qui peut vous rendre votre type ou null, vous aurez un conflit possible de type, sauf si on parle de test(s) avant assignation ou autre écriture à rallonge; ça manque quand même de cohérence.

Enfin soit, c’était amusant d’y retoucher, avec l’objectif de reprendre son éditeur en main, toujours pour ce projet éternel qu’est Nahyan.

Angular 17 recèle d’un tas de nouveautés qui, je pense, pourraient, avoir leur rôle à jouer, voire en refactorisation, mais je m’avance un peu. Par exemple Signal, la sortie de Zone, la nouvelle syntaxe de templating, etc.

Masque splendide

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

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

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

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

1 problème ?

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

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

3 problèmes ?

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

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

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

Raycasting ? 4 problèmes donc ?!

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

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

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

Fin ! 8D

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

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

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

Un système de masque

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

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

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

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

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

Modification du système de rendu

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

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

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

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

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

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

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

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

Inversons le rendu et affichons un fond à la transparence.

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

Résultat corrigé du rendu.

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

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

Et le raycasting ?

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

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

Prochaines étapes

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

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 !

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 🙂