Thursday, November 23, 2023

Making swim fun

Somewhere last year, I had been reading something about water levels in platformers and whether it might be possible for them to be actually fun. Because, well, I don't think I'll manage to copy the awe DKC sharks may produce and I can't ask my brother to come up with something like David Wise's Aquatic Ambience for my Nintendo DS title.Yet there will be water.

There have been some platformer titles over the last years that came with fun-to-play water levels. I think about "20000 lums undersea" level in Rayman Legends and some level of DKC: Tropical Freeze. Swimming in Ori and the Will of the Wisps was pretty pleasing as well. All these games share something: they benefit from analog stick and they depart completely from their 8-bit and 16-bit counterparts by letting you target any direction freely. Your character will typically need some time to turn himself towards the direction you want though, which works fairly well.

Les niveaux aquatiques dans les platformers, ça a mauvaise réputation. Vu le nombre d'échecs que Vanilla Lake Forest of Illusion 2 m'a infligé, je ne peux pas franchement leur donner tort. ça a commencé à aller un peu mieux avec Donkey Kong Country, d'abord parce que la musique de David Wise était aussi époustouflante que l'animation des requins, mais surtout grâce à Enguarde qui permet de cesser de se battre continuellement avec la gravité... Et d'avoir une chance contre la poiscaille.

Plus proche de nous, Tropical Freeze et 20000 Lums sous la mer l'Océan des Songes de Rayman Origins nous ont donné un nouveau mode de fonctionnement d'un personnage de jeu de plate-forme qui tombe dans l'eau. Ils sont maintenant capables de se diriger dans n'importe quelle direction (indiquée par le stick analogique), accélèrent dans la direction correspondante si on appuie sur un bouton. C'est souple. On voit son perso se tortiller pour faire un demi-tour ce qui donne l'impression d'être dans l'eau ... On peut chercher une certaine forme d'élégance dans les trajectoires qu'on prend ... d'une certaine façon, ça se contrôle un peu comme un jeu de micromachines avec beaucoup de dérapages. Ou un avion qui fait des loopings.

Dans chacun de ces cas, exit la "blind box fonctionelle"  d'Enguarde: on peut attaquer dans n'importe quelle direction, à n'importe quel moment.

