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.

No comments: