mardi, mai 28, 2013

extractanim.pl

Maybe you wonder why, after two teasers on "the great walking pendat", you still can't see it animated ? Well, the thing is I still haven't got any competent animation exporter so far. All the previous animations from AnimEDS were either hand-generated from static snapshots, or screencasts from either AnimEDS in desmume or from the game engine.

But that screencast tool (byzanz-record) shows its limits when one wants pixel art-level animation, and not merely a gameplay teaser: it's introducing dirty pixels all over the place.

So I refreshed my brain with API of the Imlib2 library I used for spr2png a while ago and started a proper animation ripper for my own .spr file format. I plan to first build static frames in .png and let a post-processing step build an animation out of that. I bet swapping will be the most annoying feature to support.

Stay tuned ...

hmm ...

edit: Well, the tools have progressed, that's sure. I still need to hack support for 32x32 blocks, though. On the other hand, it's pretty screwed for the pendat: I used pencil tiles as a quick replacement for pencil sprites content, but that's not a valid approach in the game engine, and that isn't either for the extractanim.pl tool :P

But with the "apply change to this frame and all the following" button, replacing the page and recovering graphics is a matter of 5 minutes with the DS in hands ^_^


vendredi, mai 24, 2013

Un grand pas en avant ...

Pas évident d'avoir des pentes correctement intégrées dans le déroulement du jeu.

Souvenez-vous: dans Apple Assault, si les pentes fonctionnaient presque correctement, il me restait une certaine probabilité que Bilou se "bloque" arrivé au sol, et ce n'est qu'avec la révision du moteur de jeu en Septembre dernier que le bug fut corrigé. Mais on était pas tiré d'affaire pour autant...

A force de refaire des tests pour m'assurer que Bilou ne puisse plus se bloquer dans les murs suite à un atterissage en catastrophe (en mode "pas à pas"), je finis par me rendre compte il y a quelques semaines que lorsque Bilou arrive au bord d'un livre (terminé par un tile pentu en bordure), il oscille quelques fois entre "marcher" et "attendre" avant de finalement se décider par sauter. C'est la plupart du temps presqu'imperceptible pour le joueur qui ne notera peut-être qu'un temps de retard, mais je me suis mis avec ce projet d'avoir un moteur de jeu irréprochable. Exit l'à-peu-près et les excuses bidon: si je ne peux pas être le moteur candidat pour Super Mario World 3 sur SNES, alors le côté "documentation de comment on aurait sans doute pu faire les choses avant de passer à la 3D" perd sa raison d'être.

My nephew had pointed out that Bilou could get stuck in the "School Zone" quite a while ago, and while I was checking that I fixed that properly, using step-by-step mode in Inspector Widget, I realised that Bilou would also temporarily "stick" to book corners before falling down. In stop motion, one could have noted that it flickers between walk and idle 2 or 3 times before actually jumping off the cliff. I want a rules-perfect engine, not a "we can play it out", so I invested several evenings figuring out what was actually wrong.

As in many engines, I handle slopes using a 'hotspot' pixel that must stay on the 'curve' defined by the slope. The rest of the character (the dark box) may enter slope tiles or be in the void, what it doesn't cando() is entering those black, solid tiles (which is checked separatedly). Making sure this looks nice is the level designer's problem, not the engine's. Within a tile, when the horizontal coordinate is changed by the 'walking' behaviour, we retrieve the ground height for the corresponding target pixel and align the hotspot there. With that approach, we may end up in a tile that has no ground at all (gh=0) when switching to the next (horizontal) tile. In that case, the alignment is performed on the tile just below.