But the part I prefer is how you get a speed boost into the direction of choice with the JUMP button, and especially how you jump out of water in Ori and the Will of the Wisp like you were a true dolphino. I could certainly do something alike in Bilou Dreamlands. (at first, I wanted to make it an unlockable move so that you could discover secrets later on when you've unlocked some ability ... but that's something for Bilou's Adventure instead).

Of course, with the NDS DPAD, I can't truly have free aiming like in the switch/wiiu titles, but maybe I can find something approaching:

  • when you hit FOOT, you get a speed boost in the one-of-eight direction you're aiming with the DPAD.
  • during that move, you can modulate your direction (say, +/-15°) around that main direction with the DPAD
  • the swim animation eventually comes to a slow down step where you can chose a new main direction and hit FOOT again to keep speeding.

Une des choses que j'ai préférées dans ces jeux plus modernes, c'est la manière dont notre personnage peut jaillir hors de l'eau si on fait une "attaque" près de la surface. J'avoue qu'au départ je pensais utiliser quelque-chose inspiré de la physique de nage de Fury of the Furries, où on ne sait sortir de l'eau que s'il y a une berge suffisament "à niveau". Puis offrir le "mode dauphin" avec un level-up, comme le fait Ori and the Will of the Wisps. Mais bon, l'objectif ici, c'est un "dreamland", pas un castlevania. Pouvoir bondir hors de l'eau dès le début du jeu, pour le fun, c'est l'objectif. Première chose à tenter, donc: permettre à Bilou de jaillir de l'eau si on tente de sauter près de la surface.

Deuxio, prévoir un dash-swim qui propulse Bilou dans une direction indiquée par le DPAD si je tente de sauter dans l'eau. ça pourrait être intéressant d'en profiter pour essayer de faire l'équivalent aquatique d'un wall-jump parce que j'ai toujours trouvé plus simple de se propulser en utilisant le bord de la piscine que de nager à proprement parler.

Tertio, le dash sera normalement suivi par une période de "retour au repos" pendant laquelle le bouton saut n'aura aucun effet, mais ça pourrait être intéressant de prévoir d'enchaîner sur un dash si on utilise plutôt le bouton "ramasser". On nagerait alors à la vitesse maximale en enchainant pied - main - pied - main avec le bon tempo... Pas d'attaque aquatique prévue pour l'instant, mais je n'ai pas non plus un bestiaire aquatique débordant ... 

Quarto(?): permettre d'infléchir la trajectoire vers le haut ou le bas avec des petits coups de DPAD pendant un dash horizontal. Et vice versa ... et diagonalement. Ce sera l'équivalent à la croix directionelle des mouvements libres au stick analogique.

One other fun thing that might be worth experimenting is doing the equivalent of wall-bounce underwater. At least, it might be the easiest way to get a first experience with wall jumps in my engine.

And finally, one possibly terrible (or fun) idea would be to keep moving fast underwater with properly timed FOOT / HAND / FOOT / HAND button pressing
 

Saturday, November 18, 2023

tile 4 { swim.flow = (256,0) }


Enfin! Je m'attaque enfin à la dernière face de mon 'newmeta/newmap': les tiles spéciaux. Il y avait déjà les blocs spéciaux, auxquels on peut attacher des zones de collision et des actions. Il y a les pentes qui se passent de commentaires et les tiles aux propriétés "directes" qui permettent de combiner jusqu'à 6 propriété distincte (eau, sol, air, lianes, ...) librement. 

Dans le cas des "nouveaux" tiles, le script définissant le jeu va pouvoir encoder librement les propriétés voulues pour chaque type de tile. J'aime y penser comme à un accès indirect, mais c'est probablement parce que j'ai fait trop d'assembleur :-P. Et ça ne s'arrête pas là: je veux les utiliser pour les tapis roulants, le sol qui glisse, et ce genre de choses. Chaque type permet donc aussi d'aller paramétrer des valeurs le concernant auprès des différents contrôleurs.

En pratique, on va définir dans un fichier script.gam

tile 8 {
    is flowdn "0055220000552200"
    props fc8 # WATER
    swim.flow = (0,512)
}

La première commande dans le block, is est utilisée dans l'éditeur de niveau, pour fixer le graphisme représentant le type de tile. La commande props donne les fameuses propriétés indirectes. Ces deux-là existent aussi pour les blocs interactifs. La dernière sera passée à une nouvelle fonction SwimControllerFactory::setTileVariable() qui s'occupera de tout ce qu'il y a derrière le signe = et enregistrera le "mouvement forcé" à appliquer dans GobSwimController::think() quand on se trouve sur ce genre de tile.

Hello. I've been writing notes about how to have conveyers and flow-in-water since at least 2020 2019. Now is the time to get it done. At last. The syntax of the new "tile" description will look as much as possible to that of the "block" description, used for collectibles and more. Like them, it can describe the value of cando() flags to be used by controllers with a props statement.

What only they can do is set values for 'controllers variable'. Say that we want water to be able to push Bilou in some direction while he's swimming, we need a table of swim::flowx and swim::flowy that indicate how much Bilou should be moved at each frame if he stays on the given tile.

After some refactoring of the iWorld class and some more code in the script parser, this is finally possible. And with some bugfixes on the level editor, I've got a test case coded for the "three rooms" demo where Bilou is moved away from the waterfall because tiles say so.

Quelque jours plus tard, j'ai enfin ajouté le code qu'il faut au contrôleur utilisé pour la nage, téléchargé une nouvelle map avec des tiles "pousseurs" sous la cascade, et voilà: une première mise en application du concept où Bilou, tel un Fury, se fait embarquer vers le fond, puis un poil sur le côté avant d'être recraché vers le haut par les remous...

Friday, November 10, 2023

Neocities

Pendant des années, j'ai eu un site présentant mon projet d'OS et mon activité sur la démoscène... et quelques-uns des jeux qu'on avait réalisés avec des amis. Le point commun entre eux, c'était le nom de l'équipe: "PPP Team (Software)". Il a migré de mon compte étudiant à mon compte de chercheur, puis il a fini par disparaître corps <body> z'et bien quand j'ai quitté l'unif.

Dommage parce qu'il n'y a plus rien pour parler de notre passage sur la Inscene ou des concepts originels du Clicker32 sans lui.

Mais entre deux tweet, j'entends parler d'un nouveau service d'hébergement orienté "bon vieil HTML", sans lourderie en php: neocities. Vu que mon premier hébergeur était geocities, ça fait mouche, bien sûr.

Je transfère donc mes derniers backups sur  https://pppteam.neocities.org/ pour voir ce que ça donne ...

Somebody on twitter mentioned neocities ... Since I have old (pre-php) web contents that has turned unavailable lately, I decided to re-upload it on brand new https://pppteam.neocities.org/ ... So you can visit my old demoscene archives and operating system development manifesto and I don't have to wonder how I could convert that into wordpress

bin ça donne plutôt sympa ^_^

Sunday, October 22, 2023

done transition on looping animation

I've been crawling on the dark side of hobby game development lately. Dark because it mostly involved too much hex numbers and too little pixels to feed enjoyment while doing it. Dark because it implied admitting that I have not been paying enough attention to things like "whether unit test still pass" and "whether it might be the right time to merge that into default". Things that make the difference between coding and coding as a job. Now I have to repay that attention debt, right after the buggy-tool debt and even before I can look at that memory leakage debt. Given how long it took, how little motivation I had to do it more than 1h per day, I wonder whether I shouldn't have opted for something less useful but more enjoyable first...

Anyway, I went on with cherry-picking bits of the 'newmap' branch that ought to have been on independent branches and merged into default long ago until I finally identified the fix that stabilizes on what pages animations are actually loaded.

Y'a les jours où le développement de jeux, c'est fun. Puis il y a les jours où le programme supposé anticiper les défauts du moteur de jeu vous crache de l'hexadécimal à la figure. Tout ça parce que j'avais espéré pouvoir remonter dans le temps avant la décision "allez, on va faire évoluer le moteur de jeu pour qu'il puisse supporter plus de pentes, plus de types de sol et encore plus de folies!" ... parce que je n'avais pas pris la peine de vérifier que la modification juste avant ("allez, on va autoriser la fuuusiooon de fichiers graphiques au moment du chargement pour permettre d'avoir un monde 2") ne cassait rien de ces fameux tests automatiques.

Et encore: l'hexadécimal, il a presque fallu aller le chercher dans les boyaux de la bête qui se contentait sinon de nous roter un "transition sur fin d'animation en boucle". Eh oui. Et j'avais beau reprendre les fichiers dans mon éditeur, vérifier à coup de MD5 que j'utilisais les bons, vérifier l'état du GobTransition dans le débuggeur, rien ne parvenait à expliquer comment l'animation d'inkjet se retrouvait tout à coup avec une boucle.

I have small scribbled characters and notes about it all over my yearly notebook, but I thought it might be worth another "big picture" sketch page to summarize all the things I had forgotten about my own code and re-discovered while working on this specific bug.

Like how you can load two files in the sprdo helper tool to see which animation #543 truly is ... I should have remembered that earlier on. As soon as I noticed that the the commands I could see in the debugger were not matching what they should (they were ending with a 'LOOP' command, just to start with. I tried comparing them with the line above, the room above, but I should really have just compared them with the next page ^^". Then I've been distracted by that "meds+4" extra token on some files, but it was just a hint for Level Editor. I guess that would have been obvious if I had refreshed memories of "world 2" by reading my own blog posts again. But for some reason, I only thought about that tag now, when blogging about how I eventually figured out that I was missing some fix written long ago, but not merged on "default" ^^".

Au final, ce que j'aurais du faire, c'est comparer cette animation incompréhensible à celle se trouvant au même emplacement, mais sur la page *suivante* dans mon fichier school.spr ... parce qu'il me manquait sur la branche que je cherchais à corriger un malheureux patch corrigeant un décalage d'une position au moment d'enregistrer les animations du fichier supplémentaire à côté du fichier de base :P

Saturday, September 30, 2023

Un p'tit coup de marteau ...

Mes dernières sessions avec AnimEDS s'étaient toutes soldées par des "guru meditation". Peu de travail perdu, heureusement, le problème se produisant généralement soit juste au début, soit juste après une sauvegarde. L'impact sur ma motivation à continuer à animer mes p'tits persos en prenait quand-même chaque fois un coup: j'aurais pu effectivement perdre un travail précieux.

Alors j'ai noté de faire du debugging de tout ça. Il y a un bail. D'abord refaire une version précise de l'animateur (rH2021) et garder le .nds et son .elf à côté des fichiers re-générés à tout bout de champ quand je bricole du homebrew, histoire que quand le problème se présente console en main, je puisse effectivement utiliser la valeur du registre PC pour retrouver un numéro de ligne dans le code. C'est le cas depuis Juin.

I can't help wondering whether this is worth translating. It's another epic (?) showdown between me and my code to see who's got the StrongARM. But well, it has been bugging me since January, crashing the animation editor almost every time I used it. I'm just lucky I never lost anything important but motivation to work on some cute things over the evening. The "guru meditation" screens I got during the first half of the year were almost useless: the AnimEDS build on my 'lime' DS was so old I did not have the matching .ELF file for my debugger anymore. Believe it or not, the RealLife (tm) has turned so intense that I even had to write down an agenda check list with "rebuild ; keep .elf apart ; upload .nds to lime" to actually get it done.

Pas de chance: c'est un de ces bugs où l'adresse ne dit pas grand-chose parce qu'on est a suivi des pointeurs de fonctions qui ne voulaient rien dire. Mais! Bonne nouvelle, avec les nouvelles animations pour l'Appleman, le bug est plus facile à reproduire: il suffit d'essayer de copier une frame d'animation juste après avoir chargé la première animation du fichier dans l'éditeur.

Alors j'ai ressorti mon émulateur et mon débuggeur et là, bonne nouvelle, ça foire aussi dans l'émulateur. Différemment, mais ça foire, donc c'est débuggable. Et là, j'ai galéré pendant des heures ... Des structures qui ne veulent rien dire, des vptr complètement à l'ouest ... Il faut dire que pour encoder la ligne du temps utilisée par l'éditeur, j'ai pris une std::list de std::pair de classe dérivées. Bref, on se perd sous les couches de templates, de membres dépendant de l'implémentation et d'optimisation du compilateur qui prend un malin plaisir à rendre inaccessible les variables dont on aurait besoin.

With that done, I wasn't much more lucky. It's one of those bugs where you try doing a virtual function call on something that isn't truly an object and thus end up in the middle of nowhere, especially where memory contents doesn't match any valid opcode. Hopefully, while trying to get the crash to write down registers values, I realised that the bug was actually easy to reproduce (just open one animation and try to copy the frame before selecting any frame) and a bit later, that it also happened with the same file in my emulator. At least I could save the evening where I navigate DS memory to reconstruct objects on paper this time.

J'allais jeter le gant puis un soir j'ai noté dans mon calepin "fait du débugging indirect en surveillant les constructeurs". Bah oui: la variable aura beau avoir été optimisée, il faut bien qu'il soit construit à un moment où à un autre, le TIFrame qui explique pourquoi j'ai du n'importe quoi dans les registres au moment de copier cette première frame.

Sauf que ... non. Les "TIFrames" sont construites vides, puis au fur et à mesure que l'AnimationParser traite la liste de commandes destinées à GEDS -- le moteur de jeu -- il complète les coordonnées, les numéros d'images, etc. J'étais sur le point de laisser tomber (traduire: réécrire tout avec ma propre classe 'liste' plus propice au debugging) quand je réalise que je peux "figer" l'afficher d'une ou de TIFrame dont je capture l'adresse à la construction, et suivre leur évolution jusqu'au bug (l'animation en question ne contient en réalité qu'une frame et une commande de contrôle). Et là, surprise, tout va très bien (Mme la Marquise :)

 It did not save navigating memory altogether though. The way gdb handles std::list and std::pair combined to the amount of variables that were actually "optimized away" when they would be critical to have around still turned that debugging session into some guru meditation. I first thought that it was because of some incoherent animation instructions into the animation itself, but inspecting how the animationParser processed them shown everything should be fine. Yet, if I tried to see what frame was triggering the bug, all I'd get was garbage non-sense. I was about to replace all that std::* by some pype::* when I realised that I could just break on constructors of the contained TimeItems to see what the list contained.

That did not work either, unfortunately, because when TimeItems are added to the list, they are "blank" items that will be modified by the yet-to-come UPDATE animation commands. What did help, was creating some static watches at addresses discovered in a constructor-breakpoints so that I could look at the state of my list just before the offending call. That and realising that the list was just fine, thanks, but that I was using an iterator that might not have been updated since the previous list had been trashed and replaced by the new one :P

Une seule explication restante: c'est l'itérateur qui devient invalide au bout d'un moment. Je sors mon cahier A4 histoire de me faire une map UML de tous les bouts de code impliqués et c'est bien ça: quand on choisit une animation à éditer, la liste est vidée, mais l'itérateur reste inchangé, ce qui est invalide.

Ah oui. Vous vous demandez "pourquoi le marteau" ... et vous n'avez pas Thor. C'est à cause de cette blague d'ingénieur où un consultant rentre une facture de $100000 pour une intervention et ça rouspète chez le client parce que "vous avez juste donné un coup de marteau!". Et le consultant de reconnaître qu'il a fait une erreur et de préparer la facture suivante

  • 1 hammer hit: $10
  • knowing where to hit $99990

(bon, c'était vraiment une grosse machine très chère qu'il fallait dépanner). Bin c'est un peu la sensation que ça me fait sur ce bug-ci. Sauf que je ne vais pas recevoir des cents et des mille et que je ne devrai pas les débourser non plus :P

 

Wednesday, September 20, 2023

Tiny Thor, at last!

I have records of tracking Joe Manaco's "Tiny Thor" title for as long as January 2021. With gorgeous art from Henk Neiborg and brilliant soundtrack by Chris Huelsbeck, the game's author even revised his project to match the level of those invited developers. I've been waiting for it to be released on the Nintendo Switch, and yeah! Huzzah! it did!

Most of the game is amazing. Gorgeous art, (very) challenging levels and smooth action. I hope I will manage to get far enough in the game, but it is already pointing out that my skills are not on par with those of the intended audience. Holding a trigger to freeze while aiming a shot in a platformer is not a bad idea, but I struggle to explain that to my fingers ^^". As a result, I end up in spikes or with my hammer flying the wrong direction more often than I should. Note that I had the same issue with that-space-pirate-game

Rho, ce que je l'ai attendu, ce jeu! Rien que savoir qu'il y avait Henk Nieborg aux pixels ... D'autant que contrairement à d'autres projets esthétiquement attractifs (comme CyberShadow ou Flynn, son of Crismon), ici on est sur un mode de jeu qui fait véritablement partie de mes styles de prédilection au niveau du gameplay. Mais le voilà! il est sorti sur Switch et il a été rejoint par Chris Hülsbeck aux platines.

Niveau game design, c'est bien rôdé. Niveau difficulté, c'est une autre histoire. Non pas que le jeu soit injouable ou infogramesque, mais le peu d'habileté dont je dispose est mis à rude épreuve. Une des opérations de base est de maintenir une gachette enfoncée pendant que le stick directionnel indique la trajectoire que Mjolnir le marteau devra suivre une fois lancé ... manipulation qui m'avait déjà un peu donné de fil à retordre dans FlintHook et que j'ai du mal à faire entrer dans mes doigts.

Levels in Tiny Thor are quite long, as I'm approaching the second boss. It took me no less than 30 minutes for the last level I played (it took about 10 minutes to a fairly trained player). Should I leave the game on pause at one of the numerous checkpoints in the game, I will take the risk that one of my kids pick the controller to play a game of Kirby in the Lost World and zap my progress. That definitely reminds me of the level design of Donkey Kong Returns and Giana Sisters: Twisted Dreams

Spiders... One of my favourite monsters

The game also share one major skill barrier with those two other platformer: when you respawn, you start with just one hit point. Should you hit anything while you're trying to reach the location where you died, you'll die again. There are extra hitpoints to be found, and a fairly nice gameplay mechanic where you can recover your lost hitpoint à la Sonic, except it has a variable time à la Yoshi Island. That means one obstacle you could have passed in a few attempts will now take you over 10 tries just because you're super-fragile as soon as your second attempt, unless Joe granted you a health pick-up just next to your respawn point (which hopefully happens from time to time).

Une fois que cette manoeuvre est devenue un peu moins laborieuse, on arrive dans des niveaux dont la longueur elle-même devient un challenge. Pas loin de 30 minutes dans le niveau contenant le 1er boss (qu'un joueur un peu mieux entrainé atteint en 10 minutes). Il y a un bon nombre de checkpoints dans un niveau mais je ne peux m'y "arrêter" qu'à condition que mes enfants ne cherchent pas à utiliser la switch pendant l'après-midi ^^". Au moins, la console (contrairement à une PlayStation) est prévue pour des veilles prolongées, mais ça a tout de même un petit goût de Giana Sisters: Twisted Dreams. D'autant que, comme dans ce dernier, si j'échoue entre deux checkpoints, je reviens extrêmement vulnérable pour réessayer le passage délicat. J'entends par là "un-coup-t'es-mort". Comme dans Super Mario Bros direz vous. Oui, sauf qu'on voit rarement autant de pics et d'ennemis en vrac dans SMB qu'on en voit dans Tiny Thor. Et de toutes façons, SMB1 est toujours trop dur pour moi :P

Il y a tout de même moyen de ramasser un coeur-pour-un-coup-en-plus. On en a même de temps en temps un dans un bloc à côté du checkpoint. Là je pourrai encaisser un coup sans défaillir. Le coeur s'enfuit alors en rebondissant un peu comme un Yoshi dans SMW, et j'ai un temps limité pour le récupérer. Une mécanique que l'auteur avoue avoir emprunté à Yoshi's Island, les pleurs de bébé en moins. ça m'aura certainement sauvé la mise à pas mal de reprises, récupérant un coeur au bout de quelques secondes, puis me faisant à nouveau toucher un peu plus tard, le coeur se sauvant à nouveau mais me laissant cette fois moins de temps pour le récupérer jusqu'à ce que je tombe sur un bloc-coeur où je pourrai rajouter des secondes à ce précieux compteur. Mais une petite noyade, et toute cette jolie chaîne s'interrompt: on repart aussi fragile qu'un Rick Dangerous depuis le dernier point de sauvegarde. Les vies sont infinies, et ce n'est pas un hasard.

You sure have guessed now: the game is hard. It doesn't feature any 1-UP or lives counter and there's a reason for that. The developer was aware of that and provides a menu with a set of  "game genie"  options, like how much you stick to walls, how high you jump (yes, truly), whether your heart bounces much or not when you lose it, etc. A good idea, well executed, but if you ask me, it lacks one critical option: to swim out to safety. I'm glad I did this in School Rush, despite it might not be perfect and might not completely save "your" lives, but given the number of times I've drown that Tiny Thor, I now know it was good.

Just for the record, I re-played levels 1-2, 2-2 and 3-2 using the in-game time counter to measure how long I spent between two check points. The duration is comparable to the average 1:30 minutes per School rush *level*. Yes, the question of whether the game should have offered mid-level checkpoints is still itching me.

Friday, September 08, 2023

operator sockaddr*()

J'ai passé un peu de temps cet été dans du code third-party qui avait un autre dialecte c++ que moi. En particulier, ils aimaient bien faire des objets-wrappers métamorphes. Prenons par exemple une adresse réseau. C'est pénible: on peut l'avoir sous forme ASCII ou dans un entier 32-bit (ou un gros blob si on est en IPv6). Et de toutes façons, pour s'en servir il faudra qu'elle soit emballée dans un sockaddr, seule connue de l'API socket, qui reprend un identifiant de 'famille de protocole' et un numéro de port.

Tout ça semble justifier une classe NetAddress qui aurait des constructeurs NetAddress(std::string fromUserInterface) et NetAddress(uint32_t fromProtocolMessage). Très bien. On peut aussi en faire en réalité un wrapper de `struct sockaddr_storage`, et remplir les différents champs au moment de l'appel. Avec éventuellement un NetAddress.setPort() pour faire bonne mesure.

Mais ce n'est pas ça qui va changer le fait que connect() et bind() travaillent exclusivement avec des sockaddr. Là où j'ai été surpris, c'est qu'au lieu d'un NetAddress.getSockAddr(), j'ai eu droit à

operator sockaddr*() {
    return reinterpret_cast<const sockaddr*>(&address);
}

Séduisant a priori. Elégant, même. Mais à l'usage (et surtout à la lecture), ça s'est avéré être un fiasco. Je tombais sur du code du genre connect(toServer, myServerAddress, options) suffisament loin de la définition de myServerAddress et je zappais qu'il ne s'agissait pas d'un sockaddr classique, mais de l'objet emballant. Particulièrement piégeux quand on essaie de découvrir d'où provient l'exception InvalidArgument que rien dans le code de connect ne semble pouvoir générer.

Et ç'aurait pu être encore pire si myServerAddress avait été un pointeur vers une NetAddress ... Là, on aurait dû écrire connect(toServer, *myServerAddress, options), parce que c'est un NetAddress-même que le compilateur a appris à "traduire" en sockaddr* à l'aide de l'opérateur. Pas un pointeur de NetAddress. Perturbant au possible quand on se souvient qu'en C on aurait écrit connect(toServer, &server_address, opts);

...

Sinon, vous, l'été, ça a été ?

Saturday, September 02, 2023

Le dessus des arbres

Je commence à avoir une assez imposante collection d'arbres à analyser dans mes #pixelstudy, et il faudra probablement que j'ai quelque-chose de satisfaisant à dessiner pour la partie supérieure des arbres dans Bilou Dream Land... analysons donc.

Et commençons par un mockup de SnakePixel (owlboy) qui nous réinterprète pour "16-bits" l'Amazonie de Duck Tales. Avec des branches où les feuilles sont distinguées les unes des autres et une palette assez généreuse de 6 à 7 tons de verts pour chaque plan. L'arbre à un look "tileset" avec ce bloc de 64x56 (encadré) qui répété sur une même horizontale. Un autre bloc 64x? est utilisé pour les lignes au-dessus. Un bloc de 128x64 est utilisé pour la jonction avec le tronc. Je ne pousse pas plus loin, parce que plusieurs pixels remarqués ça et là me laissent croire que l'auteur n'a jamais essayé que ça tienne *vraiment* dans les 64K de VRAM de SuperNES.

One part of the current 'green zone' tileset displease me, and that is the top-of-tree tiles. I collected a few more reference pictures of trees (possibly with the canopy part visible) and since I'm sick this week-end, it seems like a good time to finally pixel-study them. Let's start with SnakePixel's mock up of Duck Tales Amazonia level, with a generous 6-7 tones of green per tile, and palette variation depending on the plane we're seeing. His trees canopy on the foreground are mostly made of 64xalmost-64 blocks repeated horizontally, but with another kind of blocks as you move up or down in the tree. Leaves are cleanly separated from each otther, although not outlined. I spot a dedicated 128x64 area for the leaves-to-trunk transition, but also lots of little details at the edges of 'common' blocks that suggest the author was more drawing inspiration from 16-bit limitation than really trying to work within them.

Un autre arbre qui m'avait tapé dans l'oeil, c'est celui d'Alwa's Legacy, dessiné par Vierbit pour le jeu de MikaelForslind. Le style est assez particulier, surtout pour la partie la plus haute de l'arbre (qui arrive assez peu souvent à l'écran, comme on peut le voir dans mon screenshot ^^".Il y a pas mal de zones "plates" de couleur unie bordée par des feuilles très larges, au design simple mais nettement détaillées. Pas de tentative de coller à un tileset réduit, mais on note clairement des réutilisations de certains motifs, soit tels quels, soit retournés, soit avec une autre teinte.

Let's move to another tree. One that really did it into a game (and which actually made me buy the game :P) : the big garden tree of Alwa's Legacy drawn by Vierbit. I've been lucky enough that the game author Mikael Forslind offered me a copy of the original in-game trees layer for detailed study. A true gem, since I feel like this style of tree would be perfectly capable of hosting some actual gameplay, with some leaves-platforms, but also some vine-ish things to hang on. At first I was surprised that it was even possible: we seem to see the tree mostly 'from below' here, except that we also see it up to the top, which seems incoherent. Except if the tree's canopy is not pointing at the zenith, but is bending towards the rear of the scene.

Et très sympatiquement, Mikael m'a envoyé le layer tel qu'il est utilisé dans le jeu, avec les deux arbres entiers et les rivières qui les séparent. On remarque que sur le haut de l'arbre, on a des feuilles plus ou moins nettes selon qu'elles sont à l'avant où à l'arrière, pouvant aller jusqu'à de simples silhouettes de feuillages superposées.
Autre élément remarquable avec cet arbre, c'est la quantité de "feuillage intérieur" qu'il nous dévoile. Vous savez, ces zones sombres ou seule une courbe de feuilles apparaît ça et là dans les branchages.

On a more macroscopic scale, it is surprising to see that mix of highly-detailed, very large leaves (with the brightest color) while some other part of the tree just use a flat color with some leave-shapes outline. Interesting to see that, while the tree has obviously not been made to fit a tiled grid, Vierbit clearly did reuse some bunch of leaves at different places, sometimes flipping them, sometimes applying a color swap.

The inner, dark area has only silhouetto of leaves, with the exception of hanging vine-styles ropes of leaves. That makes composition of the structure more free, with the extra bonus that we usually don't have to show branches to far away from the trunk because they eventually disappear into dark leaves before reaching any place where we'd have to show them too thin or mixing realistically with detailed leaves.

Les feuilles n'y sont qu'esquissées, sauf dans le cas des guirlandes de lianes qui dégoulinent des branches, .. et qui m'inspirent pas mal au niveau gameplay si je parviens à ajouter les mécaniques qui m'intéressent dans le nouveau jeu.

Les branches, aussi peuvent se permettre d'aller un peu dans le sens qu'on veut, et elles n'ont pas nécessairement besoin de se dirriger vers une partie spécifique du feuillage, puisqu'elles finiront masquées avant d'atteindre le state de brindilles.

Alors j'avoue qu'au départ, cette perspective m'a surpris. "Mais? on ne voit jamais un arbre sous cet angle !?" Eh bien, à mieux y réfléchir, si: si son tronc est penché vers le fond de la scène et que son feuillage est donc majoritairement plus loin du plan vertical où se trouve la base de son tronc. Si ça permet de mélanger art et gameplay, ça me va tout à fait.

Another set of tree tops that made me buy a switch game was those of Eagle Island. I seem to see the construction pattern, here: draw arcs, then cover them with leaves. Colors within an arc are mostly identical. Each leave is its own cluster, making it distinguishable from the others but without any per-leaf shading or texture. I wonder whether something alike could be used for Bilou. I dig the color choice a lot, though.

Je remets aussi pour le coup le haut des arbres de Eagle Island, principalement construit de feuilles larges mais moins nettes (entendez par là: une seule couleur par feuille, sans détail intérieur) qui sont disposées le long de courbes dont le rayon serait vers le haut. Du plus bel effet pour le décor, mais moins facile à intégrer, je pense.

J'aime beaucoup leur teintes, par contre, on est sur une palette de 6 couleurs allant du h156s89v46 au h167s65v27 avec un h135s67v59 et h117s70v73 pour les feuilles plus lumineuses

Another nice-looking one from Whipseey, althoug it's a bush, not a tree. Well, it did show up while looking for Eagle Island screenshots, so let's have it here, esp. since it is pretty straightforward to guess how they were drawn: 1) stack balls 2) give balls protruding 2x3/3x2 rectangles (that will be leaves pointing out), 3) draw mouth-like shadows at the bottom of the balls

