Wednesday, January 31, 2024

Hello 2024

Hey! The blog has finally reached over 1M views before it turned 20 years old! Not that it means much since most of the hits are likely scans from automated tools like the Internet Archive (or search engines Google doesn't know about). But yet, it is nice. I totally missed the opportunity to release something significant for the 30th anniversary of Bilou, but it has been an inspiring year and I haven't dropped the dream of a more elaborate game. So Happy new year ;)

Monday, January 29, 2024

Over-engineered ?

Maybe I'm starting that "swim made fun" thing by the wrong end. What I really need is to be able to move Bilou through water and to get him out of the water. Sure it is fun to swim in Rayman Origins, it was pleasant in DKC and a nightmare in TNMT on NES. But unlike all these games, I have no "water level" planned in Bilou: Dreamlands. The reason why I started working on dash-in-water is not because travelling through water would feel empty without it. It isn't even because I have level sketches where jumping out of water is a key mechanics. It is solely because in the current demo, you can't reach the green room exit unless you trigger an unintended double-jump with precise button mashing that should be left to speedrunners.

J'avais donc un beau projet pour les mouvements dans l'eau. Je venais de commencer à ajouter "on accélère si on appuie sur JUMP" et J.L.N a pris "la manette" en main. Il a nagé à droite. Il a nagé à gauche, puis il a voulu sortir de l'eau. Et bien sûr, lui n'a pas attendu que Bilou revienne à l'état "je patauge en attendant" avant de presser le bouton de saut: il l'a fait depuis l'état "bilou nage à gauche", là où j'avais prévu un "fonce dans l'eau vers la gauche" en réaction au bouton "pied"... Bilou n'a donc même pas tenté de s'élever d'un pixel. J'avais échoué le test n°1 de Miyamoto: "the JUMP button makes Jumpman jump". Et donc je me suis enfin posé la bonne question: "Est-ce que j'ai vraiment besoin de tout ça, moi?"

Parce que dans mon cahier bleu, avec les croquis de level design que j'ai l'intention d'utiliser pour Bilou's Dreamland, l'eau est bien présente, mais je n'ai jamais un "niveau aquatique" à proprement parler. Ce qui motive cette recherche sur les mouvements dans l'eau, c'est juste le fait que la berge est trop haute dans la démo. Et donc, garder une mécanique simple "JUMP" à la surface = sauter hors de l'eau et sauter hors de l'eau près de la berge = sortir de l'eau serait impeccable si j'éditais un rien l'écran-démo pour que sa berge soit un tile plus bas ^^" (et éventuellement que j'augmente de 15% la hauteur du saut depuis l'eau, actuellement défini au doigt mouillé)  

I wanted to use the opportunity of changing the level map to also fix an issue I had with the interaction of surface-swimming and following the water flow. I presume that the core of the issue is that the *middle* of the character's bounding box is used to determine whether there's a flow to follow. But "stay at the surface" code maintains Bilou above the flowing tiles. As soon as one pixel of the box enters the "full water, flowing downwards" tile, it starts pushing Bilou upwards.

I made a few bad moves, like trying to have full-water-flow-downwards tiles higher (closer to the surface), but that didn't work at all. Instructing the level that "water-flowing-downwards" can be felt through did work pretty well, I think.

Pendant que je testais cette nouvelle map avec sa nouvelle "berge" moins haute, je me suis rendu compte que depuis qu'on sait nager à gauche et à droite, les remous de la cascade ne nous entrainent plus vers le fond. La flottaison est trop efficace et le centre de Bilou n'entre plus dans la zone qui le tire vers le bas. J'ai un peu chipoté, mais heureusement, il suffisait de permettre à Bilou de "tomber" aussi dans cette zone pour que tout rentre dans l'ordre (enfin, je pense)

Tuesday, January 23, 2024

Input Buffering.

Tout est parti d'un tweet de Case Portman, auteur du jeu "Flynn, Son of Crismon" dont je suivais le développement dans lequel il nous expliquait en quoi le "jump buffering" est fondamental dans tout jeu de plate-forme. Un nom un peu barbare pour les francophones qui consiste plus ou moins à faire du voyage temporel avec votre manette de jeu.

