mercredi, décembre 31, 2014

Ciao 2K14.

Pour ce qui est du blog, 2014 fut certainement une année un peu maigre. Inévitable, j'imagine, suite à mon changement de boulot, et surtout la contraction du temps de midi. C'est un boulot qui me plaît, avec des registres, des modules kernel et des transferts DMA, ce qui veut dire aussi que je suis moins en manque de code à la fin de la journée de travail.

I hope you haven't been deceived by the lowered posting rate in this year. I changed to a job with more registers, more kernel drivers and DMA transfers. The environment gives less room for lunchtime-hobby-coding, but I enjoy it a lot. Hopefully, the list of things to adapt to have daily tasks flowing in this new chapter of life reaches completion.

Du point de vue de l'aventure de Bilou, toute l'année aura tourné autour de la transformation -- sur la proposition de Pierrick -- d'un niveau d'essai en jeu d'arcade du type "cours vite!" avec l'encre qui monte. J'ai eu le feedback de Kirby Kid qui a attiré mon attention sur l'importance d'un règlage plus fin des paramètres pour ajuster les monstres et les niveaux.
J'atteins aussi des interactions nettement plus sophistiquées (enfin ;) notamment avec les dumbladors empilables sur lesquels on peut grimper. 

Bilou's anniversary level has been converted into an "arcade" game currently featuring 3 levels, with a target, a challenge and a "game over". I got precious feedback from gameplay expert Richard "Kirby Kid" Terrel. I'm doing my best to proceed with tuning issues he pointed out. I'd like to have a 4th level before I come with a neoflash release (if neoflash ever resume doing coding competitions). Rushing pendats, stacking bladors, stunned inkjets, raising ink swamp, floating spongebops ... Nothing like "deep ink pit" has appeared, but things are definitely shaping towards something that better fits the levels I designed for the School Zone.

Il n'y aura pas eu beaucoup de progrès du côté des éditeurs -- la panne de mon cybook au début de l'été n'y est certainement pas étrangère. LEDS s'en tire un peu mieux avec un mode de prévisualisation sympa pour les blocs spéciaux mais qui, en pratique, est peu utilisé (les descripteurs de blocs ne sont pas présent dans tous les niveaux :P). Le système de "copier/coller" d'écrans entier s'est avéré bien utile pour les nouveaux niveaux de SchoolRush. Par contre, le mécanisme prévu pour éliminer les objets hors-cadre lors d'un redimensionnement de niveau n'est arrivé nulle part.

This time, there hasn't been much evolution to my edition tools, I'm afraid. They barely adapted to the new needs identified for the game conversion. I reached a data size where WiFi transfer are impractical and I use the emulator much more than earlier in the project. RunME isn't really following up all those game engine/character behaviour evolution. I'm truly focusing on producing the next game, although it's not going fast.

Oh, granted, I may have played more others' game, too. With two Zelda games to discover, a good deal of Fez and some backlog of Nintendo 3DS games. Washing up sessions tend to be longer since I discovered AfterBit and Speed Game vlogs... And I hope another 3DS game will be tested soon, although it's an old friend.



Avec un nouveau Zelda, la découverte de WindWaker, les documents d'origine de Rick Dangerous et les sources de Commander Keen, j'ai été gâté. J'espère pouvoir faire bientôt le point sur le cadeau inattendu reçu à Noël.

dimanche, décembre 28, 2014

Keen ClipToSprite()

This is the code snippet for the ClipToSprite() action that results in pushing monster/keen interaction when the monster stands on the left. The code adapts characters expected motion (which differs from its computed speed and is transient) so that intersections between characters hitboxes remains empty.

The condition highlighted with yellow puzzled me at first. In fact, we'll note that leftinto == xmove (the difference between characters speeds) when they were exactly side by side at the previous tick. Would there be 1 pixel distance between them at the previous tick, we'd end up with leftinto == xmove -1. So what it really implies is that we won't get pushed at high speed if we fall down just on the left of a carrot running to the right with a large leftinto value (e.g. if we fall on carrot's head and move down through it).