Commençons par un rappel du fonctionnement des pentes dans mon environnement découpé en pavés (les "taïlze/tiles"). Bilou n'y est qu'un rectangle qui doit pouvoir naviguer sans rentrer dans les tiles solides (noirs). Il possède en plus un point de référence (hot spot, en rouge vif sur l'image) qui doit rester en contact avec le sol lorsqu'on suit une pente. Pour avancer d'un pixel vers la gauche, le hot'spot passe d'abord dans un tiles complètement vide (gh=0) où il n'y a pas de sol à suivre. La fonction do_slopes détecte ça et teste du coup le tile situé juste en dessous. Son pixel le plus à droite correspond à une hauteur gh=-8. On se retrouve ré-aligné sur le prochain pixel ... tout va bien.

Le problème apparaît seulement lors d'une transition pente/trou, comme j'en ai ajouté sur le bord des livres. Ici, lorsque'on regarde sous le tile vide, il y a ... un tile vide. Pour do_slopes, celà signifie qu'il n'y a plus de pente à suivre.  Idéalement, on devrait donc juste se retrouver "le long de la ligne bleue" inférieure, qui empèche Bilou de tomber jusqu'à ce qu'il l'ait complètement quitté. Mais voilà, à ce moment-là, on est pas encore sur le sol! Du coup, lorsqu'on teste s'il est possible de tomber d'un pixel, la réponse est "oui" et le contrôleur envoie un FAIL.

Now, when we instead have a sloped edge, as with books, both tiles are AIR and have no "ground" to align on. The do_slopes function then consider there is no slope to follow and let the walker::think behaviour function to adapt accordingly. Walker's behaviour is then to walk as long as possible, testing whether Bilou cando a move downwards (meaning he can fall). That would be all fine if Bilou was aligned on the blue line by then, but he isn't. he's still one pixel above, because that's where the slope ended.

Would have it implied that Bilou started falling earlier, I wouldn't have minded. But that's not what the state machine decides. from its perspective, the backward testpoint is still on the ground and thus the FAIL even is translated into a transition to idle state rather than towards fall.

Seulement, voilà, l'échec du contrôleur n'implique pas forcément une chute. C'est à travers les transitions du comportement de Bilou que les choses vont maintenant se jouer. Tomber d'un pixel ne serait pas impeccable, mais celà nous conviendrait. Seulement, pour celà, il faudrait que le point-test placé au sol indique du vide ... et étant décalé sur la droite, il est toujours dans la pente! Du coup, c'est vers l'état "do_slopes. C'est ainsi qu'on parvient finalement à quitter le livre après ce qui semble être un instant d'hésitation.
à l'arrêt" (idle) que Bilou passe. Mais puisqu'on a toujours le DPAD incliné vers la gauche, on quitte dès l'image suivante cet état pour tenter à nouveau une marche... qui échoue encore. Heureusement, à chaque échec, on avance d'un quart/demi de pixel qui correspond au "mouvement entammé".

Une fois que l'Inspector Widget est devenu assez précis pour faire ce genre d'analyse, il devient assez évident que la solution est de forcer le personnage à s'aligner sur le sol quand on quitte une pente, et de ne considérer qu'on chute que s'il est possible de descendre alors que le personnage est déjà aligné sur ce qui devrait être du sol.

The actual solution is of course to ensure that we're aligned on ground when there's no slope. Only if we cando the move downwards *while being on a tile boundary*, we will claim it a FAIL move that should switch to some other state. But to realise that, significant upgrade on InspectorWidget's precision was required. I even was tempted to turn this post into another "Inspector Widget's novel" post, but the implication on the game engine and behaviour coding was too high, and I'd rather went for a tutorial shape.

Pour faire bonne mesure (et pour éviter que Bilou ne se retrouve en suspens sur un bord de livre parce qu'il n'a pas sauté assez haut), j'ajuste enfin le saut à travers une plate-forme à sens unique (les branches de Apple Assault): on ne peut plus atterir sur ce type de plate-forme que lorsqu'on vient d'au-dessus de la plate-forme.

mardi, mai 21, 2013

Animating with AnimEDS

AnimEDS place les différents cellulos (étapes -- ou 'frame') édités sur une ligne du temps. Plutôt classique pour un programme d'animation, me direz-vous. On peut décaler l'une ou l'autre étape d'animation pour ajuster le timing, mais surtout on peut "casser" un délai en deux en dupliquant l'image en cours.

The "split" button appeared in AnimEDS in early July 2012 -- that is, right when I started using the tool for real. It allows one frame to be split so that one can refine animation from keyframes, and it has been the key feature for almost every animations I created lately. I start setting up all the limbs for an "initial" frame, then copy into 3 to 5 key frames depending on the animation, and adjust position/sprite used by the different limbs to get something fairly close to what I'd have used with "traditional" animation. The result is more or less the 4 hightlighted frames on the picture.

L'animation du crayon soldat est assez longue -- 16 images -- et n'aurait clairement pas eu ce nombre d'étapes avec le moteur d'Apple Assault. Pourtant, il n'y a que 4 étapes-clé (mise en évidence sur l'image) qui sont celles que j'aurais eu tendance à dessiner sans AnimEDS. Vous noterez aussi assez rapidement qu'un grand nombre d'images sont très proches l'une de l'autre.