Quand le joueur est au sol et qu'il fait sauter son personnage, on s'attend à ce que le saut soit pris en compte à la frame exacte où le bouton est enfoncé. Pas trop difficile. Supposons que le joueur veuille rebondir sur le sol en finissant un saut. Il sera assez fréquent qu'il appuie sur le bouton de saut légèrement avant de toucher véritablement le sol. Son intention est de rebondir. Le code peut faire deux choses: soit ignorer la demande de saut vu qu'on est en l'air, soit effectuer un saut dès que l'on atteint le sol. Et clairement, les jeux qui suivent la première approche sont désagréables pour les joueurs.

In my notebook for 2022-2023, I had a small page about Case Portman's post on Jump Buffering (link recovered, thanks, search Engine :P), where the author of "Flynn, Son of Crismon" explains:

It's a nifty little feature that will add *a lot* more flow to your game. This can also be applied to almost any action! Shooting, melee attacks, dodge rolls, even menu selection.

Don't let the name frighten you: what it truly means is that you're time-traveling with your  DPAD, pretending than your jump button presses did not happen when they did. If time was a photoshop/gimp layer, then jump buffering would be something like "snap to guide". He went on with a detailed and beautiful gif animation showing how to achieve that and make the game more tolerant on the case where player is about to land on ground and press the jump button in one of the last few frames mid-air. Many 8-bit games failed to do so and feel unfair when played.

Dans mon moteur de jeu, tout celà est pris en compte par le contrôleur "dpad". Pour chacun des boutons, ce contrôleur retient si le bouton était déjà enfoncé lors de la frame précédente, et pendant combien de temps encore il doit être considéré enfoncé. La fenêtre de temps autorisée (6 frames, soit 1/10eme de seconde) sera la même pour tous les boutons. Je me suis refait un petit schéma de la manière dont c'était géré parce que je l'avais complètement perdu de vue. "to[i]" sur ce schéma, c'est le timeout associé à un bouton donné.

I realised then that how my engine does this slipped out of my brain: it was about time to document it and blog it. It all happens in the DPAD controller, where a counter will be started if a "new press" is detected and reset if the button is released. The counter is decremented every frame while you're holding the button. But earlier tests in SchoolRush shown that this is not enough. In some part of the character behaviour, we actually *must* indicate directly whether the button is still pressed. Without that, there's no such thing as "keep jumping higher as long as player holds JUMP button" or "keep floating as long as player holds JUMP button". My engine handles that with a bitmask that indicates which button should report "pressed" instead of "held".

Par contre, on peut définir état par état quels sont les boutons pour lesquels on veut un mécanisme du type "enfoncé il y a au plus 1/10eme de seconde" et ceux pour lesquels on veut "maintenu enfoncé depuis aussi longtemps  qu'on veut. 

Quand on définit "MOVE + FOOT" pour l'état "flotter" (dans les airs, façon super-cape de SMW), ça signifie que le DPAD et les boutons de sauts ne passeront jamais par "decrease timeout". l'état "chevauchée d'éponge" évitera quand à lui de faire expirer "MOVE + HANDS". Et pour tous les états où on a rien précisé, c'est le DPAD et la gâchette droite (qui sert de déclencheur pour la course) qui seront maintenus.

But watch the catch: while the controller code defaults to 'held', the script instead indicates which inputs are expected to be "held", and defautls to "press" ... that's why you see 'using dpad(MOVE|FOOT)' for the FLOAT state, where it prevents the press to expire while floating.

Monday, January 22, 2024

How long before we play ?

Somebody asked The Question: how long before we can play the game (presumably Dreamland). So I picked my secret blue notebook and for every level, I tried to identify what I'm still missing to let you play it. Each of the checkboxes cost about 1 month of hobby time. maybe. I won't try being a project manager here. Not during hobby time. Oh, and blue boxes cost more. Possibly up to 3 months.