Une seule fonction -- ClipToSprite -- permet à Keen de s'aligner sur d'autres objets en définissant un nouveau mouvement qui s'exécute en plus du mouvement prévu par la fonction think(). Le principe de base est de calculer de combien de pixels les deux personnage s'interpénètre et de repousser celui qui n'est pas solide par un déplacement opposé.

Il est important pour comprendre ce code de se souvenir que le moteur de jeu travaille en plusieurs passes et que tous les personnages ont déjà leur nouvelles coordonnées quand les tests de collision ont lieu. Les valeurs originales de xmove et ymove ont donc déjà été appliquées. Par la "condition jaune" (leftinto <= delta_xmove), on s'assure que Keen ne sera repoussé par le bord d'un objet que s'il n'avait pas déjà dépassé ce bord à l'étape précédente (mais sans que la collision n'ait lieu parce qu'il était au-dessus du chariot, par exemple).

The ID keen engine is multi-pass. That means when ClipToSprite() is invoked as result of keen/courier contact, setting keen.ymove=0 doesn't prevent keen from falling, because he already moved down in the previous step. Instead, it prevents him from moving down again as result of the carrot collision. This design avoids issues with moving platforms without requiring a re-ordering of the objects evaluation.
This is also made possible because objects have a separate react() function to align against level structures.


Pour les plate-formes, des tests verticaux font suite aux tests horizontaux également présents dans les "monstres pousseurs" comme la carotte véloce. S'il est surprenant que le déplacement horizontal de Keen soit exactement celui de sa plate-forme (et pas Keen.xspeed + Cart.xmove comme le voudrait la physique), c'est qu'un premier mouvement a déjà eu lieu. Il ne reste plus qu'à appliquer le mouvement de la plate-forme à Keen, le repousser vers le haut et surtout s'assurer qu'au moment de l'appel à KeenAirReact, la logique du jeu pense qu'il y avait un bloc solide sous Keen. C'est le rôle de la variable hitnorth qui conserve le type de tile qui a interrompu le personnage lors de la phase de déplacement.

Characters that push you but you can't ride ends after those horizontal tests. For carts, platforms and Keen4e's bouncing red ball, you'll also process the "bottominto" test. Note that since we already moved Keen according to the direction keys, setting keen.xmove=cart.xmove and invoking ClipToWalls() again -- which includes applying (xmove,ymove) to (x,y) coordinates of Keen -- combines cart's move with Keen's own move as expected. Hacking the hitnorth member will make KeenAirReact() function believe that some solid ground was found during move/clip step.

mardi, décembre 23, 2014

Kirby 3D

