Showing posts with label testpoints. Show all posts
Showing posts with label testpoints. Show all posts

Sunday, November 06, 2022

Scorpeye... Enfin!

 Oui, je l'avoue, j'ai été lent. J'ai été distrait (au sens attention détournée). J'ai été démoralisé par cette "carapace" qui ne voulait pas faire demi-tour à chaque fois ou qui faisait des demi-tours intempestifs. J'ai douté, de cet empilement de couches qui devait me permettre de laisser des personnes non éduquées au C++ de bricoler aussi leur propres personnages. Sauf que tout ça, c'est bien joli mais que quand vous avez un dysfonctionnement quelque-part, le débugger vous balance des instructions machines alors que le problème est dans le bytecode qu'elles interprètent. Et mon "inspector widget" (pas encore fonctionnel dans le projet 3 rooms) est loin de valoir ddd.

It's been a long time, I have to admit. I've been slow to get things running; I've had lost the hope that I could get that scorpio-shell turning back when it hits a wall... and only when it hits a wall. It felt like I had built a Jenga stack of complexity in order to have a 'simple' interpreter for state machines. But a language is never less complex than the tools you provide to debug it. And Inspector Widget fell short when trying to explain the issue (partly because it is surprisingly not yet working on Dreams.nds)

Une intuition pendant mes vacances ... j'ai retracé la machine d'état de mon scorpion-cyclope. J'ai bien identifié deux ou trois incohérences, mais rien qui ne conduise au bon comportement une fois corrigé.

Oui, je sais aussi, par rapport à ce que je bricolais sur le côté pendant mes semaines de vacances, c'est maigre. C'était des vacances un peu ... particulières.

Puis hier, j'ai repris la machine d'état de pendat. Lui aussi, il réagit quand il fait demi-tour en se prenant un mur. Et il n'est peut-être pas parfait, mais il marche en général bien mieux que cette satanée carapace.

Alors que scorpeye tentait de reconnaître l'absence de sol à l'aide d'un testpoint, pendat se contentait de "dire" "ah bin si la vitesse horizontale est tombée à zéro, c'est qu'on s'est pris un mur. Sinon, c'est qu'on a plus de sol. C'est forcé."

I found only a small evening of ndsdev during my summer holiday this year, and I used it to map the state machine of Scorpeye. It helped me finding a few weird stuff, but it would not make things run properly enough, unfortunately. Then I realised past Inktober that I already had a 'working' monster that bounced off walls. That was the running pendat. And I started mapping its state machine too (granted, after all these years of dsgametools, I should have given myself a tool to make that automatically). To my surprise, it almost never used the same tricks to perform the same task.

For instance, I used a TestPoint below scorpeye to tell me whether there was ground underneath when it claims it failed to slide on the ground. But this is unreliable because it checks only one point to decide for ground while the WalkController checks the whole surface. Instead, the pendat would test whether it came to a halt horizontally. If it did, that means there has been a wall. Otherwise, it bets there's no ground left.

Same with fall/slide transition: the scorpeye tried to use a testpoint to know whether it FAILed because of a ground. Pendat code author (past self) knew the only thing that can make GobGravityController FAIL was a ground. Being stopped by a wall triggers an event. That means no testpoint validation is required.

Finally, I gave myself an extra variable (there are still plenty available for scorpeye) to remember the sliding speed so that we can bounce walls without having to rely on an ImpactController to save the last speed, because so far I don't think GobWalkerController is an ImpactController. It requires to double some states (like left-thrown vs. right-thrown), but that should not be a big deal.

Still, there's one thing that I'll need to fix with unit testing: from times to times, being stopped by a wall while 'walking' doesn't make the move FAIL, but it does clears the speed. Pendat doesn't suffer for that because it has an "increase horizontal speed" statement in its running states that prevents the speed to stay null. I had to add the same statement to the shell, but it shouldn't be required. that's a todo item for XMAS, I guess.

Il me fallait un double-état pour la carapace lancée, une variable supplémentaire pour retenir la vitesse à la place de la 'variable d'impact'... ce genre de choses. Mais il y a un élément suspect. Pour qu'il n'y ait aucun soucis, je suis obligé de mettre une instruction forçant la carapace à accélérer. Sans ça, il reste des situations dans lesquelles je retrouve ma carapace toujours dans l'état "on glisse vers la droite" mais avec une vitesse nulle est un mur juste à ça droite.

Comme si GobWalkerController avait un bug qui l'empèchait parfois de signaler l'échec (FAIL) même si la vitesse tombe à zéro. ça, ce sera une tâche pour un test unitaire.


Saturday, September 11, 2021

Wall walk test.

If I want to have an understanding of what causes Bilou to get into walls when he arrives straight into it, I need to find a way to reproduce that at will. I have to update my unit tests to cover that new scenario.

The difficulty with that setup is that the mecha will have to keep walking towards the wall for some frames, and only then try to turn back. Scripting a fake game pad controller to force the mecha to do that sure won't be the easiest path to follow. But I have a plan.

Si je veux comprendre ce qui conduit Bilou à finir dans les murs, il faut que je trouve un moyen de reproduire ça à volonté, c'est à dire reconstruire un setup dans l'environnement de tests "unitaires" que je me suis construit. Sans ne sera pas élémentaire, parce qu'il va falloir faire en sorte d'imiter le comportement d'un joueur qui continuerait d'avancer vers le mur avant de faire demi-tour, mais j'ai ma petite idée... Introduire un 2eme "mecha" qui marche normalement sur un terrain plus large et faire en sorte que notre sujet-test suive sa position de la même manière qu'une des chauve-souris-baie, sauf qu'il marche. Séduisant, non?   

Let's have a second mecha that is walking "normally" on one large floor while another one, the Gob-Under-Test is tracking its position and tries to follow it, just like Berry Bats would do, except "walking". That should work, right ?

Well, reality is hitting me hard, here. I haven't managed to get the test cycle work properly. I'm caught in the process of navigating the history and running because even with the obvious sorted out, my new test gets a "memory is leaking" warning and the former 'slopes' test isn't working either.

ça, c'est en Théorie. Mais une fois la frontière avec la Réalité franchie on se prend une jolie douche froide, sous forme de fuite mémoire dans le module de tests. Puis c'est le test sur les pentes qui ne passe plus. Et comme je n'ai pas encore commencé à utiliser la branche "default" comme "ok, tous les tests passent", je suis bon pour quelques retours vers le futur à bord de Mercurial. Je vous fais grâce des Makefile qui ne détectent pas les changement de version du toolkit et des fichiers .cmd qui ne sont reconstruit que dans certaines conditions, mais j'ai fini par trouver le commit qui plombe l'ambiance.

I think from now on, I'll use the 'default' branch as a way to track what is properly passing automated tests and what isn't. Good news, commit 6ab9f8ca2931 dating from this year (April 13rd) pass the slopes tests.