Je n'ai pas vraiment envie de jouer au gestionnaire de projet pendant mes temps libres. Mais voilà, la question a été posée: "dans combien de temps est-ce qu'on pourra jouer" (à Bilou Dreamland, je présume). Alors j'ai repris mon cahier bleu, celui des secrets, des niveaux et des idées de gamedesign. Et pour chaque niveau déjà esquissé, j'ai fait l'inventaire de ce qui me manquait déjà. Pour les 3 premiers mondes prévus pour le jeu. Je n'ai encore aucun niveau pour le 4eme (ni aucun pixel, d'ailleurs). Chaque case à cocher sur cette map géante, c'est probablement 1 mois de hobby. Probablement 3 pour les cases rehaussées de bleu. Faites le compte ;-)

edit: avant la prochaine démo,
  • [done] faliciter la sortie de l'eau
  • [done] animer les chutes et les pentes de sable
  • [done] Appleman en CompoundGob

Mais la bonne nouvelle, c'est que certaines de ces cases ne sont pas indispensables pour avoir une version "jouable", c'est à dire où on puisse atteindre la fin des niveaux. Les PNJ, par exemple, sont supposés être optionnels tout comme Yoshi était optionnel. Parfois, une substitution par un élément déjà existant pourrait s'envisager. Certains niveaux de la school zone seraient mieux avec des lattes en guise de tremplin, mais déjà jouables en utilisant les gommes rebondissantes de School Rush, etc.

Sunday, January 21, 2024

De l'eau par ci, de l'eau par là ...

Tentons donc de nager. La première chose serait de pouvoir stabiliser Bilou à la surface sans l'immobiliser pour autant. Ce n'est pas impossible parce que j'ai une bande de 2 tiles à la surface qui est à la fois de l'air et de l'eau. Une ruse pour que Bilou-qui-coule puisse succéder à Bilou-qui-tombait et que la dernière position valide pour l'un soit aussi une position valide pour l'autre. Bilou-qui-flotte à la surface devrait donc éviter d'entrer dans une zone qui n'est que de l'eau ou dans une zone qui n'est que de l'air ? Il lui faudrait pour ça un contrôleur dédié ?

props: 0fc8
props: 8fc4
props: 8fcc
props: 8fcc
using swim
using gravity
using gravity
using float (?)

So, switching between "fall down" and "pushed up by the water" is not quite satisfying to emulate "floating at the surface". Partly because that means every transition between the two resets the animation. Mostly because how leaving water is notified and how that prevents DPAD events to be notified, making things like 'jump out' or 'switch to swim-left' impossible. So I spent time now and then during the week to figure out what I should do to get the desired effect. I do have a row of tiles that are both air and water... could it be enough to try and confine Bilou there when he's floating ? That wouldn't be impossible, but Bilou is 13 pixels high and the row only 16. Not much headroom ...

Mais en fait, ça s'est avéré encore plus simple que ça. J'avais entre-temps réalisé qu'il serait peut-être suffisant de faire tomber bilou-qui-flotte s'il est tout entier dans la bande de surface et de le faire monter s'il est bas dans cette bande ou s'il a commencé à s'enfoncer dans l'eau. Mais en testant, il s'avère que juste "tirer vers le bas dans la bande-surface, pousser vers le haut si on est ne serait-ce que d'un pixel dans la partie que-de-l'eau" donne le résultat qui me convient. Les mouvements de Bilou sont souples et amples, son animation n'est pas interrompue inutilement et sa position est pertinente par rapport aux graphismes.

Hopefully, it turns out that I only need that area for the 'pull down' part of the move. Pushing up can happen anywhere in water because this is the default behaviour in water. I had plan for an extra "surface position" parameter in swim controller instead of a new float controller, but it turned out that was useless as well because the default position (where Bilou is completely in the 'both' row) is just what I need given current graphics.


 

That seemed proper time to try the new 'swim right' and 'swim left' animations inspired by Fury of the Furries. But there I got annoyed again by how other controllers "steal" the focus of an earlier event. This time, the DPAD event notifying that I've pressed LEFT got replaced by an event mentioning that speed changed direction. It did not happen when pressing RIGHT because positive speed to the right and null speed at rest appear to have the same sign.