Et puis, en cherchant celles d'Eagle Island, je suis retombé sur ce buisson plutôt sympa de Whipseey. Faites des ronds, ajoutez-leur des rectangles de 2x3 ou 3x2 dans la même couleur pour faire les feuilles qui dépassent, ajoutez des grosses zones d'ombre dans les bas des ronds pour faire l'ombrage et vous obtenez un buisson du plus bel effet... ou peut être un terrifiant hydre des bois prêt à dévorer les champis imprudents ... allez savoir :P

Et puis bon, je termine avec un petit screenshot tout pourri de Tiny Thor parce que je suis en train de jouer au jeu (enfin, pas là tout de suite, hein) dont les graphismes bluffants sont par le talentueux et renommé Henk Nieborg et que oui, il y a des arbres dedans ! ... sauf que ...

euh. Ouais. autant la maestria est indiscutable dans le tronc (c'est bien parce qu'on va en voir beaucoup), autant le feuillage, franchement, j'ai du mal à ratacher ce style inimitable à une quelconque forme 3D. je passerai donc mon tour sur ce coup-là.

Oh, and yeah, there are trees in Tiny Thor, the latest game I've been playing on the switch. It has stunning pixel art by Henk Nieborg globally speaking, but to be honest, I could hardly devent the shading decision in that specific big tree. So I'll just not try studying it either.