*Then* the magic "split" button gets into action. I insert between frame 1 and 2 a copy of frame 1, adjusting the delays so that the animation still renders the same, and I'm now free to add an intermediate step to increase the smoothness. This is where the "small crosses" depicting actual (white), previous (straight, greyed) and next (slanted, greyed) position of the current limb proves extremely useful, given that a "dynamic" animation usually implies that there's a lot of movement in a small amount of time, and then a lot amount of time where limbs are only moved by a couple of pixels.

En fait, pour donner un air "martial" au crayon, je passe le moins de temps possible entre deux états quasi-fixes "pied au sol" et "pied en l'air" (comme l'image 01). Par contre, pour éviter que ça ne se traduise en une animation saccadée, il y a un léger "temps de freinage": 3 étapes donc, ou le pied bouge d'un seul pixel d'avant en arrière. J'avais utilisé le même style d'approche avec la course de Bilou, et je crois que ça va devenir peu à peu mon style préféré.

jeudi, mai 16, 2013

"J'vais chercher mon frère à la guerre"

Quelques blocs repris du niveau, les pieds et les mains de Bilou, les yeux de l'encrier et en 10 minutes me voilà avec une assez chouette animation du crayon qui marche au pas dans le bas de l'écran d'AnimEDS... prêt à aller rejoindre les gommes sauteuses et les éponges dingues dans un niveau-anniversaire pour Bilou ^_^

With multi-palette system and clean eyes/hands/feet sprites it only took me some 10 minutes to come up with a first animation of the pendat ... Just what I needed to build the "anniversary level" one step further. I pretty love how AnimEDS allow me to come up with dynamic animations... I'll have to reverse-tutor this one ...
(I shall not replace emulator screenshots with mobile phone real shots -- I shall not
replace emulator screenshots with mobile phone real shots --
replace emulator screenshots with mobile phone real shots -- I shall not ...)

mardi, mai 07, 2013

One must FAIL.

I've been investigating that "stuck in walls" bug for some times now, with little luck to fix it. I'm finally reaching the conclusion that this is not an usual bug, but the bitter outcome of some short-coming in the current implementation of the game engine.

The behaviour of Bilou and NPCs in the game is ruled by some iGobController classes, such as DPAD processing, momentum, gravity and the like. At each frame, each of them think() to evaluate the current game state and adapt the character's state accordingly (e.g. make it fall faster in the case of gravity controller).

The outcome of this "think()" function in turn defines whether some transitions on the state machine should be evaluated. It may either be NONE (no transition involve, we happily stay in the current state), EVENT (something special happened, like the direction reverting or some key being pressed, for which an change in the animation might be desirable), and FAIL (impossible to hold the current state for longer, e.g. we've landed on the ground, there's no further falling possible).

Initially, when Bilou FAILed to fall, it usually meant that the next move would take him into a wall, and so the move itself was cancelled. Yet it proved to be impractical alone. I had need for the impact speed so that e.g. I could decide to make Bilou bounce if the speed was high enough. Then I had need for aligning the move to the closest wall, and not simply cancel it.

All in all, the remaining code is killing speed where it should better not, and obviously not killing it where it would have to. That smells like some legacy code that need to be re-thought from cleaner base.
[done] FAIL => can't do more than this frame in this state. (but move isn't necessarily cancelled)
[done] no longer sticks on walls
[todo]  hspeed is reset to 0 when Bilou leaves a platform. That should only be the case if forced so by a GobExpression
[think] smoother slope-to-fall transition required.