Du sympa et *beaucoup* de récup dans ce nouvel opus de la chiquelette rose la plus populaire du monde Nintendo... Variante simple mais efficace des power-up centimes de vie à récolter: la couleur indique la valeur (comme les rubis d'Hyrule) et de mignonnes petites fleurs dévoileront un bonus quand on passe devant.... Allez, bonne nuit :)

I completed tonight the Kirby 3DS game I borrowed to my brother a couple of month ago. Quite cute, as one could expect. The depth exploring system doesn't add much to typical 2D gameplay (not more than offering 2 screens, at least) but it doesn't ruin the experience either. They put effort in the boosted vacuum mechanic, that's quite obvious. Using the colour of 'coins' to indicate they have higher value worked better than glueing 10 coins together. Taking note ...

Par contre, avec ses interactions avant-plan/arrière plan, son gameplay 2D dans un environnement 3D et sa structure très géométrique (contrairement à Donkey Kong Returns, beaucoup plus organique), Kirby Triple Deluxe est certainement le jeu qui vous donne la meilleure approximation de ce que j'avais en tête quand j'ai commencé à étudier le phong shading pour faire un Bilou à base de 3D, en 1998. Et les pieds de Kirby qui ne se plient pas quand il bouge vous donne aussi une bonne idée de pourquoi je n'ai pas continué dans cette voie-là après les premiers essais de rendu dans un modeleur.

lundi, décembre 22, 2014

Keen's Inception

Coincé entre deux générations de "game engine" de la série commander keen, on trouve un jeu étrange, faisant presque figure de "lost levels": Keen Dreams. Une grande part du design global de l'excellente série "Goodbye Galaxy" est présent: décor en perspective cavalière, personnage de 40 pixels de haut, environnements variés et son SoundBlaster. En revanche, keen se promène en pijama et en pantoufle et ne dispose ni de son neurolaser, ni de son célèbre pogo. A la place, Keen peut lancer en cloche des mines transformant les ennemis en fleurs à leur contact. Le gameplay demande donc beaucoup plus de précision que dans les autres épisodes, d'autant plus que les ennemis ne resteront pas transformés éternellement. Ça n'est pas sans rappeler le lancer de taille-crayon dans Bilou, je l'avoue, mais si je vous en parle, c'est surtout à cause de ses sources, ajoutées sur github début septembre, et que je suis occupé à analyser. Les sources sont essentiellement en C avec quelques blocs d'assembleur en ligne, chose plutôt rare pour l'époque.

Bien sûr, j'aurais préféré que Javier et Chuck nous proposent le code de Goodbye Galaxy, notamment à cause de l'absence de pogo dans cet opus, mais c'est le premier Keen à proposer des pentes, ce qui n'est déjà pas si mal.

Keen Dreams' source code has been released last September. I wish it was the code for one of the Goodbye Galaxy episodes, of course, as it is one of the games I played the most, and the one with the richest features set -- pogo, gun, moving platforms and shooting ennemies -- which isn't crippled with clipping bugs. Anyway, Keen Dreams has slopes and shooting/pushing ennemies. The map design with its "info layer" suggests that most of the engine has been kept between Keen Dreams and Goodbye Galaxy. The character's moves (jump, run and pole-climbing) are direct translation of GG spritesheet. The pogo is missing, and I will have to define how the lone "canteloupe cart" can be ridden. 

Rather than stunning gun, Keen throws "flower power" seeds that are affected by gravity somehow like my dumbladors. Gameplay-wise, I cursed that decision quite often. Aiming for those fast-moving, aggressive vegetables with something that follows an arced curve, bounce on the ground and only stuns for a limited amount of time made imho this episode almost the hardest of the series. So far, I haven't found arced curve of bladors that hard to use, but on the other hand, monsters in Bilou's Schoolzone don't kill you instantly on contact.

N² with high N could turn Nightmare.
En plus de leur calques "graphiques", les niveaux possède une couche "info" qui indique quels monstres créer au chargement du niveau (scaninfoplane et HandleInfo). Les monstres de l'ensemble du niveau sont conservés dans une liste liée, mais seuls sont "actifs" ceux qui sont assez proche dans l'écran. Leurs collisions sont gérées par un parcours imbriqué de la liste (N²) -- rien d'équivalent à mon système de "castes", donc --  mais vu le nombre réduit de monstres par écran dans le level design, celà ne pose pas de réelle difficulté, sauf peut-être dans le niveau des vignes.

Compared to my own engine, code for managing collisions looks quite simple. There is no Hero/Ennemy casts, nor collision masks. The engine checks every pair of "objects" for intersection of axis-aligned bounding-boxes. That could easily turn into programmatic nightmare in a game with heavy number of monsters like Apple Assault (up to 10,000 checks per frame), but here, the test is skipped as soon as one of the monsters is off-screen (active is false), and there quite little places where the screen shows more than a handful of ennemies at once. 

Funny enough, although everything may have its own contact() function and both are invoked when two objects come in contact, there is no passive/active role... yet, monsters typically don't use their contact() function at all, and whether Keen should die or be granted more points is all encoded in Keen's own contact() function, and contact() function for the flower-power seeds has knowledge of which monster can be stunned and which object shouldn't be affected.

Casts make it linear and scalable
On a une fonction "contact()" pour chaque monstre qui régit les changements en cas de collision. Pas non plus de notion de "actif/passif" mais une organisation où "PowerContact" (pour l'arme de Keen) transforme tous les objets en fleurs et KeenContact() mêne à la mort de Keen presque systématiquement. Ces fonctions contact ont la possibilité de faire n'importe quels tests (on est dans du code C), y compris aller tester l'étape d'animation de l'objet avec lequel on est entré en contact ... ce qui permet de concentrer toute la logique de collision dans quelques objets. Pour les monstres, il n'y aura en fait aucun code pour les contacts.

Ça vaut aussi la peine de regarder de plus près le système des "ticks" qui règle le comportement des personnages. Ce type de code est le plus souvent absent sur console. La vitesse du CPU est connue et la mise à jour de l'image à l'écran assez rapide vu la structure choisie pour le processeur graphique. Mais on est ici sur (vieux) PC, avec une vitesse quelque part entre 6 et 40MHz pour le processeur principal et un système de rafraîchissement de l'écran passablement complexe. Le jeu ne tournera certainement pas à 60 images par secondes, ni même à 30 ou à 12. On aura plus que probablement un temps de rendu (entre l'instant où la logique du jeu a fini sa mise à jour et le moment où la nouvelle image est effectivement visible à l'écran) variable.

The game logic is built around the notion of time ticks, which are a virtual equivalent to video frames on a game console like the Nintendo DS. However, unlike a console, the PC (ranging from 6 to 40MHz by that time) cannot guarantee we'll have a new frame rendered every 1/60th of second -- maybe not even every 1/12th of second. It's much more likely that the framerate will be irregular, depending on bus availability for memory transfers and complexity of the current scene. ID software developers thus measure how much time elapsed since the last rendered image and deduce how much game logic _ticks_ corresponds to this time. The StateMachine() function then compensates by stepping the characters by (xspeed,yspeed) the appropriate number of time, invoking the think() function when needed. Rather than experiencing slow downs, we'd experience a drop in the frame rate, but no kid on earth would complain about that from a shareware

The code logic deciding whether think() should be called or not is quite complex, allowing some part of monster behaviour code to indicate "do not think for N ticks, and slide me at constant speed" or "only invoke think() when time is ready for the next animation frame", etc. The benefit is that most of the code that accommodates for "process N ticks at once" is in that generic logic, and the monster-specific code remains as simple as updating speeds, not moving coordinates. Another function will then react() to the new position of the character on the level (does it still has ground under its feet ?)

L'idée (toujours présente dans les Quake modernes, pour ce que j'en sais) consiste à mesurer le temps qui s'est écoulé depuis la dernière demande d'affichage et à exécuter k "pas" (les ticks) de la logique de jeu, où k * durée_d'un_pas = temps_écoulé. Ainsi, la fluidité varie mais le timing du jeu reste constant et on ne perçoit pas de réel "ralentissement". Ce qui témoigne de la qualité du design, c'est le fait que le code du comportement des personnages peut être écrit sans devoir se soucier de ce mécanisme: les fonctions DoActor et StateMachine prennent intégralement ce comportement en compte et sont capables de gérer une transition d'état au milieu du laps de temps à simuler sans pour autant faire N appels aux fonctions think() des personnages. Autre élément qui se retrouve aussi dans les FPS d'ID software: la fonction "think" n'est pas forcément appelée à chaque moment. Selon les besoin, elle peut être invoquée sur les étapes d'animations, à intervalle régulier ou aléatoire.

Dernier traît intéressant dans les grandes lignes de l'organisation du code: la fonction think() d'un personnage ne se préoccupe généralement pas des collisions avec les murs. Elle se contente de lire l'état du jeu et de décider de la direction/vitesse/animation à suivre. Une deuxième fonction, react(), associée elle aussi aux états du personnage, sera appelée après que le déplacement ait eu lieu et s'occupe d'aligner un personnage qui aurait rencontré un mur. J'y reviendrai dans le volet prochain.

samedi, décembre 13, 2014

Amélioration Spongebop ...

Je me suis amusé l'autre jour à imaginer comment les SpongeBops pourraient être plus interactives dans le jeu. Les faire flotter sur l'encre, bien sûr, mais aussi les transporter comme les dumbladors. Et pourquoi pas s'en servir également comme d'un "parapluie" contre les jets d'encre ?

So far, we can grab spongebops while they're swinging, and use them to reach higher places ... but that requires significant learning of the game mechanics. Dumbladors can be used to detach them but so far, it doesn't bring much difference. I wanted to add "carrying" and floating, which is now ongoing. I'd also love to make carried sponge a protection against falling ink droplets...

  • [done] pick up and throw spongebop
  • [done] spongebop floats on ink
  • [done] ink droplets stops on spongebop
  • [done] pendat turns back when walking into a spongebop.
  • [todo] floating makes spongebop move up and down
  • [done] stands on floating spongebop. Don't bounce.
  • [done] better physics when thrown
  • [done] Bilou stays hung when Spongebop is jammed.
  • [done] blador-like virtual walls to avoid Bilou holding the spongebop from far away.

mardi, décembre 09, 2014

Leçon de tuning: la théorie


 
Tune it well ...
Animating a running pendat within restricted pixel space turned out better than I initially expected. Now, I have to tune that additional move to ensure it improves the gameplay. The speed at which it moves, the distance from which Bilou is detected and the acceleration were all arbitrarily picked while writing the script. They work rather well, but they can surely be tuned for maximal results.

Bien. J'ai rajouté une animation pour que le crayon-soldat puisse charger Bilou quand il l'aperçoit. Reste du coup les questions de tuning à régler:
  • à quelle vitesse doit-il courir ?
  • à quelle distance peut-il "sentir" Bilou ?
  • combien de temps doit-il mettre pour atteindre sa vitesse maximale ?
L'enjeu de ces règlages ? faire en sorte que les réflexes puissent continuer à tirer le joueur d'affaire et éviter de se retrouver dans une situation à la Rick Dangerous où le joueur doit mémoriser tout le parcours pour pouvoir jouer.
Bilou and Pendat positions over time
The issue with arbitrary values is that they can twist the gameplay towards a state where players can no longer react to what happens and have to memorize the level just to complete them. I have been convinced by Kirby Kid's blog that this is not the way platformers should be built and that they should instead allow to be "played while reading" once we mastered core skills (estimate trajectories, press buttons, read timings) and learnt game's physics. As our player has reaction time to the walk/rush transition of the pendat (a) and that Bilou needs some time e.g. to reach a height where collision no longer occurs (b) -- and possibly only keeps that height for some time (c), values exists where player cannot possibly escape. These must be avoided.

Je me suis donc donné deux cas d'étude: "Bilou tombe devant le pendat et doit s'échapper en courant" et "Bilou doit passer par-dessus le crayon en sautant". A partir de là, on peut représenter la distance Bilou-crayon au cours du temps et voir s'il y a ou non risque d'avoir une collision.

T_player_reacts + T_Bilou_reaches_speed < T_pendat_clears_distance
T_player + T_Bilou < Pendat_Detection_Distance / Pendat_max_speed

A sa vitesse actuelle, le pendat met 81 frames (1" 36 centièmes) pour aller de sa position actuelle à celle de Bilou. Bilou, lui, met 30 frames (1/2 seconde) à atteindre sa hauteur maximale (b) et on peut compter qu'il y reste 20 frames (c), pendant lesquelles le Pendat aura avancé de 30 pixels ... Assez pour se croiser sans accroc. J'ai vu la plus jeune testeuse de la S-team réussir ce saut d'instinct. Celà signifie qu'elle a un temps de réaction d'au plus 600ms ... Hmm ... Oui, le pendat actuel est loin de demander des efforts de réactivité puisqu'on estime à 200ms un bon temps de réaction à un stimulus, pouvant descendre près des 100ms pour un sportif entrainé. Et puisque Bilou est quasiment au centre de l'écran, celà signifie que si on voit le pendat quand il fait demi-tour, il nous attaque sitôt qu'il se retourne. Rétrécir la distance de détection et la compenser par un "sursaut" du crayon pourrait améliorer la situation

While my testing-nephews were giving it a try, I went through to "escape case" -- run away or jump over -- to map where would time go. Not even the youngest player had issues with the original timings, which left a generous 600ms to the player to dodge the rush. That's about 3 times the delay usually presented for fast reaction. It "turned" out that direct rush of the pendat is not the most dangerous -- so I can easily shorten the detection distance and have pendat entering the screen walking and only start chasing Bilou when at a distance close to 1/3 of the screen width. What is truly dangerous is that bounce when he enters a wall, and then quickly turns back. It means if you're waiting for him at the top of that wall, your window to sneak behind is small. In fact, the thing that may prove more stressful is to shoot him down with a blador. But that should simply tease you to get the straight-throw power-up ;)

En fait, le rebond contre le mur est beaucoup plus piégeux. J'imagine que c'est dû à la cassure du mouvement, plus difficile à estimer. Et l'atteindre avec un taille-crayon affecté par la gravité sera plus délicat, mais pas trop quand-même puisque le crayon a le bon goût de rester dans la zone active du taille-crayon pendant sa courbe. Ouf.

Avec tout ça, le "croco-désagrafeur" retombe dans l'oubli ...

dimanche, décembre 07, 2014

Super State Bros.

Une tentative de mesurer la complexité du comportement de Mario dans Super Mario World, avec l'idée de le comparer à celui de Rayman, histoire de mieux comprendre comment ce genre de chose peut monter à 15K de code source. Par comparaison, le code de Bilou prend 900 lignes de script et pas loin de 20K ... La partie des "micro-contrôleurs" qui définissent les actions élémentaires combinées dans la machine d'états prend pas loin de 1300 lignes de code, soit presque 40K caractères. C'est mesuré à la grosse louche vu qu'il y a aussi parmi ces contrôleur du code qui ne concerne que SpongeBop (pour l'instant), etc. et surtout une bonne dose d'annotations pour InspectorWidget.

How complex is a platformer character behaviour ... let's say Super Mario in Super Mario World ? Does that help me understand whether 15KB of code for Rayman Origins is compact, regular or stunning -- as it looked to Pasta Games developers who converted it into Jungle Rush game ? To figure out, I tried to depict a state-machine-like representation of all the states Super Mario can take, where a different state implies different reaction to player input.

'What is a "state" ?' might you ask, and why don't we see small-vs-big or fire Mario here ? A state is not a costume it is an identifiable part of the behaviour that is distinct from a gameplay perspective. "Jump" and "Run" are different states because Mario behaves differently (gravity, friction) and because some actions are only possible while jumping while others aren't.

In figures, that sums 21 different states for Mario, 6 of them working only with specific level elements and 6 others requiring a power-up. To my surprise, Rayman Origins accounts for a quite similar number of states, mostly because punching and kick'ing suspend some actions and can be cancelled like in a fighting game, therefore asking for a dedicated state while Super Mario's fireball can be spawned without cancelling any other move. The original Rayman on PSX had much simpler behaviour, with only 11 distinct states.


En chiffres: 9 états de base pour Mario ("soulignés"), 6 états supplémentaires pour interagir avec des éléments précis des niveaux (entre parenthèses) et 6 autres états qui ne sont disponibles qu'à travers un power-up. Graphiquement, la "sprite sheet" de Super Mario World est plus vaste, mais je n'ai pas répliqué ici "saute, saute avec un champignon, saute avec une fleur de feu, saute avec une plume, saute avec yoshi" parce que tous sont identiques au niveau du code de comportement: ils réagissent de façon identique au commandes du joueur.

En comparaison, je n'ai compté que 11 états dans le comportement du premier Rayman sous DOS/PSX, dont 5 doivent être débloqués au cours de la progression du joueur. Bien moins complexe qu'un SMW, donc. Mais les baffes (qui affectent la vitesse de Rayman), les attaques-rodéo et autres glissades, les rebonds et la nage de Rayman Origins.

Le personnage de Xargon en est à 10K symboles (389 lignes de code)... Ah, oui ... il faudra que je regarde d'un peu plus près le code de Keen Dreams.