edit: I just happen to have discovered Roskur's Run a few days after this got posted and creator from Dirty Beast Games very nicely posted their placeholder bush that could also be used for a fairly interesting top-of-tree.

Et un p'tit dernier, sympatique post du dévelopeur de Roskur's Run dont j'avais apprécié le "placeholder art". Elégant, tilable, varié, déclinable... tout ce qu'il me faut ^_^

First it has low colour count, which suits me well for background purposes. It tiles well (top and sides reuse same pixels) and the central, smile-like shadow brings in interesting but freely-positionable detail to avoid monotony of a flat colour. Conceptually, it works much like the bush of whipseey, but with shapes that better match the current style of my background leaves.


Saturday, August 26, 2023

Apple Bop ?

Une pomme qui vous fonce dessus, ça fait mal. Forcément. Une qui vous tombe sur la cafetière également. Surtout quand elle est aussi grande que vous. Si vous lui tombez, dessus, là, c'est elle qui tombe dans les pommes. En disant ça, j'ai couvert 75% des interactions possibles entre Bilou et les Applemen dans Apple Assault.

If you get an apple on your head, that hurts. If you're thrown an apple in the face, that will hurt too -- especially if the apple is almost as large as yourself. Chances you won't get hurt if you stomp on an apple while falling but that will likely do the apple no good. All these cover 75% of the interactions between Bilou and the Applemen in Apple Assault.

