Wednesday, February 12, 2020

Nouveau Gimp

Je n'ai malheureusement pas réussi à ce que Gimp conserve ses anciens paramètres lors de l'installation de mon nouveau PC de bureau. J'ai même été franchement contrarié de les voir passer à une interface "tout intégrée en une seule fenêtre" comme le "nouveau" lecteur PDF d'adobe (eh non, plus moyen de continuer à utiliser Acrobat XI un peu plus longtemps). Mais au moins, chez gimp, on a une cochette [x] Single Window Mode pour si on veut utiliser une image de référence sur un écran pendant qu'on bidouille ses pixels-à-soi sur l'écran d'à côté.

Restons confiant, donc. Après tout, j'ai aussi pu remettre mon 'F12 = snapshot" en cherchant 'screen' dans Edit > Keyboard Shortcuts. (enfin, il fallait quand-même d'abord activer [x] Use dynamic keyboard shortcuts dans Edit > Preferences).

Je n'étais pas franchement convaincu par l'interface revisitée pour changer la taille du pinceau/crayon. 'faut dire que je suis plus souvent entre 1 et 4 pixels qu'entre 20 et 800, personnellement. La 'nouvelle' interface sépare chaque barre en deux zones, une qui permet un règlage absolu (sur le dessus de la barre. Le curseur souris devient une flèche verticale) ou relatif (sur le dessous de la barre. Le curseur ressemble à "changer la taille des colonne"). Mais ni l'un ni l'autre n'offre la vitesse de réaction que j'attends de mon outil.

Je sais que j'ai déjà changé ça: j'avais remappé CTRL+SHIFT+roulette, qui par défaut change la 'forme' du pinceau (ovale vertical -> cercle -> ovale horizontal). J'ai juste oublié comment j'avais fait.

5-10 minutes d'exploration des préférences. Toujours rien. Je cale. Je finis par refaire une recherche et tomber sur la vidéo-tuto où j'avais probablement trouvé le truc la première fois. Je n'étais pas loin: il faut bien aller dans 'input controllers' (logique), mais ce n'est pas un clic droit qui me proposera de configurer les choses. Un bon vieux double-clic-qu'on-avait-oublié-depuis-l'ipad.

J'ai pris tool-increase-size-relative, mais je ne suis pas 100% convaincu. J'aurais aimé pouvoir passer de 1x1-2x2-3x3-4x4-5x5 à 6x6, au lieu de quoi j'ai le choix entre 1x1-2x2-4x4-8x8-... et 1x1-3x3-3x3-3x3-5x5-5x5-5x5-7x7-...
Bah. ça fera l'affaire au bureau, j'imagine.

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.

Monday, February 10, 2020

GameEngine design: script-to-code

Somewhere in 2009, I added support for actions triggered by the script-part of my game engine but implemented in C++. This is the cornerstone of "GEDS" game engine. This is how run-time monsters spawning and sound effects work. This is also how level load request are processed.

The design survived the years, so let's see how responsibilities are split.

  • Anything that will start invoking code upon game event (collision, new input, animation completion, etc.) is captured in a *Gun instance. The instance carries all the parameter needed for firing a specific kind of code. E.g. An instance of the LevelGun capture the specific level script to load. An instance of GobGun captures which type of monster to spawn, etc.
  • Configuring the *Gun instance happens at script parsing time. In other terms, most of the *Guns could actually be constant objects.
  • The c++ code instanciating an *Gun must be located from the name of the action on the script (with a using [action-name] ([arguments]) as [holster-slot-number] statement). That requires every type of "gun" to have its associated "factory" class, so that an instance of each "factory" can be registered into an std::map to be the link between actionname and the *Gun constructor.
  • Amount of support code for a new *Gun or a new *GunFactory is minimal. Each class have only one (virtual) method, either create() or shoot().
  • *Gun instance typically forget about what they 'shot' as soon as shooting is done. Often, the created object is registered as an animated item at the game engine, which will take care of running its code periodically. This may lead to the need for an additional "progress" object when there isn't any yet, like with the TrackSequence following the instructions (e.g. shooting more guns) as a sound track unrolls in the music player.
  • when processing script expressions, a palette of "guns" (the multi-slot holster, if you want) is received in addition to the set of variables accessible to the script. However, the transitions between states are the one capturing those palettes.
  • The GameScript is responsible for recording every *Gun instance created, so that they can all be reclaimed when the level is destroyed and we switch to something new.
  • There is one guns palette per "state machine file", with the constraint that all the states used by a single "character" in the game have to be described within the same file, this means that a "character" can only use up to 16 different sounds+special effects.
  • The GameScript has the ownership of all these palettes. Reusing guns from parent's palette is explicitly requested. The GameScript knows when to stop using a given palette for new transition and switch to a new/old one because it has seen input/end statements. There is room for improvement here.