I cannot possibly express how satisfying it is for me to see that little animation coming to life and respond to my left/right/left/right keypresses. I guess I'm glad I kept up with gamedev so far ^_^

Wednesday, January 10, 2024

Nager mieux

I like to release a demo on special occasions like new year or my birthday. It is twice unfortunate that I had none to show by the end of 2023 since it was the 30th anniversary of Bilou's original design. But there was something still lagging behind: for an untrained player, it takes countless trials to get out of the water in the green room. When the season holidays started, I thought I could just change the state machine a little bit so that Bilou would jump out of water if we hold the FOOT button.

Il y a un élément qui m'empêche encore de faire une nouvelle démo avec les améliorations de l'an dernier: c'est difficile de sortir de l'eau. Je m'en suis rendu véritablement compte en laissant un peu la démo actuelle entre les doigts de J.L.N ... Je m'étais donc donné la mission d'essayer de faire fonctionner ça pendant le congé, mais avant même le premier essai, j'ai compris que ça n'irais pas comme je voudrais. Parce que pour qu'on puisse "jaillir de l'eau si on a appuyé sur le bouton de saut près de la surface", j'ai besoin de pouvoir mémoriser que ce bouton a été enfoncé. 

It didn't work as I expected though. Maybe it could be adjusted with some cleaner input buffering, but since I intend to use some different SWIM mechanics to help the game being fun, it seems silly to hack something else first. So I went on, picked my animation editor and started crafting swim left, right and up animations, each split so that they player would have to chain button pressing to reach full speed and Bilou could come back to some "rest position" otherwise.

ça pourrait être réglé avec une peu de tuning sur l'input buffering, j'imagine. Mais comme j'ai aussi prévu de passer à un autre système de nage dans lequel Bilou reste dans l'équivalent d'un dash sous-marin pendant quelques frames quand on a appuyé sur "pieds", ce serait naturel que l'on jaillisse automatiquement si on est dans cette phase de dash. J'ai donc passé deux petites soirées à faire des animations dans MEDS pour que notre brave Bilou brasse mieux.

Manque de pot, une animation supplémentaire s'est invitée dans le fichier. J'ai prévu 4 "pages" d'animations pour bilou.spr, mais cette nouvelle animation est sur la page 7, décalant toutes les nouvelles animations

- code avant la nage avec les sprites avant la nage:
  - démarrer la pyramide: ok
  - passer de la pyramide à l'école: ok
  - sortie de l'école: gros crash.

Une petite modif' plus tard (là, ce soir) pour éviter que l'animation excédentaire soit à la fois sous le contrôle du jeu et sous le contrôle du reste (sinon, ça fout un chaos digne de Jurassic Park dans le gestionnaire de mémoire) et j'ai de quoi commencer à utiliser toutes ces jolies nouvelles animations. Sauf que ça fait bizarre de voir Bilou essayer de rejoindre la surface la bouche grande ouverte (animation "super jump" recyclée) puis fermer la bouche une fois qu'il arrive à l'air libre :-P Edit et il va falloir que j'en fasse une ou deux de plus pour permettre au joueur de quand-même avancer. Sur base des animations de Fury ? Pourquoi pas ...

But unfortunately, some bits got twisted, and I ended up with an undesired 'temporary' animation stuck in the bilou.spr file. Then I picked the wrong decision of ignoring it while loading rather than fixing the editor and saving the file again. It worked when I tried the animations in the green room, but when I tried to 'move' between rooms later on, everything blew up. I had HDMA experiments catching my attention, so I was all out of holidays when I finally understood why it broke and how to fix it. Sounds like you'll see the swimming another time ^^"
Edit: one more thing ... Dash-swimming might be fun, but I also need something to use when player just navigates with the DPAD. Maybe Fury's swim sheet will be the template I need for that ...

Saturday, January 06, 2024

Raycasting ?

There's an awesome effect one can achieve once you have HDMA in a platformer: add depth to your background with "floors" like in DKC2 Lava lagoon. There will be complete occlusion of the 'far' background plane (wall) by the 'near' background plane (floor side), so having them scroll at different speed is 'just' a matter of updating X scrolling register at the right time. Having the floor positioned above or below the "seam line" between two walls is 'just' a matter of adjusting Y scrolling register earlier or later so that they 'skip' part of the far background art to show front background art.