Now, what if Bilou run into an apple. Should he still get hurt ? I know it is the default behaviour in platformers: if Mario runs into a koopa, it gets hurt no matter what direction the koopa was facing. You can only kick a koopa shell if you stomped the koopa first. You can create stories about it like "yeah, as you kicked the koopa, it clawed to the ground, turned back so fast you couldn't react and bite you. You're dead", and by '86 standards, they would work. But does it mean we should continue like that ? Can't we find something more fun ? It's an apple, after all. It's not all covered with deathly spikes!

Mais si c'est Bilou qui arrive dans le dos de la pomme ? Est-ce que le choc devrait aussi lui faire mal (à Bilou) ? dans Apple Assault, la réponse est "oui". Dans la plupart des jeux de plate-formes, la réponse serait "oui" aussi. Un koopa qui bouge, c'est un blesse, qu'on l'approche par-devant ou par-derrière. Mais est-ce que c'est forcément la bonne chose à faire ? Est-ce qu'il n'y a pas moyen d'être plus *fun* que ça ? C'est une pomme, après tout. Pas une chataigne couverte de pics.
En plus, maintenant, il y a un précédent avec les Pendats de School Rush: on est blessé quand ils nous foncent dedans ou quand on tombe sur leur pointe. C'est plutôt logique. on est blessé aussi quand ils font demi-tour, mais de nouveau, vu la violence du mouvement, un mauvais coup est vite parti. Par contre, tant qu'on est dans leur dos, on pourrait presque les pousser, on ne se fait pas mal. Alors si c'est vrai avec des soldats, ça devrait aussi être vrai avec des pommes, non?