mercredi, mai 01, 2013

Inside the Inkjet

Pas si simple de mettre Bilou dans un encrier, même une fois le résultat à obtenir défini. Il serait sans doute bon de reprendre les évolutions de la semaine qui se sont agglutinées dans une "todo list" qui n'était pas initialement prévue pour ça.
En premier, il me fallait un moyen de forcer les sprites à clignoter de manière sélective, et dont la définition se ferait au niveau des scripts. C'est pour un test, je ne fais pas dans la dentelle et j'introduis simplement un nouvel opérateur dans les expressions de changement d'état qui modifiera cette propriété de clignotement. En quelques essais, je peux rendre Bilou semi-transparent lorsqu'il s'est fait touché. Encourageant.

It wasn't supposed to be that long. With the desired rendering defined, I'd have thought I could implement the flickering-translucent-inkjet in one sunday afternoon at most. I started with a couple of simple checks, like blinking Bilou when hit, and then selectively blink only some limbs. That was all neat and sleek. I had forgotten, however, that 'pulling' some limbs worked only "locally" by rearranging limbs within a sprite (although 3 distinct, globally ordered OAMs lists were introduced).

Mais je ne veux pas que l'entièreté de Inkjet clignote: seulement le "patch" destiné à masquer Bilou lorsqu'il est à l'intérieur. Je change donc la sémantique de cet "opérateur-clignotant" pour qu'il reçoive plutôt des bits de contrôle et laisse le rôle de "compteur" à une variable accessible depuis les expressions du script. Je peux du coup faire clignoter seulement le corps de Bilou ou une de ses mains ... tout va pour le mieux.

Par contre, le code permettant de modifier l'ordre d'affichage des composants d'un sprite (les OAMs du hardware, donc) n'offrait jusqu'ici qu'une modification interne au sprite, faisant passer une main devant le corps, etc. mais sans permettre de placer un composant devant Bilou et un autre derrière. Or, c'est essentiel pour "entrer" dans l'encrier.
C'est le comportement "global pull" qui utilise les mêmes commandes "pullmask" qu'auparavant, mais qui force maintenant un changement de zlist pour les composants tirés vers l'avant-plan. Le code du moteur de jeu n'était pas tout à fait prêt pour ça.

Once invariants for pulling limbs into list 0 on demand were ready, I realised the path to victory was cluttered with unrevealed bugs dating from z-ordering and even per-controller-events introduction. It's all fixed now, but it leaves a kind of annoying "not-quite-professional-coding"... I guess I should have known this from the start, this being a hobby project, I can't always afford being professional :P

That being said, I definitely want to take the time to clean up the interface and provide some coherent way to define the (pullzero?, flickermask, pullmask, flickertime) variables in a cleaner way.


Il m'aura encore fallu un ou deux temps de midi pour comprendre pourquoi ce damné encrier refusait de tenir compte de mes commandes pour continuer à clignoter. En fait, il restait un bug datant de l'introduction des évènements-par-contrôleur. Qu'un seul contrôleur voie sa liste d'évènements prise en compte lorsque deux (ou plus) disent simultanément "évènement", je pouvais m'y attendre. Qu'un "évènement" et un "impossible" se traduise en "réinitialisation", franchement, je ne l'avais pas vu venir. Il aurait été plus "pro" de concevoir des cas de tests suffisament riches au moment où cette nouvelle fonctionalité a été ajoutée. Seulement voilà, un pro code aussi en-dehors des temps de midi :)