The awesome part is that depth effect added with the horizontal planes, of course. That is about adjusting both X and Y scrolling registers and that will need maths to explain properly, using some pre-slanted texture as shown in the ripped contents of DKC2.

First immediate thought after I realised that it could be more than welcome in the pyramid level of Bilou's Dreamland was that it is actually an instance of the raycasting algorithm. Yeah, that infamous thing that turned ID software away from my beloved Commander Keen and sent them trashing the whole era of 2D platformers with first-person shooters. No wonder why, despite my affinty for maths and software optimization, I never ever felt tempted to code one myself. But hey, do it vertically instead of horizontally, and this is precisely what we need to decide whether to show floor, ceiling, wall or cut-through floor-to-ceiling structure.

So before trying to reconstruct the algorihtm that might have been used on SNES, let's see what it would cost us.
  • the background scene must be structured along a 2D grid. I'd say 64x64 pixels would be fine by me
  • For every scanline , we will have to step through the grid, one tile at a time, essentially checking whether one more "depth" step takes us farther than one "height" step in our case.
  • We need a direction vector associated with every scanline. That implies 192 square roots per frame, but hopefully, those computations are the same for every frame: they depend on the camera-to-screen distance and define angle used to trace through each pixel. They could be pre-computed even at compile time and stored in a look-up table.

So for every scanline Ys of the screen, (DDA) raycasting will give us a pair of (Zw, Yw) coordinates in the world that is shown on that scanline. Most likely, we don't want to compute the distance to Zw, Yw nor use that distance to adjust scrolling speed. Instead, Zw should directly be used to decide the scrolling speed. Well, unless we're on a horizontal surface, that is. Well, I can't help thinking this is overgeneral and overkill, despite übercool.

The trick in the SNES DKC2 implementation is that we have pre-rendered floors and ceilings. They already feature some depth-of-field effect. if you use them as-is. And being 440px wide, they're significantly larger than the screen (256 pixels iirc). Where does that 440 value come from ? Well, I guess this is screen_width + pattern_width, as the flat stripe shows a repeating pattern of 184 pixels. So whereever you are in the pattern, you can always have at least one full scanline ahead. The most distant line of the ceiling has only 96 pixels between two patterns, matching exactly the size of the tiled background wall. That means if the 'front' part is moving exactly at 1 pixel / frame, the tiled part should be moving at 0.52 pixel/frame so that Thales theorem is satisfied.

That size difference also tell us how far away the tiled parallax layer should be from the 184x32 parallax layer (and thus how deep the floor/ceiling objects are): they're as far from each other as the 184x32 layer is from the "camera"

  s16 xref = REG_BG1HOFS;
  s16 yref = (offset >> 2) % 192;
  s16 xamp = xref + xref / 2;
  s16 yamp = yref + yref / 2;
  int ytrigger = 224 - yamp;
  int btrigger = 192 - yref;
  int i, j;
  for (i = 0, j = 0; j < N; j++, i += 2) {
    if (j > ytrigger && j < ytrigger + 64 
|| j > ytrigger + 256) { data[i] = xamp; data[i+1] = yamp + 30; } else { data[i] = xref; data[i+1] = yref + (j >= btrigger ? 128 : 64); } }

That doesn't make the 3D-effect of DKC yet, but at least it gets me synchronous parallax with a single hardware layer.

Next step: find the zones where ceiling and floor should be shown. And there, trials and errors became too complicated to figure out. Hopefully, I found a way to analyze the problem with maths. Most of what's computed is derived from that "yref" value, which is normally the input from camera position. What I need to do is use that yref as horizontal axis and study how "triggers" that define top or bottom of areas evolve, cross and areas overlap appear or disappear. And once the (simple) maths were written, it took only half an hour to write the code to do it right.

With this graphics, that's the best I can do... let's see how it follows up once I have dedicated background.

Oh, and while I'm at it, there's a stunning online tool out there, to visit every map of every DKC SNES game: the DKC-atlas.