When you consider that, in School Rush, pendats -- despite their aggressive and spiky appearance -- will not harm Bilou (only block him) if he comes in their back, it would make the whole game more coherent if we would equally have the back of Applemen harmless. Plus, it could actually be pretty fun to see an Appleman slightly bounce forward on such a collision, as if Bilou had kicked him in the back while walking, and only then actually turning back, being surpised to see Bilou and finally attacking.

Voilà donc ce que j'ai envie de faire pour Bilou: Dreamland (et la green zone dans le futur): si Bilou bouscule un Appleman dans le dos, c'est l'Appleman qui fera un bond et Bilou ne subira aucun dommage ... dans l'immédiat. Parce qu'en revenant sur le sol, l'Appleman fera alors demi-tour et, voyant Bilou, lui foncera dessus (comme il fait depuis 2009). Ce qui n'empêche pas le joueur de sauter par-dessus l'Appleman, peut-être même suffisamment tôt avant que celui-ci ne se retourne, et qu'il ne se rende donc compte de rien. ça,, ça a le potentiel pour être bien fun ^_^

Saturday, August 12, 2023

Swinging animations

Plus le design de Bilou: Dreamland avance, plus la possibilité de s'accrocher et se balancer prend de l'importance dans le gameplay envisagé. Et c'est tant mieux sauf pour une chose: mon moteur d'animations est actuellement très mal adapté pour ce genre de chose. Comprenez, on risquerait plus de se retrouver avec quelque-chose de raide comme Mickey Magical Quest plutôt que les mouvements souples de Fury of the Furries. Ce serait d'autant plus dommage que Spongebop ne souffre pas de ce genre de limitation et que ça rend furieusement bien. Mais voilà, l'image de Spongebop est indépendante de l'angle de sa corde.

The more I sketch some design ideas for Bilou: Dreamland, the more swinging with a rope-like object occurs. I won't claim it will be a primary game mechanic, but it will certainly be an important one. There's just one drawback : so far my game engine offers no support for that kind of animation. Oh, sure, I could go the Mickey Magical Quest way of hard-coding a few frames and the positions Bilou should take to swing, but given that I have a freely-swinging Spongebop in the previous game, I'd prefer go for something like what I've enjoyed in Fury of the Furries: game physics that allows 360° swinging, with free positioning anywhere on the circle defined by the current rope length and almost free extension / reduction of the rope length (Mickey can only climb).

One picture every 10° to swing a tiny
Pour Fury on a pas moins de 36 images différentes et la possibilité de faire un tour complet de son point d'accroche si la physique le permet, soit 10° entre chaque image. Mickey n'offre que 7 images différentes pour un angle de balancier de 90°, soit 15° entre chaque image. La différence devrait être légère (on passe quand-même de 60 fps à 40 fps avec un rapport pareil), mais là où ça change tout, c'est que les angles de la corde elle-même sont discrets à 15° près avec Mickey, dû au fait du hardware 'par tiles' de la super NES. Du coup, les positions de Mickey deviennent elle-même discrètes (bien qu'il puisse remonter sa corde). 

Je me suis donc repenché là-dessus pendant mes p'tites vacances, à l'abri dans mon épave de bateau pirate et j'ai peut-être bien une solution. L'idée serait de compléter les deux modes d'animation actuels (l'un qui suit une ligne du temps, l'autre qui colle à une série de déplacements) pour faire un système dans lequel on va choisir d'avancer ou non dans l'animation selon le vecteur-déplacement actuel. En gros, je fais une animation dans laquelle Bilou se balance autour de sa main et le moteur passera à l'image suivante quand Bilou va dans la même direction que celle suivie par cette image dans l'animation.

The summer idea about it was to handle that with a new way of playing back an animation: rather than enforcing a delay between two frames or a specific motion, we would compare the in-game speed vector (xg, yg) against inter-frame motion vectors (xa, ya). You'd then step to the next frame in the animation only if (xg, yg) is "beyond" the expected motion to the next frame. A bit of maths done in a sandbox (literally) shown that we can tell that just by two integer multiplications per frame. It will need separate animation for swing-to-the-left and swing-to-the-right, but I'm perfectly fine with that.