Lots of guns. But anyone in GEDS can at most carry 16 of them at a time.
Clearly, the major limit comes from fixed-size palettes of gun, which itself comes from the byte-code nature of the (parsed) script expression where only 4 bits are dedicated to the storage of which-gun-to-use. This is perfectly ok for a Mario-like game, where the distinct number of actions per character remains low. I would definitely need to extend this (and other parts of the game engine like sprite memory management) if I was to create a run-and-gun game where you can pick up lots of different weapons, each requiring a different kind of amno sprite and sound effect.

Sunday, February 09, 2020

Thank you, Fabien

It's a true pleasure to keep reading the reports of Fabien Sanglard about the "polygons of Another World", one system after another. He went for people who did the ports, like a great investigator from Pix'n'Love would and mix those live coding moments with technical details about the various 16-bit architectures that make it quite exciting and pleasant to discover.

Of course, there's already a significant amount of things I have learnt about the bowels of Another World: because Eric Chahi himself documented them on his website. Like the language used for game logic, the development of the polygon modelisation tool and so on. That makes it (imho) perfectly for Fabien to focus on getting high-performance drawing primitives. But if you really want to, he covered the virtual machine source code back in 2011. 

For instance the episode about the homebrew GBA port finally allowed me to understand why the heck there were thumb-vs-ARM modes and IWRAM-vs-EWRAM and things alike in gba coding. With a simple diagram of how chips and busses were connected. That could definitely help if I was to port some Bilou games to the GBA later on (likely I'll take closer attention to make that possible with "Bilou Dream Land"). But I enjoyed the description of how the Amiga used its blitter to fill polygons quickly, too. and how the SNES version could have been running at 60 fps if the manager had said yes to a SuperFX chip on the cartridge.


Thursday, February 06, 2020

Attempting to reference a deleted function

Now, all of sudden, the C++ compiler decides to make copies of my exception class before throwing it. Well, apparently, it is allowed to do so. But for some other reason, that very same C++ compiler (wasn't gcc this time) decided that my classes shouldn't receive a default copy constructor, and complains that "my" code is "attemtpy to reference a deleted function" when throwing the exception.

Oh well.

Most of the code isn't new, and it has compiled with this very compiler with these very flags just an hour ago. But while working on it, I suddenly changed at least one thing: I made it so that all the arguments of the constructor for the base class (the one complaining) are now constants. I suspect that Mr-Smart-C++ decided that my code would be happy to have one CustomException in .rodata with the corresponding fields compile-time-ready and that it would then copy that into the area allocated for ExtendedException (child class) instead of "constructing" a new one from scratch.

Now, why wouldn't it create a default copy constructor ? Well, I introduced something new in the class, that is a std::stringstream. And it turns out this one has a 'deleted' copy constructor... this apparently propagates and makes the containing class considering it should flag the copy constructor as deleted as well.

Just when you thought you had mastered the language. Welcome to Wonderland ...

Sunday, February 02, 2020

Bilou Dream Land (codename)

Bon, j'ai plus ou moins déjà présenté l'idée, mais comme c'est un peu perdu au milieu d'autre chose je remets le couvert. J'ai décidé de mon prochain projet de jeu. Il s'agit toujours d'un jeu de plate-forme, et toujours aussi dans l'univers de Bilou. Mais ce ne sera pas "infinite Pyramid". Pas encore.

My 'todo map' for 2019 was trying to review all the things that could possibly use a bit of work, but had no clear direction of where to go.  For 2020, I finally have a direction to follow: a new game code-named "Bilou Dream Land" so far.
  • Ce sera une version "courte" de Bilou's Adventure avec un début et une fin. Plus de gameplay en boucle cette fois-ci.
  • On y parcourra la Green Zone, la School Zone, la Desert Zone et probablement la Peaks Zone.
  • Les zones seront plus courtes que prévues pour Bilou's Adventure: plutôt la taille d'un niveau de Kirby's DreamLand (d'où le nom de code) que celle d'un monde de SMB. (ça fait quand même l'équivalent 2 à 3 niveaux de la version BASIC de Bilou's Adventure).
  • Dans la mesure du possible, je reprendrai les level design d'origine, avec des adaptations pour corriger les 'injustices', mais je m'autorise à avoir des portes-magiques sans devoir les justifier.
  • J'essaierai d'y introduire des boss.
  • Pas de pouvoirs 'définitifs' à la Rayman, mais des power-ups comme ceux qu'on a pu voir dans School Rush, probablement avec un nouveau-venu qui fait grappin
  • Les PNJs de la BD pourraient bien servir de "pouvoirs spéciaux" liés à une zone. Napin qui creuse, Froggy qui nage... vous voyez l'idée.
The idea is to pick four environments from Bilou's Adventure and use them to create a platformer that has the length, the rhythm and the depth of the seminal Kirby's Dreamland. I'd reuse existing level design as much as possible, like the 'anniversary School Zone' map and the sketched maps for Green Zone.

If we think of Bilou's Adventure as a Cave Story-like adventure with multiple endings based on player's in-game choices and actions, then Bilou Dream Land would be the set of levels you'd play if you'd go for an any-%, going for the quickest-to-reach ending where Bilou and Bouli barely managed to escape the planet, not trying to investigate anything about why they crashed and whether that world needs to be saved.

There'd be some fixing on the old maps, possibly some keys or switches by NPCs, but that shouldn't be as ambitious as The Big Green Zone Relooking I've been talking about 3 years ago. The aim would rather to make the game more fun to play, and clearly not to get rid of any magic door.

Saturday, February 01, 2020

DoSlopes

Ok. Ajouter des pentes dans un jeu de plate-forme, ça devrait ne pas être compliqué, mais dès qu'on modélise son niveau à l'aide de dalles pré-définies (toujours les bons vieux tiles), ça cesse d'être simple. J'en ai déjà beaucoup parlé (cf. le tag) mais sans jamais vraiment (reality check: si) présenter la solution que j'utilise depuis Apple Assault: la fonction doslopes.

L'algorithme derrière doslopes travaille uniquement avec le 'hotspot' des sprites, et veille à garder ce point en contact avec le sol tout en appliquant un déplacement horizontal donné (qui peut s'étendre sur plusieurs tiles). Il utilise aussi une fonction annexe -- groundheight(x,y) -- qui donne la hauteur du sol pour un tile du niveau à partir de la base de ce tile. On y reviendra.

L'algorithme est constitué d'une boucle optionnelle (en jaune) pour atteindre le tile de destination puis une phase d'affinage (en vert) qui calcule la position du sol pour la position horizontale voulue.
En sortie, on obtiendra un  déplacement vertical correspondant au déplacement horizontal donné. A chaque déplacement intermédiaire, doslopes s'assure que l'on est pas en train de s'envoyer dans un mur à l'aide de la fonction cando -- la base de la gestion des interactions sprites/niveau.


Le principe de base, c'est de regarder la hauteur du dernier pixel d'un tile pentu juste avant de le quitter. Il peut avoir une 'hauteur = -8' ce qui fait positionne le joueur juste au-dessus du tile (comme on le fait normalement pour du sol) et permet du coup de regarder le bon tile pour la suite de la pente au tour suivant.