Caveats when doing things like this:

  • Makefile does not detect whether I change devkitpro version, so things that used to work years ago on an older devkit may fail to build nowadays and you'll have to manually clean output/*.o
  • some tests depend on fakreoot/*.cmd, which is not automatically re-populated from SchoolTests/efsroot/ or AppleAssault/efsroot/. and in SchoolTests, the files are generated from SchoolTest/cmd/, which does not occur if you just rebuild unit tests.

18731874

edit: I identified a commit at which the TestBasicSlopes started failing. Surprisingly, it was not a side-effect or whatever: the commit directly does


       if ((cspeed&cp) && !cdata[0]) continue;// this testpoint only works with speed
-      if ((chkflr&cp) && !(checks[pos] & F_FLOOR)) {
+      if ((chkflr&cp) && (checks[pos] & F_FALLTHRU)) {
        if (isi) isi->TestPoint(xtile, ytile, pos, checks[pos], "floor");
        cdata[GOB_TESTPOINTS]|=cp;
       }

Which cannot have no effect on how testpoints behave in the slope tests. But if we compare how monsters in the green zone behave before (1873) and after (1874) this commit, it is clear that it has desirable effects. So I'm now sure I have to fix the tests, not the way testpoints are handled.

Je m'attendais à une sorte d'effet de bord, résultant malencontreusement d'une modification qui n'aurait rien à voir, mais non. Ce commit modifie directement l'interprétation que le moteur de jeu a du concept de "sol". Jusqu'à 1873, pour être du sol, il fallait avoir la propriété "FLOOR". Depuis 1874, il suffit qu'on ne puisse pas tomber à travers une surface pour qu'elle soit considérée comme sol. 

edit++: okay. The leaking object was a 'builder' helper used by the GobState objets. The builder is a std::* object temporarily used while e.g. collecting transitions from a given state before they got 'flattened' into an array for the 'running' part. The array, just like GobStates are allocated in the 'tank', a larger memory chunk meant to host many small objects that all have the same lifetime: one level.

Ah, et pour les plus curieux, j'ai trouvé la fuite de mémoire. ça se passait dans le code qui accumule les informations sur la machine d'état avant de les "geler" dans un brave tableau de taille fixe. Sauf que ce gel est lié à l'introduction de transitions entre les états et que les tests qui fuitaient sont "tellement simples" qu'il n'y a pas de transition: juste un état unique dans lequel le sujet-test reste du début à la fin. Au travail. 

In the 'slope' tests, each state was used to spawn a GameObject using that state for the test. There were no transitions: we first use a forward-moving mecha to check things are fine when moving forward with the Forward state, and then a distinct backwards-moving-mecha. But for my new test, I need transitions. That means that I won't spawn for every states. That means some states are not 'frozen' into the tank, and hence still hold references to std::* objects like vectors or strings. These are the leaking ones. Let's get cracking!

Friday, August 06, 2021

Testpoints and borders

 Testpoints are the oldest check technique I've been using to implement collisions in my games. From Calimero to Bilou and even Crazy Brix. The idea was simply to check the color of a specific point on screen to decide whether it is solid or not. In Calimero, white was solid, and black was not. In Bilou (QuickBasic), pixels with all their 4 lower bits set were non-solid. Everything else was.

But by 2000, it was no longer sounding sufficient. I had a long technical discussion with Gedeon by then, who was enthusiast about Crazy Brix, but pointed out that ball/bricks collisions were imperfect. I had found (by browsing and floppy-swapping- and printing and reading) mask-based techniques for sprite-to-sprites collisions through shifting and masking. But I was still missing a way to do the same for ground and walls.

Then I came up with a paradigm shift: instead of doing checks on a bitmap, I could have a vector-based description of the level boundary and wrote a letter to Gedeon to present him how I'd do it.

There are still testpoints associated with sprite positions, but now they can be attached to the closest level boundary. They can be split in 4 casts depending on whether they should block as floor, ceiling or walls. Once a testpoint is associated with one border, checking whether we're in or out is trivial. One test, and that's it.

My idea was that we could build a graph or a network of boundaries for each cast, so we can switch from one boundary to its neighbour.

I had high hope to use that in my Ultimate Game Maker, but it although I said to Gedeon that navigating the 'network' shouldn't be that hard, just having a Mario-pipe in the level and falling below its height would make our 'right wall' (initially far away) become very near, and possibly skipping many other obstacles that are "shadowed" by the pipe.

edit: by re-typing this on 2021, it turns out that the solution to boundaries network might simply be to use something inspired by the 'portals' used in 3D engines (Ogre ?): above my Mario-pipe, you'd have an "empty" right-wall. That means when you're jumping and have no walls in front of you, the scanning is halted by a 'right-portal' that just serves the purpose of saying 'there's a pipe right below me'. Only when you go past the horizontal position of the pipe, you unlock the wall-boundaries that are further away.

Ah. Bin c'était supposé être un post de traduction d'un vieux post pour le tag-de-la-semaine et à y re-réfléchir, j'en viens à me dire que la clé, pour pouvoir utiliser ces bordures, c'est de rajouter des bordures-portail pour prolonger les bordures réelles. ça conserve le potentiel de décrire la structure du niveau avec moins de données qu'une tilemap, mais par contre il risque d'y avoir du travail pour l'édition de niveaux...

Et en cherchant un peu (autre chose), je retombe sur des illustrations qui doivent dater d'après la lettre à Gédéon, mais d'avant la tentative de moteur 3D pour Bilou.





Tuesday, February 11, 2020

Au tour de meka-blador ...

Même si, j'avoue, mon analyse de "et qu'est-ce qu'on fait maintenant" m'avait conduit à définir MetaEdit dans l'éditeur de niveau comme la priorité suivante ... Mais la tentation a été trop grande: comment se comporterait un vrai personnage dans les tests automatiques de pentes que je viens de faire fonctionner avec un 'tic-tac-Bilou' complètement abstrait.

Je n'en suis pas au stade du Test.Read("blador.cmd"), mais j'ai au moins pu charger un vrai .spr et en extraire les animations (Merci, MedsAnim) puis constuire un nouveau cas de test basé sur un bout de texte extrait directement de blador.cmd.

Pour les bonnes nouvelles on s'arrête bientôt. Le meka-blador obtenu comme ça ne passe pas les tests. C'est qu'il est basé sur la vieille technique des testpoints qui sondent l'environnement autour de lui, et l'arrêtent avant qu'il n'approche trop du bord d'une plate-forme (là où le comportement de Bilou le laisserait avancer au-delà du bord). Mais blador ne saute pas.

Bref, mes collègues m'ont lancé dans la lecture de "Working Effectively with Legacy Code" où il est notamment question de "comment va-t-on s'y prendre pour que notre code écrit avant l'introduction des tests unitaires puisse subir des tests unitaires sans endommager sa fonctionalité". Je fais donc des p'tits exercices pratiques avec un iStateInspector que l'on peut définir au niveau de la classe GameObject et qui sera passé aux appels à GobState(::do_checks(), notamment) à la place de l'ancien flag "verbose_prints". Les vieux printf avec des coordonnées et des CON_GOTO peuvent donc quitter le code de GameObject.o: ils seront remplacé par des appels au StateInspector (s'il existe) qui peut être défini au niveau de MapTests.

Tuesday, September 24, 2013

5 days left

I have some pixels for the "cork platform", I have tunable spongebops. Eraser jumps will not work very well with "invisible walls" as those would act as wall that make no physical sense for the player (unlike Rayman's airborn anti-toons and platforms trapped in cages). Instead, I could have the "prepare" state of verso testing the ground where the next jump should end, and turn back if that location isn't safe.

Would such a remote testpoint work ?

Plus que 5 jours avant la fin du mois de septembre, celui qui est 20 ans après la 'naissance' de Bilou. J'ai dessiné une 'plate-forme en liège', j'ai des éponges au mouvement ajustables. Les gommes-sauteuses ne marchent pas terriblement bien avec les 'murs invisibles' , donc je rajoute un état où elle se prépare à sauter et où elle 'sonde' le sol pour décider de faire ou non demi-tour. Si ça marche, ce sera un record battu pour les testpoints.

Tuesday, June 08, 2010

Animeds

As soon as I've got Apple Assault running and my level editor "repaired" so that we can toy with monsters despite the new "file-scope" state numbering, the next big thing I'd like to work on is the animation editor extension to SEDS.
So far, animation edition is pretty crude: you just push frames by clicking them on the sprite sheet, and you can't modify them afterwards. Plus, you can't save your work for later use in a game.


Le prochain "gros morceau" pour l'édition de jeux sur la DS, ce sera l'introduction d'un "véritable" éditeur d'animation. Enfin, dès que j'en ai fini avec Apple Assault et que LEDS est réparé, bien-sûr. Parce que si SEDS est déjà capable de donner un aperçu d'une animation, il faut bien avouer que devoir tout recommencer au moindre faux-pas est moyennement intéressant. Et qu'être incapable de sauver son travail le cantonne au rôle "d'outil de validation" pour les graphismes en cours.
Pourtant, le workflow de base restera le même: une fois qu'on s'est choisi la spritesheet avec laquelle on travaille, on ajoute des étapes à l'animation en cours en cliquant sur les sprites sur la droite de l'écran.

I've toyed with a lot of concepts around "animeds", which most likely explain why it's only 5% done so far. But this last one seems to work better. It keeps the basic of the current system (click a sprite to add it as a frame), but it builds a timeline representation where you can do additional manipulation. I used the animated apple bonus as an example:

  • click a frame on the timeline to select it
  • L-click somewhere on the time line to move it there
  • select another frame,
  • move it as well.
  • click the "time arrow" to adjust the whole animation duration (and scale delays of every frame accordingly).
And that's just for the basics. From then on, being able to store the animation list in the .spr file rather than being part of the .cmd file will be a major improvement in how I build new monsters.

Pour pouvoir gérer et retoucher tout ça, il faut que j'ajoute un widget "ligne du temps" sur lequel il serait possible de déplacer les étapes d'animation pour ajuster les délais. Accélerer et ralentir l'animation complète peut aussi s'avérer intéressant. "Historiquement", les animations sont stockées sous forme de commande texte parce que je n'ai pas voulu introduire de dépendance trop forte entre le moteur de jeu et l'éditeur. Sans doute une sage idée compte tenu des "condloop" et autre "move 2 *" qui sont venus se rajouter par la suite. Je ne vais donc certainement pas essayer de produire des GobAnims[] dans mes fichiers .spr, plutôt une version "lexèmisée" de l'animation avec une liste de constante et une liste d'instruction à traiter conjointement pour produire ces GobAnims.

I also need the opportunity to duplicate/kill a frame ([+ X] buttons depicted below as (3)); undo a deletion (the little x at (5) can be clicked to summon a frame back to life)

The most challenging aspect, I guess, will be to include engine-related features such as conditional loops (loop the animation only if testpoints are satisfied), the use of "spatial timeline" rather than "temporal timeline" (yeah, that's an odd concept, but I don't see how to phrase it differently :P)

And then, finally, I'll have to include that so-long-delayed composed -- à la Rayman --animation that was present in the BASIC version ^^"


J'ai quelques idées pour avoir une représentation graphique efficace des opérations telles que "copier une étape, supprimer ou récupérer une étape supprimée", etc. Ce qui sera plus complexe, ce sera justement de prendre en compte les boucles (conditionnelles ou non) et les déplacements (en ajout comme en remplacement des délais)... Je me demande dans quelle mesure il serait possible de garder cet aspect-là dans le .cmd, tiens.

Thursday, February 18, 2010

Todo ... later

Time to compile a list of "pending issues" that I'd like to see resolved to progress in my projects. Only then I can actually start adding support for vines, moving platforms, conveyers or whatever else fancy stuff "Bilou Mining" could use.

  • [testing ...] stand and land on slopes
  • [done] fix vertical scrolling issues (cf. Morukutsu comment)
  • [done] a "pause" action that dumps GOB state and stops the game engine so that I can proceed with collision debugging.
  • [done] properly stomp applemen (woodworms seems ok in gedsdemo)
  • [thinking ...] load any background music from the gamescript
  • [done] require a press on a button to trigger a jump.
  • improve berrybat's behaviour
  • [wish] animXX mirror animYY to ease coding of .cmd files.
  • [done] explicitly export states from monsterxxx.cmd into levelxxx.cmd
  • [done] (refactory) give monsterxxx.cmd its own namespace for animations.
  • [done](refactory) 'on event ...' transitions bound to micro-controllers and 'on fail' transitions to testpoints, the same way 'on hit/found' are bound to test/area statements.
  • [done] (refactory) stateXX..YY->stateZZ on event [...] (...) to allow a transition from multiple input states.
  • (wish) a "media manager" to avoid redundant loading of maps, spritesheets, modules, ...
  • [done] (bugfix) make sure all OAMs are disabled when a new level starts.
  • [done, afaik] (leds, bugfix) newly cloned monsters should be playable too.
  • [done] (gfx) draw collectible nuts&bolts
  • [done] (gfx) a separate frame when Bilou gets hit.
  • [done] (leds) "play" button that saves the current .cmd file as "autoexec.cmd"
  • [done] (blog) widen the view, update heading banner
  • [thinking ...] blocks that fall/disappear after Bilou walked them (incl. gfx).
  • (leds, bugfix) You might have several times the same gobno allocated ??
  • (leds, bugfix) Cursor for block-picking doesn't work properly ??
  • (seds) more "storage areas" : tileset, sprites, anims, sketches
  • get the garbage out of the kitchen tonight

Bon, bin y'a du boulot! Entre les bugs dans les la maturation des fonctions expérimentales de LEDS, les graphismes à rajouter, les commentaires de Morukutsu sur la jouabilité, les difficultés pour le debugging des collisions Bilou-Appleman (les applemen étant invulnérables de temps en temps sans que ça ne soit prévu) et le "refactoring" nécessaire pour pouvoir continuer à gérer plus de monstres correctement ... Alors seulement on pourra commencer à penser à des plate-formes mobiles ou des tapis roulants.

Friday, May 22, 2009

Moon walk ?

As seen in former post: Scripting Sunday:

TODO: moving Bilou through the animation isn't the right way to go. Maybe "delay x" in the animation sequence to have the next frame triggered only when we move to the next pixel would be better
Faire marcher un personnage est loin d'être une tâche facile et en particulier, ça demande une synchronisation parfaite entre le déplacement du sprite et son animation. Le moindre décalage et le joueur aura l'impression que le personnage "glisse" sur le sol au lieu de marcher. En clair, le pied en contact avec le sol doit rester au même endroit par rapport au sol jusqu'à ce qu'il se lève. Ce genre de défaut était assez fréquent dans les jeux de mon enfance (hein, Eric ;). Une manière assez simple d'y remédier est évidemment de faire courir le personnage, de préférence "à la supermario"

La solution que je pensais y apporter était relativement simple: intégrer les déplacements dans l'animation à coup de "move x y" entre deux images. C'est comme ça que je déplace le wooworm et ça réussit plutôt bien. Le hic, c'est de combiner ça avec une vitesse éventuellement variable et le test des collisions qui a été ramené dans le contrôleur (qui ignore tout de l'animation en cours).

I've never been satisfied by typical game-making animation tool that just let you define a constant moving speed (e.g. one pixel per frame) and an walking animation that you try to match that. It always gave me the feeling that the hero is "sliperring" on the ground or walks with rollers. I initially planned to fix this by integrating moves to the animation itself (i.e. 'show frame A, then wait for 2 frames, move by 2 pixels horizontally and show frame B'). That's how woodworm works, but i couldn't simply extended to Bilou's walk... Not until a friend of mine suggested that i could also delay the animation _until sufficient movement has been accumulated to 'hop' two pixels away_. Here comes the specific code that does that (only when the animation is "self-moving", which would be typical from ladder climbing, walking and other "friction-based" moves.

C'est Pierrick qui m'a donné la solution a mon problème en racontant comment du temps du CPC il faisait faire des sauts "réalistes" à son petit bilou en modifiant la durée d'affichage à chaque emplacement vu qu'il lui était impossible de placer le bilou (eh oui, c'était lui) entre deux tiles (pas de sprites en CPC basic ?). Plutôt que de chercher midi à 14 heures (du genre "modifier la vitesse d'écoulement du temps pour que Bilou coure plus vite"), j'ai juste changé l'interprétation de "move x y" en "ne passe à l'étape d'animation suivante qu'une fois que le contrôleur aura 'accumulé' le décalage suffisant. Traduit en code, ça donne :

inline bool trymove(int dx, int dy) {
    forcechecks=false;
    if (selfmove) {
        int cflags  = cast==HERO?F_PLAYERTHRU:F_MONSTERTHRU;
        bool notyet = (dx>0 && cdata[4]<dx) || (dx<0 && cdata[4]>dx)
                   || (dy>0 && cdata[5]<dy) || (dy<0 && cdata[5]>dy);
        if (notyet) return false;
        if (cando(dx,dy, cflags)==cflags)  {
            x+=dx; cdata[4]-=dx;
            y+=dy; cdata[5]-=dy;
            return true;
        } else {
            cdata[4]=0; cdata[5]=0;
            return setstate(state->dochecks(x>>8,y>>8,world, cdata));
        }
    } else {
        x+=dx;
        y+=dy;
        return true;
    }
}
  • selfmove est défini par état : true pour monter à l'échelle, false pour tomber, etc.
  • cdata[STEPX] et [STEPY] accumule les valeurs de cdata[XSPEED] et cdata[YSPEED] (vitesses définies par le contrôleur) en mode "selfmove" (normalement, on a directement x+=cdata[0])
  • trymove(dx,dy) est appelé lorsqu'une étape d'animation utilse la commande "MOVE"
  • condloop et check permettent de vérifier les testpoints sur des frames données (p.ex. quand le personnage a de nouveau les pieds au sol).
Voilà. J'avais envie de démystifier ça. J'espère que ça sera utile à l'un ou l'autre.

A pair of per-object variables (cdata[])will thus be used to accumulate some intended move until that "step" size becomes large enough for the move x y instruction found in the animation list. The trymove(dx, dy) tells whether such a move is possible right now or must wait until more motion has been accumulated.

edit: De manière étonnante, Miyamoto avait lui fait le choix délibéré, dès Donkey Kong, de casser le lien entre animation et déplacement parce qu'il jugeait qu'une animation de marche réaliste "ne collait pas à l'action frénétique d'un jeu vidéo" (l'Histoire de Mario, p.240)

2024 #choice reality check: It is still there, and I like how it makes many character feel like they're in contact with the ground. But let's be honest, it makes the code a bit more complicated every year. It makes it possible to have characters whose motion accelerates and decelerates over one step with a constant average speed, but offseting the graphics for a truly-constant-speed could achieve something similar, and that wouldn't even be complicated for a compound sprite. So there I am. Maybe this was not a good choice, but I haven't replaced it yet.

Monday, April 27, 2009

Coding Funky Funghi

J'avais envie de mettre côte à côte "comment je me représente le comportement de Funky Funghi" et "comment je réalise ce comportement avec mes outils actuels.

state12 :anim15 {
  using stopper
}

state13 :anim6 {
 using gravity(24,1536);
 testpoint off (16,32)
}

state12->state13 on done [t] (v4 0 = 200 * 500 + ~ :1)
state13->state12 on fail [t] (v4 1 + 4 % :4 0 :1)

The gamescript for funky funghi is a good example of what i called "more sophisticated monster behaviour" in my earlier post about how to write the game logic. Eventually, such behaviours will be edited *on the DS* with a mostly-graphical tool, but the logic is already there: Funghi has two states (jumping and on the ground) to which i can attach different animations and "core behaviours" such as 'affected by gravity' or 'just stay in place'. It also has internal variables (loaded with v4 and stored back with :4, for instance) that both manipulates its internal speed (:1 sets vertical speed, for instance) and can be used as monster-specific state.

I reused the UML state machine model where each state transition can have a predicate that indicates whether the transition can occur (between [square brackets]) and an action that alters the object before it enters its new state.

Voilà donc à la fois un "mockup" futuriste de l'éditeur de monstres sur DS et le genre de script que je bricole en attendant pour quand-même avoir des monstres qui se balladent dans mes niveaux. Histoire que vous suiviez :

  • Funghi a deux états: au sol (12) ou en saut (13). Ca me permet notamment d'avoir une animation spécifique à l'état "au sol" et synchroniser la "déformation" du sprite avec la collision au sol (chose dont le RSD Game-Maker était incapable).
  • Comme tous les objets que j'ai jusqu'à présent, Funky Funghi respecte la convention que v0 est la vitesse horizontale et v1 la vitesse verticale. A priori, ces valeurs sont gérées par le contrôleur et utilisées par la classe SimpleGob pour mettre les coordonnées à jour. Elles sont aussi accessibles dans la machine d'état (:1 signifie "écrire la valeur dans v1)
  • Chaque "gob" possède 12 compteurs sous son entier contrôle. Funghi utilise le premier d'entre-eux (v4) pour compter ses rebonds. "v4 1 + 4 % :4" est une expression post-fixée (pensez 'calculatrice HP' qui doit se lire "prend v4, ajoute 1, calcule le reste de la division par 4 et stocke le résultat dans v4 à nouveau". Ce calcul est effectué chaque fois que Funghi entre en contact avec le sol.

The above script is a bit complicated because i decided to rule out flow control when i 'designed' the language (something i learnt in my thesis), yet i need here that Funky Funghi jumps higher every 4 bops. Plus, it's a bit ugly because i used post-fixed arithmetic (2 + 3 is thus written 2 3 +, as on a HP calculator). Hopefully, the existence of predicates allow me to compensate the lack of conditional expressions in actions by multiple transitions, each with a predicate and a separate action. See the code below, it's actually clearer (sort of) with similar efficiency.

A la fin de l'animation "déformation", la nouvelle vitesse verticale est définie. "500 ~ :1" donnerait à chaque fois la même implusion (~ est utilisé ici pour prendre l'opposé d'un nombre, donc v1 reçoit -500 ... eh non, je n'ai pas implémenté les constantes négatives. Sue me).
Pas d'expressions conditionnelles genre "si(test, réussi, échec)" comme on en trouverait dans Excel ici (ou test?réussi:échec pour les programmeurs); c'est une restriction délibérée, et sans doute la plus contraignante de mon évaluateur d'expressions jusqu'ici. Or, je dois décider de fixer la vitesse après rebond à 700 ou 500 selon qu'on est au 4eme saut ou pas. Ma première approche était de tenir compte du fait que v4 0 = teste la valeur du compteur de rebonds et laisse 0 ou 1 sur la pile selon le résultat, et de jouer sur la multiplication absorbante : t 200 * 500 + vaut 700 quand t=1 et 500 quand t=0. D'où v4 0 = 200 * 500 + ~ :1.

En fait, il y a plus simple puisqu'à défaut d'expressions conditionnelles, j'ai séparé prédicats et actions. Rien ne m'empèche donc de réécrire la machine d'état sous la forme suivante:

state12->state13 on done [v4 0 =] (700 ~ :1)
state12->state13 on done [t] (500 ~ :1)
state13->state12 on fail [t] (v4 1 + 4 % :4 0 :1)
Alors oui, bien sûr, c'est horrible à relire. L'idée n'est évidemment pas d'obliger l'utilisateur final de GEDS à écrire des expressions de ce genre-là, mais plutôt de se donner un "langage intermédiaire" (un bytecode, quoi) comme résultat d'un éditeur de monstres plus graphique.

J'aurai donc quand-même mis un an entre "l'idée sur papier" et le protoype qui tient la route pour la définition des contrôleurs (bon, j'ai fait d'autres trucs entre-temps, évidemment). Et l'intégration des "test de possibilité de déplacements" dans le contrôleur (reléguant les testpoints au rang de sophistication optionnelle) n'est pas la moindre des décisions.

Sunday, April 26, 2009

Petit à petit ...

Le gros du "refactoring" est fait. J'ai bazardé le plus gros de ma gestion de collision basée sur les "testpoints" en faveur d'un test de propriétés sur la zone occupée par le sprite. Et progressivement, je mets à jour les règles définissant le comportement des différents sprites pour en tenir compte.

Une petite modif' d'une classe GobController par-ci, une commande 'xs = 0' dans les transitions entre états par-là ... Ca prend forme. Et j'en ai profité pour rajouter un funky funghi, histoire de voir ce qu'il donne animé.

Most of the refactoring effort on world collisions management is done. I'm now (slowly) updating the state machine rules to take new controllers into account (these use 3-state logic : EVENT, FAIL, NOPE rather than just boolean logic) and fixing bugs due to inconsistencies here and there. Initially,

Comment ?? Vous ne savez pas qui est Funky Funghi ??

Ah oui, c'est juste, je ne l'ai pas encore bloggé ici. C'est la version revue et corrigée de Boppin' Toadstool, qui apparaît dans la bande dessinée et que j'ai pixelisé il y a quelques mois en Suisse. Au départ, je voulais me le garder pour vous faire la surprise, une sorte de petite animation exécutable sur DS où on aurait eu les sprites rejouant le scénario de la BD.

This also allowed me to introduce Funky Funghi, the remasterised poisonous, bopping toadstool into the woods. Funghi confirms that 32x32 sprites work well with the engine and that Gravity controller can apply not only to the hero (though it required a bit of tweaking ^^"). He's also the very first monster to use internal counters: much like the Mad Mushrooms of the shadowlands, Funky Funghi will do three small bops before he goes for a higher jump.

J'ai encore quelques petits bugs à corriger, mais le plus gros est déjà réglé. Début de semaine, figurez-vous que dans certaines circonstances indéfinies, Bilou s'envolait à vive allure, hors de tout contrôle jusqu'au moment où, sortant de la zone de jeu, il provoquait un magnifique "Guru Meditation" ... Comme vous voyez, on revient de loin ^_^

Mais là, il y a un rayon de soleil et je vais aller me faire une p'tite balade jusqu'à la bulle de collecte des verres.

edit: cliquez sur la photo pour regarder la vidéo youtube.

Friday, March 20, 2009

ça descentote ...

Je me faisais un petit tour de jeux SNES rapidos pendant que ma fée écoute Cedra... En retombant sur Twinbee, je me rends compte d'un élément intéressant: le perso peut rester sur une pente même si un seul pixel s'y trouve.

Autre élément: dans ce jeu, une pente "repousse" automatiquement le joueur vers le bas. Si vous marchez le long de la pente, arrivé au bord formé par un mur celui-ci vous "éjecte" avec une vitesse presqu'exclusivement horizontale.

Un système de contrôle original, qu'il faudra que je compare avec le comportement de SuperMario et autres Rayman the Hedgehog Commander ...

As i was busy converging my data to a new USB backup drive, I gave another try to some of my favourite platformers. Twinbee (rainbow island adventures) caught my attention with its slope management. First, slopes always "push" the character downwards, so you cannot "stand still" on a slope. If you walk down a slope, you'll get "thrown" horizontally when you go past the edge. Interestingly, you can stay "on the edge" of a slope, as shown on the screenshot, hanging in the air (i haven't checked whether it also happens when walking down the slope or only when being "pushed backwards when you stop walking up the hill").

I'll investigate that deeper, comparing the physics of slope-walking in Twinbee, Fury of the Furries, Mario, Keen and Rayman, and possibly some Sonic game if i manage to locate some. My feeling is that gameplay of slopes should prevail on accuracy of physical behaviours to make the game fun. Slopes are not just a way to have your character elevating as he moves: in a platformer, they should be used to tune the difficulty of basic moves (jump or stun ennemies).

Konami's decision to have Twinbee "pushed backwards" when halting on a slope clearly makes anything above a slope harder to reach precisely, yet the fact that you're stopped at the edge of the cliff compensate by not throwing you into hazards. Of course in Twinbee, slopes also play an important deflecting role when you're flying with your boosters, but that's a different story.


New Super Mario Bros. : pratiquement aucune pente en bord de plate-forme dans ce jeu. Les seuls emplacements où j'ai pu le détecter, c'est soit dans le niveau des glaces ou pour des plates-formes spéciales (champignons mobiles, leviers ...), pour lesquels le bord est carrément arrondi et où Mario suit l'arrondi. La friction est parfaite : hormis sur les pentes enneigées, Mario tient en place sur les pentes.
No real slope-edge to report in that game, except for some special objects (moving mushrooms, levers, falling rocks ...).

Commander Keen : dès que le test-point central de commander Keen n'est plus sur le sol, Keen tombe et se fait "repousser" par le mur. A noter que, compte-tenu de la faible largeur de Keen, il y aura ajustement de deux pixels à peine. En saut, il y a moyen de voir Keen "atterir presque" sur le bord d'une pente et se faire soudain repousser. Dans le mouvement, l'ajustement en X donnera aussi l'impression que Keen "saute" vers l'avant en quittant la pente en marchant. Idem: friction parfaite.
As soon as the "hot spot" test-point of Commander Keen leaves the slope, keen falls and is "pushed" by the wall. Given the small width of Keen, this is merely a 2-pixels extra move that you'll barely detect unless you try to land "not quite" on the edge of the slope.

Rayman : les pentes sont toujours terminées par un bord horizontal. La seule exception que j'aie relevé à cette règle, c'est dans le pencil pentathlon où au bout d'une course effreinée, Rayman passe le long de 3 crayons inclinés. Ceci dit, les crayons en question sont terminés par des gommes-bumper, et donc on a pas vraiment la physique d'un "bord incliné" comme les plate-formes de Commander Keen. A noter que, du coups, Rayman sait s'accrocher à tous les bords, alors que les bords inclinés ne permettent pas à Keen de se suspendre dans Goodbye Galaxy. Ils ont bien tenté de lever cette interdiction dans "Alien Ate my Baby-Sitter", mais c'est la source de bugs innombrables dans ce jeu.
Virtually all slopes are terminated by a horizontal edge. The only exception i could spot is the Pencil Pentathlon, but there are bumpers at the edge of those pencils, so it's not exactly what you'd call an edge.

Mario World: 4 angles de pentes différents dans ce jeu. Pas de doute : on cherchait à démontrer la supériorité de la nouvelle SNES ... A noter qu'il y a deux types de "bord de pente" : celui qui donne sur un mur solide, et celui qui n'est qu'un bord de plate-forme. Dans le cas "bords de mur" (comme avec le beetlebug), je ne note que des pentes possédant un rebord horizontal.
Four different slopes! That's what i call "technically amazing" for introducing the new (back then) Super Famicom. Two kind of slopes here: those who are merely platforms (side picture), and those who are the edge of a solid area (bottom picture). In first case, mario will fall as soon as the test point is no longer on the slope (like Keen), and in the second, there is usually a horizontal edge after the slope (like Rayman).

Sur les pentes à moins de 45°, friction parfaite. Les pentes à 45° et plus repoussent Mario vers le bas. En clair, il sera impossible de s'y arrêter et difficile d'y contrôler sa vitesse. Une pente comme celle représentée sur l'image du haut correspond donc à une double difficulté : on tombe plus tôt (des que le test-point a quitté la surface) et on ne sait pas s'arrêter. En revanche, c'est bien un "test de boîte" qui a lieu pour les surface horizontales.
Also note that friction differs from the slope: over 45°, the slope is slippery and you cannot stop, while slope below 45° behaves like plain horizontal ground.

Kirby (squeak squads): c'est le jeu idéal pour ce genre d'observation car l'animation de Kirby est différente selon qu'il est à l'arrêt sur une pente ou sur une surface horizontale. De plus, comme dans Keen, la friction est parfaite quelle que soit la pente (un jeu pour gamins, quoi, mais j'adore). A noter qu'à l'inverse d'un personnage comme Keen, Kirby se prète plutôt mal à un mécanisme de "test-point" unique, puisque sa base au sol est particulièrement large.
Interesting game to study as there are two different idle frames depending on whether Kirby stands on a slope or on a horizontal area. Here we can observe an additional "horizontal" move after the hot spot has left the slope, but while kirby cannot "fall" yet due to the wall (box-cando test).

Phénomène intéressant, dans le cas d'un "bord pentu descendant" comme sur la photo ci-contre, on observe un comportement "horizontale - pente - horizontale - chute". Dès que le "test-point" qui sert à suivre une pente a quitté la pente, Kirby a donc le choix entre deux actions: continuer à avancer à l'horizontale ou tomber. Il ne tombera pas tant qu'une partie de sa base reposera sur le bord du mur (d'où la deuxième phase horizontale). Du point de vue du code, ça pourrait se traduire par:

  • test = cando(FALL, x+dx , y+1);
  • if (test&FALL) return switch_state(falling);
  • if ((test&SLOPE) && testpoint_on_slope(x+dx,y+1)) return align_slope(x+dx, y+1);
  • if (cando(WALK, x+dx, y)) return simply_move(x+dx,y);

Friday, February 27, 2009

World Collisions

Left alone, testpoinst are not really a satisfactory way to handle collisions between objects of a game and the world.
First they don't cover enough and cannot catch all the odd situations. Testpoints are points and they cannot detect the case where you're jumping through a corner unless you have many of them (up to one per tile your character covers).


Je ne suis plus trop emballé par les tests-points tels que je les avait décrit précédemment. En premier, ils ne capturent suffisamment toutes les collisions. A moins de les multiplier, on risque toujours d'avoir une partie du personnage qui rentre dans un coin de mur lors d'un mouvement, et en donnant à chaque état sa propre liste de testpoints, on ne fait qu'augmenter les possibilités de bugs.

Second, they are only boolean. They don't let you express that a fish can only move into water, that a bird can only move in air, and that a frog can move through both, but that only the ghost can also move through walls (and i'm sure we can come up with a negative space creature that can only walk through/over walls). This is a situation much more common that one could think. As soon as you add ladders to your levels, you want to express that climbing the ladder "is only possible over ladders tiles". That was a quite strong restriction of Recreational Game Maker where the only way you could create a ladder for a platformer game was to set gravity=0 on ladder tiles ... As a result, your character zooms through ladders if you press the 'jump' key and miserably 'fail to climb up the air' if you press the 'upwards' when he's not over a ladder.

Le deuxième souci avec les test-points, c'est qu'ils sont simplement binaires. Alumés ou éteints. Point barre. Pas moyen de préciser qu'un poisson ne peut se déplacer que dans l'eau ou qu'un oiseau ne peut aller que dans l'air, mais que la grenouille peut évoluer dans les deux milieux. Si on y réfléchit bien, ce genre de situation apparaît bien plus souvent qu'on ne pourrait le penser. Le simple ajout d'échelles dans nos niveau oblige de définir que "escalader une échelle" ne peut s'envisager que s'il y a une échelle à escalader. Le Recreational Game Maker sur lequel j'ai développé (entre-autres) la série des badman ne permettait pas ce genre de subtilité: une échelle était tout simplement un bloc sans gravité, de sorte que l'on pouvait y monter en sautant -- à une vitesse franchement irréaliste -- et on aurait vu le personnage ridiculement patauger dans l'air en faisant de petits bonds si on avait appuyé sur la touche "monter à l'échelle" alors qu'il n'y avait aucune échelle.

The approach of "the nature of a tile" is thus interesting: it can express (through flags) who can do what over that tile. This is a concept used both in Xargon and (afaik) in the side-scrolling Rayman game, where you have "monster-walls" that monsters can't cross (though Rayman don't feel them). This is imho an elegant solution to many level design problems. Let's take the walking block of "Inside the Machine", who wanders around its initial location, "guarding" a virtual spot even if it could theoretically walk further. Morukutsu relied on a "distance" counter that is increased/decreased through the animation code, which won't let you alter the 'coverage area' of the ennemy through level design, but only through implementation ... not so fun.

L'approche "nature du tile", telle qu'elle est utilisée dans Xargon est intéressante à plus d'un titre, et je soupçonne les concepteurs de Rayman (la version 2D) d'avoir suivi une approche similaire. En particulier, elle permet l'introduction de "grillage à monstres", des blocs invisibles et non-bloquant pour le personnage et les projectiles, mais qui sont assimilés à un mur par certaines plates-formes mobiles et par certains monstres. Comme je l'avais déjà précisé lors de l'analyse des maps de Commander Keen 5, je trouve que c'est une technique élégante pour construire les niveaux. Si je compare à "Inside the Machine" (l'autre jeu dont j'ai étudié les sources pendant mes longues soirées d'hiver en Suisse), l'alternative consiste à programmer les monstres de sorte qu'ils ne s'éloignent jamais de plus de n pixels de leur position initiale, en maintenant une variable supplémentaire "distance" qui les force à faire demi-tour. C'est exactement le genre de "pré-câblage de code" que je cherche à éviter. Décider jusqu'où un monstre va, c'est une question de level design, pas de programmation du comportement des monstres.

The final argument is that some testpoints are typically more important than others, and there is a strong relationship between which testpoint is triggered and which movements you can do. A problem i have with the current implementation of collisions in Bilou is that as soon as one of the testpoint checks fails, the whole move is cancelled. If you fall along a wall, you don't expect your character to "stick" on the wall because you keep pressing the D-pad in the wall's direction. Yet, that's exactly what Bilou does right now.
Instead, we'd like something like

if cando(dx,dy) then (x,y) := (x+dx,y+dy)
elsif cando(0,dy) then (x,y) := (x,y+dy)
else { 
  (x,y) := (x,align_on_tile(y+dy)); 
  trigger_testfail_event 
}
where cando(deltax, deltay) checks the whole area covered by the character at a new temptative location. But still, you don't want such behaviour for all your monsters, and it's certainly something that is specific to jumping. You wouldn't see this ever in a Zelda game : instead you'd have the character "walk around" a block if he's sufficiently near to the edge of that block. So this logic is really controller-related rather than being something that belongs to the game engine itself.

So, do we still need any testpoint ? after all, Xargon's cando() used in controller-specific code seems to be fine. Actually, there's something that cando() cannot catch: mixed situations. The controller will be able to tell the game engine that it's no longer possible to fall, but it can't tell whether we are now landing, diving, or bouncing against a corner. This is where we *really* need test-points, and you'll notice that testpoints are only used for state transitions.

I still have to figure out the best way to convert my codebase to this new approach -- the fact that i'm busy working on the real-world house not really helping -- but i think it might be what i've been looking for since the start of the project.


Enfin, certains testpoints sont clairement plus important que d'autres, et le déplacement indiqué par le contrôleur doit être ajusté en fonction de la situation. J'ai le problème dans le saut de Bilou, actuellement: quelque soit le testpoint qui détecte une collision, le mouvement complet est annulé. Résultat, si Bilou rentre dans un mur lors d'un saut, il va rester "collé" au mur jusqu'à ce que je relâche la touche de direction. Celà n'arriverait pas si je pouvais donner une priorité, ou une description plus complète du rôle des différents testpoints (p.ex. puisque c'est un test-point "mur", il n'annulerait que le déplacement horizontal, etc.)
Mais en fait, ce genre de logique est commune à tous les déplacements guidés par la gravité dans un jeu de plate-forme. Ce n'est pas quelque-chose de spécifique au comportement de Bilou, et celà pourrait sans difficultés être introduit dans le contrôleur du sprite. En tout cas, ce n'est clairement pas à introduire dans la classe GameObject elle-même, puisque si on veut faire un r-type, un boulder-dash ou un zelda, on en aura pas besoin.

Mais alors, à quoi servent encore nos test-points ? eh bien, à décider de quel sera le prochain état lorsque le contrôleur signale qu'il n'est pas possible de continuer. Ca veut dire aussi que le contrôleur devra évidemment ajuster la position du sprite de manière à respecter les "cando()" mais pour que les testpoints puissent détecter quelque-chose quand-même :P
  • [done] extract persistent state (found in map and accessible by other classes)
  • [done] iGobPubstate::cando(x,y,flags)
  • [done] manipulate tile_flags array (that translate tile type into tile flags) from the script
  • [done] controllers report "fail" when no "cando()" can be applied
  • [done] who updates coordinates ?
  • [done] update GameObject::play() and trymove()

2024 #choice reality check: cando is still there. it has allowed slopes, jump-throughs and more. And I finally have proof that it also works with levels featuring water. Pfew!

Friday, May 16, 2008

Faut que je repense les test-points

Bon, Bilou saute, il tombe, il rebondit ... tout ça c'est bien joli, mais il a tendance à se retrouver un peu trop souvent dans les murs à mon goût. Et en plus, son comportement devient un peu trop complexe à exprimer dans mon modèle de machines d'état. Pensez un peu. Rien que pour le saut, il me faudrait pas moins de 10 états si je veux que Bilou se "souvienne" de la dernière direction dans laquelle il est allé :P

Great. Bilou jumps, Bilou falls and bounce. It's all nice and funny, but he ends up into walls a bit too often to my tastes. Moreover, it's getting too complicated to express his behaviour only through that state machine approach: only jumping takes up to 10 states if we want to remember the last direction Bilou has been leading to ^^"

One of the reason why it's getting so complicated is that when jumping forward, there are additional testpoints to be checked, while these testpoints are disabled when jumping simply "upwards". The solution would be relatively straightforward: i need a new class of testpoints that would be conditionnal on horizontal speed.
Une des raisons de toutes ces complications, c'est que lorsqu'il saute vers l'avant, Bilou doit vérifier deux testpoints de plus (pour éviter de rentrer dans un mur) mais que ces testpoints ne sont pas souhaités lorsque le saut est simplement vertical (sinon, on ne peut pas sauter le long d'un mur ^^"). La solution est relativement simple, heureusement, il suffit de créer une nouvelle "classe" de points-test qui ne serait active que lors d'un déplacement horizontal. Cyril avait bien proposé le passage à un moteur physique plus complet (avec gestion de la friction, de l'élasticité des chocs et tout -- vous savez comment est Cyril ;) mais je préfère garder ça au niveau du script pour l'instant.

Autre petite modif' en cours: varier les types de blocs possibles. C'est joli, un éditeur de niveau qui vous propose toutes sortes de pentes, de blocs réactifs et ce genre de choses, mais si le moteur de jeu voit juste "ciel" et "roc", on est pas avancé. Enfin, ça, ça peut prendre encore un moment.

Friday, May 02, 2008

*bounce*

Un nouveau pas important pour la réalisation de Bilou a été franchi: j'ai mon évaluateur d'expressions, et son intégration au moteur de jeu est quasi-parfaite. En clair, cela signifie que je peu maintenant indiquer via mon scripteur de niveau que lorsque Bilou arrive sur le sol, il doit:

  • rebondir si sa vitesse est trop élevée
  • s'arrêter si la vitesse n'est pas trop grande.
Avant la DS, j'avais essentiellement testé deux mode de développement de jeux: le BASIC et le GameMaker de Recreational Software.

Côté BASIC, c'était la liberté totale pour les algorithmes et les comportements des monstres, mais les possibilités graphiques restaient restreintes -- même sur Pentium '90. Côté GM, c'était le constat inverse: scrolling dans des niveaux relativement grands, un nombre d'animation et de monstre présent à l'écran quasi illimité (au point que Pascal s'est même servi de monstres pour faire les bonus de notre remake de Pop'n'Twinbee). En revanche, le GameMaker était totalement incapable de gérer un saut un peu potable. Le simple fait d'avoir un sprite différent pour la montée et la descente du saut était impossible. Alors faire faire une cabriolle au perso quand il arrive au sommet de sa parabole (comme dans Bilou sur Basic), vous pensez bien ^_^.

About 10 years ago, i decided that neither QuickBasic nor recreational software's GameMaker could still fullfil my needs for creating games. I wanted parallax scrolling, compound sprites (think of rayman, but i already had it for Bilou in QB), and most importantly, more flexibility in creating monsters attack patterns.
I don't want to have to program them at the lowest level of machine code, i want their behaviour to be part of the game data, not part of the game engine.
When restarting game development on the DS, i decided to opt for something that would be state-machine inspired, but capable of reacting to the level map, the hero's position, etc. How to actually make it working came up later while reading the "making of Another World" .

Bref, à part des fioritures comme le parallaxe ou des sprites composés (à la Rayman), mon projet "Ultimate Game Maker" devait surtout permettre une plus grande souplesse de programmabilité : un scripting des actions : ne pas se limiter à des transitions "d'un état à l'autre" pour la gestion des monstres, etc. mais permettre plusieurs transitions depuis un état en fonction de l'état actuel du monstre, et une modification de certains de ses paramètres. Comme vous pouvez le voir sur l'image, je vous concocte déjà un petit 'appleman' bien particulier grâce à ce nouveau mécanisme.

C'est en lisant un making of du jeu Another World que l'idée est revenue au premier plan pour la DS. Dans Another World, Eric Chahi a choisi de disposer d'un environnement de développement pour son jeu qui ne nécessite pas de recompilation entre deux tests successifs, mais aussi la possibilité d'écrire toute la logique du jeu indépendamment de la machine considérée. Le jeu étant développé sur Amiga500 et constitué exclusivement de polygones rendus 'à plat' (à partir d'image filmées, il n'y a donc aucun calcul 3D), il n'est pas possible de s'en tenir à un "BASIC" traditionnel, trop gourmand en temps de calcul. Eric nous concocte alors son propre petit langage, mélange d'assembleur et de BASIC (oui, quand-même) dans lequel il va exprimer toutes les réactions du jeu (genre "quand le laser entre en contact avec la base du rocher, le rocher se décroche et s'incline en oblique").

Aucun élément compliqué n'intervient (pas de chaînes, pas de structures de données complexes): Eric utilise uniquement 256 variables entières pour représenter l'état du jeu, et le plus souvent, il ne les nomme même pas.

Même le maniement du joystick est géré de cette manière.

Another World was coded by Eric Chahi on an Amiga 500. He wanted cross-platform game logic for a game whose logic is *much* more complex than a shoot'm'up, and he also wanted to avoid recompilations between two tests. He thus naturally opted for some scripting language -- a mix between BASIC and assembly -- through which he controls animations and game variables (just integers).
Well, that's more or less what i'll do for my sidescroller game engine, except that you'll have per-object (monster, hero, switch...) variable in addition to the global (per-level) variables. In my case, i can even make the scripting simpler than Eric's "bassemblic" as it will essentially be used in predicates and actions of a state machine. So no control flow is needed at all.

Eh bien, je me suis dirrigé dans la même direction avec mon Game Engine, mais en donnant plutôt 16 variables par personnage (oh, il y aura aussi des variables globales comme dans Another World, rassurez-vous). Je me suis évidemment inspiré de mon interpréteur WASP qui m'a valu mon doctorat pour le bytecode, mais en simplifiant encore un coup: il me faut des expressions, pas des programmes ici. Donc exit toutes structure if-then-else ou les boucles, qui peuvent se coder par la structure de la machine d'état. Reste à inclure quelques instructions supplémentaires (p.ex. créer un nouveau monstre ou jouer un son), mais la base est là, même si mon 'langage' est encore plus moche que le bassemblic de Eric Chahi.

# gestion de la chute de Bilou.
state4 :anim0 {
   using gravity
   testpoint off (4,16)
   testpoint off (12,16)
}

state5 :anim0 {
   using stopper
   testpoint on (4,16)
   testpoint on (12,16)
}
# si la vitesse est trop élevée, on rebondit en la réduisant de moitié.
state4->state4 on fail [v1 $40 >=] (v1 2 / ~ :1)
state4->state5 on fail [t]


En avant. Essayons de voir si j'arrive à fixer la caméra sur Bilou pour le promener dans le niveau. Et en même temps, je vais tenter de faire un petit .nds de démo pour que vous puissiez tester ça sans devoir comprendre le fonctionnement de mon 'runme'.

2024 #choice reality check: those scripted state machines have their good side but also their downsides. Over 15 years later, I still don't have any DS editor for them because they rely more and more on CPP macros to make them "easier to write", although that's not perfect either. And they make bugs complicated to fix as soon as they mix scripted expressions and sophisticated C++ algorithm like with slopes.

Tuesday, April 29, 2008

GameObject : public Animator

Bin voilà. Hier soir j'ai fait le point sur tout ce qui tourne autour de ma classe GameObject qui assure la gestion des monstres et compagnie. C'est déjà un peu le méli-mélo, et je dois rajouter là-dessus des objets multi-sprites (pensez à une chenille, et vous êtes dans le bon) et surtout les contrôleurs, capables d'influencer le comportement des monstres en assurant ce que les petits scripts et les machines d'état ne sauraient pas proposer.

Je m'explique.

Prenez un sprite comme le petit ver. Il a principalement deux états (avancer vers la gauche ou avancer vers la droite). Chacun de ces états est lié à une animation (qui entre-autres le fait bouger) et définit les conditions sur les testpoints nécessaires pour rester dans cet état (pas de mur devant lui, et du sol en-dessous). Lorsqu'un des testpoints n'est plus bon, on cherche une transition à appliquer pour passer dans un nouvel état (en l'occurence, il y a une seule transition, vers l'autre état).

En gros, c'est super-simple.

I shoot a picture of my attempt to summarize the OO design of my GameObject class (later on called "GOB" through code and blog) and everything sitting around. Let's see that with a simple example such as the WoodWorm, that simply move forward and backward on platform. The worm has mainly two states depending on its direction. Each state has an associated animation and defines conditions expressed on testpoints that must be met so that we can remain in that state. Namely, when moving to the right, the worm must have some ground behind him and no wall in front of him. When testpoints are no longer valids, we look for a transition that will lead us towards a new (valid) state.

L'appleman, lui, est un peu plus sophistiqué: il se balade également au sol (avec les 2 même états), mais lorsqu'il arrive au bout d'une branche, il peut décider de sauter au sol pour attaquer Bilou si

  1. Bilou est en-dessous de lui
  2. Bilou n'est pas trop loin.
Le contrôleur, dans le cas de l'appleman, est passif: il se contente de lui rapporter sa position relative par rapport à sa "cible" (Bilou) et c'est via les prédicats des transitions (que je n'ai pas encore implémentés :P ) que la décision de faire demi-tour ou sauter se prendra.

More sophisticated GOBs, such as the Appleman, have a behaviour that does not depend only from test points. When reaching the edge of a cliff/platform, the Appleman will either turn back if Bilou is not in its attack range or jump off if Bilou sits below him in attack range. I intend to build that by means of controllers: C++ objects attached to a state that affect either directly (altering speed) or indirectly (indicating the presence of Bilou) the behaviour of GOBs. Even reading the Dpad and moving the hero accordingly can be achieved by a controller.
This should give us flexibility while reducing the amount of CPU power thrown at interpreting scripts.

Si on prend maintenant notre copain AppleBat, il dépend aussi de la position de Bilou pour son déplacement, mais il accélère et suit Bilou. Pour faire ça proprement, je vais laisser à son contrôleur le soin d'ajuster sa vitesse automatiquement pour se dirriger vers la cible. Tout ce que le script aura alors à prendre en compte, c'est le choix de l'image utilisée (pour que l'applebat continue à regarder vers Bilou quand il le dépasse).

Le reste, à mon avis, c'est du détail de bricolage de colle, donc je vous laisse le soin de déchiffrer mes gribouillis en cliquant sur les images si ça vous intéresse.

En d'autres temps, d'autres lieu, tout ça aurait pu faire une bonne autre lettre à Gédéon ;)

edit: partly obsoleted by this post and that post.