Mais comment savoir si un vecteur effectif (xg, yg) est plus proche de l'ancien vecteur de l'animation (xa, ya) ou de celui de l'étape suivante (xa', ya') ? Je suis parti de mon vieux cours d'analyse math avec "l'extrémité du nouveau vecteur (xg, yg) doit être au-dessus de la droite définie par k*xa' = k*ya' (que l'on peut réécrire y = (ya' / xa') * x). Mathématiquement parlant, on sera au-dessus si yg > ya' / xa' * xg. Programmatiquement parlant, c'est moins drôle, à cause des divisions par zéro dans les verticales et du manque de précision des divisions / multiplications quand on se contente de travailler avec des nombres entiers. J'ai donc voulu ruser et faire un peu d'algèbre pour passer à yg * xa' > ya' * xg, qui ne fait que des nombres plus grands que 1. En fait, je venais de retomber sur l'expression de l'aire signée d'un triangle, bon vieux truc du cours d'algo II à l'unif (merci, feu PAdM). ça tombe bien: elle sert justement à calculer si un point est à gauche ou à droite d'un vecteur donné.

Il faudra vérifier expérimentalement si je dois modifier le sens de l'inégalité pour le balancier-retour, trouver un moyen d'intégrer ça au scripts (probablement avec un 'speedmove' comme on a déjà un 'selfmove'). La bonne nouvelle, c'est que le même système pourrait probablement marcher aussi pour une voiture vue du haut, des segments de lianes qui se balancent et segments de ponts qui s'inclinent :-P

extra bonus with the anim.x * gob.y > anim.y * gob.x expression, is that a faster motion (e.g. longer rope) or a slower motion (e.g. shorter rope) will not affect things, because it would affect both gob.x and gob.y by an identical factor k and thus their effects will compensate in the comparison.

Saturday, July 22, 2023

DS Ram Leakage in TestNScripts

I had thought that fixing 'slope unit-tests' in addition to 'wall unit tests' would mean my fixes to get the scorpeye behaving as intended. That would mean the 'summer code review' would be over and the 'summer game dev' could start. Well, that was hoping too much, because there's actually one more test that, for some reason, doesn't show up in ./testme --list but still exists and is important: TestNScripts. This one loads the real .cmd files of SchoolRush and checks parsing went fine. And it does not only check one of them: it will reload many scripts in the same "engine", the way the game does.


while (ntests < n) {
   TestBench tc;
   for (uint i=0; i < REDO; i++) {
       printf("== R%i/%i redo%i ", ntests, n, i);
       tc.ResetEngineResources();
       ParseScript(tc, scripts[rand()%nscripts], __FUNCTION__);
       tc.CheckEngine();
   }
   ntests++;
   tc.Over();
}

No assertion failure here, instead one of the trickiest things to track: memory exhaustion. So it's time for me to learn how to use my tracking tools again. I even left documentation for my future self back in the past (thanks, past self ^^). Once again, the problem arise also in 'default'... looks like I had been over-optimistic in September 2021 when I merged sprites overlay into default.

Well, at least if gives me hope this time: on the 'newmap branch', the forgotten test terminates with 'Out of DS memory'. I managed to get a log of what is allocated at that time, but it would require filtering what has leaked from last ParseScript, and what belongs to the current, interrupted because there's no more memory, running ParseScript. Hopefully, when the bug occurs on default, it gives me a nice 'halt because of leakage' and not a 'out of DS memory' condition. 

  • [done] understand why the new branch gets so high on memory consumption
  • [todo] make sure .spr and .cmd files in fakeroot/ are working fine (despite they now need bilou.spr and school.spr)
  • [todo] that should be the job of UnitTest.mk

edit: dummy! dummy! dummy! ... the 'DS Ram Usage' statistics in those tests is a lie! when it says 95% used, it doesn't mean you'd use 95% of the NDS 4MiB of RAM to play the script. It means that the dummy-allocator-that-never-recycle-memory used 95% of its storage to process the script. Should parsing become more complex (e.g. because we're loading one more file, or creating more intermediate structures), the memory used by the script will grow while the true RAM usage on the NDS would stay roughly the same.

Eeet ouaip. Je me suis donc morfondu un bon morceau de l'été sur le fait que mon moteur de jeu faisait péter sa consommation de mémoire. En tout cas, ça a sérieusement refroidi mon appétit pour le dévelopement de jeu après les heures de boulot. Trouver le problème, ça demandait de se plonger dans une montagne de données et les outils d'il-y-a-longtemps pour comprendre où était la fuite... Sauf qu'en vrai il n'y avait pas de fuite. Tout ça, c'était juste un ou deux messages d'erreur mal nommés qui me faisaient croire que la lecture des scripts prenait plus de 4MB de mémoire (tout ce que la DS a, en somme), sauf que l'environnement de test pour faire tourner les niveaux de mes jeux DS sur PC n'est pas prévu pour qu'on joue, mais pour qu'on trouve les erreurs. 

Donc contrairement à un système de gestion de mémoire ordinaire, quand on lui "rend" un bloc de mémoire il ne le réutilise jamais. La mémoire qu'il fournit à la demande est toujours de la mémoire qui n'a jamais servi pour une autre tâche dans le niveau, et c'est elle qui finissait par faire défaut. Vous me permettrez de croire que si vous avez envie d'en savoir plus, vous êtes aussi en mesure de lire l'Anglais?

If I want to have an estimate of the memory it would take on the NDS, I'd have to keep track of individual allocs and frees and see whether they reach some maximum at a new alloc. (2023-10-23)

What is that one-time allocator ? well, maybe you've heard of bottom-up allocator, where you just keep track of one position in memory: the top-of-used-area, and everything above the top is unused memory. Of course, as soon as you free things out-of-order compared to how you allocated them, that allocator is no longer enough. You would like at least a list of freed-blocks-that-could-further-reduce-top in case the block at the top is eventually freed. Well, the one-time allocator does not even do that. Any byte of one-time memory can be allocated exactly only once. If freed, it is "painted" as free and will remain like that until the ongoing test terminates. 

That is silly as far as memory management is concerned, but it means if some block has not benn freed when the test terminates, you know what role this memory had when it failed. There can't be any tricky-as-hell case where "yeah, that memory had been used for A and then freed, but then it has been re-allocated for B. So maybe B is wrong or maybe something still had a reference of when it was used for A". Sometimes it truly helps. When I'm not misguided by my past self with lying error messages.

Thursday, July 20, 2023

class AppleWalls : public WallTest, UsingApple

So I had managed to get shell/walls interaction mostly fixed. I thought it would be wise to get it run 'unit tests' to ensure nothing got broken by the fix, but it would do more than a few seconds of tests before failing. In Walls/Walk test , a fairly recent addition to MapTests.cpp, for which I do not seem to have any commit where that test is a success.

The test failure will produce patterns such as

--(120,0)+<-508,0>:[-224,0]     's/7f.f, S/fe08.0, '
--(118,0)+<-512,0>     's/7f.f, S/fe08.0, '
--(116,0)+<-516,0>     's/7f.f, S/fe08.0, '

--(114,0)+<-520,0>:[-232,0]     's/77.f, S/fdf8.0, '
--(112,0)     's/77.f, S/fdf8.0, '
--(110,0):[-240,0]     's/77.f, S/fdf8.0, '
--(108,0)     's/77.f, S/fdf8.0, '
--(106,0):[-248,0]     's/6f.f, S/fdf8.0, '
--(104,0)     's/6f.f, S/fdf8.0, '
--(103,0)+<248,0>:[0,0]     's/6f.f, S/fdf8.0, '

which I, too, find cryptic. I should at least inform future self that they are from saved "last state" array, dumped on exception like assert failure during the test.
  • (%{x}, %{y}) is used to report new coordinates
  • <%{vx}, %{vy}> is used to report a new speed
  • [%{dx}, %{dy}] is used to report new delayed step

Together with the arena layout defined by walls1, it can start making sense. For instance, x=104 is the lowest valid position before entering the two-# "wall" of the upper platform, where the Gob-under-test is walking. Granted, this is arcane and confusing, and it would deserve a review fix even if it is only for me. Possibly even more confusing are the `s/%x.%x` codes reported between quotes afterwards. These are generated from ChiefInspector::report() call from the WalkerController.

  • lowercase s indicates that there has been a change of tile considered in doslopes(). hex values are the coordinates of the hotspot used to decide that.
  • uppercase S indicates that doslopes() reported we're on sloped ground. hex values are the speed defined during doslopes().

Even with that wrong report of sloped ground fixed, I still have my 'follower' capable of entering walls. It happens when we stop next to the wall, but do not cancel the 'step value still needed'. More investigation needed.

With some break getspeed and cond BREAKNO (x >> 8) < 105 && cdata[GOB_XSPEED] == -520 in an epic gdb session, I could trace precisely what happens, one GobController call after another, and how the speed, delayed steps and the like where further processed.

The issue turned to be linked to the 'maxmove computation', an engine feature added in 2020 that scans the animation commands ahead of think() calls, so that we don't ask doslopes() to check for something different than what we'll actually do, else the vertical move and the horizontal move won't match and we'll end up out of the slope. The code that decides what to do when we detect a move must be cancelled assumes that the motion debt stored in the 'delayed step' variable has already been validated by controllers. In case of failure, we're safe to move *at least there*.

That was true with the "school rush" engine, where it was helpful to ensure we actually get close enough to the walls for the testpoints to work as intended. It is no longer true if maxmove shortened some of it. How precisely this can be fixed still has needed a bit more time to be figured out.

PS: while looking for the 'inspector codes' in my 2020-notepad, I got my eyes caught by a character stating "this is contemporary to maxmove introduction"... The line below reads "hopefully, no need to tweak things (how many loops are allowed while processing animation instructions, btw) too much. All it takes is building an animation list with enough 'I_MOVETO(+1, *)' commands before it loops."

PPS: of course, fixing things here broke things there, but mostly because code was wrongly assuming that some things (like vertical position or FAIL counter)

Saturday, June 24, 2023

area %d = anim %x

Bon, franchement, je me demande depuis quand ce problème attend que je m'y attelle... que ce soit mon blog, twitter ou mon calepin, impossible de trouver une date correspondant à "oui, la plate-forme se reclappe, mais quand je me mets plus sur sa gauche, je passe à travers.

J'imagine que ça fait partie des choses qui m'ont motivé, fin du mois dernier, à faire en sorte qu'Inspector Widget marche à nouveau dans la démo "3 rooms". Et la visualisation des zones de collision m'avait porté à croire que j'avais mal défini les zones de collision dans l'éditeur, sur NDS.

At some point, I noticed that InspectorWidget wasn't working anymore in Dreams.nds ... not quit ideal when you're trying to add bits of gameplay to your years-long-under-development demo. Especially not when you barely manage to get more than 1 hour of gamedev at once. And after I managed to get it back, I suddenly remembered that seeing Bilou falling through the flipping platform was nothing new and that it was linked to the platform having bad hitbox.

Parce que oui, ayant la flemme de définir les zones à la main, cette 'crocforme' est (avec la branche-qui-rebondit), un des premiers objets du jeu à importer une zone de collision passive directement avec area 0 = anim ${FLAGS}, plutôt que d'en fixer explicitement les coordonnées comme j'ai fait avec Apple Assault et School Rush.

Sauf que sur DS, les hitbox sont parfaites. Rien à redire. Je re-transfère le fichier (le lendemain), des fois que j'aurais oublié que j'avais déjà corrigé le soucis. Je recompile (le surlendemain), je teste. Pas d'amélioration. On est samedi, je sors l'artillerie lourde: une nouvelle instruction `break` pour le langage de script histoire de pouvoir mettre un breakpoint dans ddd et commencer à inspecter le comportement du parseur de script pile là où ça coince: dans la boucle qui lit les commandes 'Define' de l'animation où on fixe les coordonnées de la zone solide et des hitboxes pour l'animation.

At first, I thought I had just messed the hitbox within AnimEditor for DS. But a few checks and WiFi transfers later, it turned out that no: the data (as far as the editor was concerned) was ok, but the game still would use the bad hitbox. Checking that on the parser would be darker than night -- or so I thought. This is how the parser now supports an additional "break" instruction that is a noop where you can set a gdb breakpoint while parsing things. These are not meant to be committed, of course, but it definitely helped.

A few 'next instruction' later, it turns out that my engine is using tool-absolute coordinates for the hitbox instead of object-relative coordinates. That is, when you edit an animation in AnimEDS, you don't only store how to use it in-game, but also what you need to edit it again in the editor, and that includes where to put each component on-screen in the editor. The character's center and the edition widget center may match, but they don't have to.

Le code qui gère ça n'est pas tout neuf: il a permis d'améliorer les hitboxes pour attraper les objets dans SchoolRush il y a près de 10 ans. Mais j'ai commis l'erreur de copier-coller le code plutôt que de faire une fonction "importArea" qui serait utilisée des deux côtés: pour les zones actives et les zones passives.
ça n'aura donc pas été trop compliqué de faire en sorte que les deux utilisent le même bloc de code (le bon) et d'avoir une plate-forme qui fait *enfin* ce qu'on attend d'elle :-P

J'espère que je passerai un peu plus vite de la découverte du problème à la recherche de sa solution, la fois prochaine ^^"

If I want to fix that, I need to track not only hitbox#n ORIGIN coordinates, but also those of the 'solid' box -- the one used to decide whether the object cando() something. then when one 'ORIGIN' instruction shows up for the area we're interested in, we create relative coordinates. Hopefunny enough, it turns out I already have code that does precisely that, because I have both active and passive hitboxes and I failed to avoid repeating myself when writing the original parsing code ... and later failed to fix both copies when I realised things were not going as they should have.