Showing posts with label freezer. Show all posts
Showing posts with label freezer. Show all posts

Thursday, April 09, 2015

iScriptException ...

Something uncomfortably wrong occurs in the game engine. Objects I'm trying to restore have invalid state attached. I believe this is the reason why there has been "guru meditation" screens during last playtests with friends. It seems linked to abnormal use of the memory: while the infringing object seems to have proper structure, it seems linked to a state that is absolutely not looking like a state structure, but rather some animeds content...
That could quite match with some earlier dig into the issue where I had invalid pointers at some delete() operators, leading to red "guru meditation" screens. I unpacked my gdb-remote-analyzers-and-setup-perl scritps. It takes ages to launch the game in that fashion, but I should get a list of stack trace for those locations that shouldn't have been released. I may need to find a proper path to reproduce the issue first.

Meditate on this I will...

3 days later, with 2 runs on the first level, I managed to trigger the exception again in the "demulator", producing 1GB worth(?) of log. I have hopefully a shorter summary of the 11 addresses involved in operations that looked suspicious. I need to program the software eyes that will scrutinize those operations and explain what happened.

2 more days and "PERL's eyes" are ready. I get detailed reports with stack trace of the last allocation, the last sound deletion and the offending "double-free" issue. But what should I think about some memory block that is first allocated in "GobState::addOnDone" (thus should be a transition, somehow, or the vector of transitions), gets deleted as GobState is destroyed, and then destroyed again as a GobAnim ?_?

Finally, one object, shot during some state transition is delete twice, with a time locality pattern that makes it look like a real issue, not some side-effect. Both deletion happens during Engine::animate(), likely at a few frames interval. How has that occured ?

What transition create that object ? What object is it ? Which object created it ? ... At least those questions can be answered with the whole recording of memory allocations. I know from stack trace the address of the transition that created the "faulty" object. From there, running "Perl's eyes" once again, I find it belongs to a "fall->hit" transition of Bilou, and the object generated is a "power-down" object. That might help reproducing the error ... tomorrow.

Question: while there has been only one "constructor" call, could there be two "reganim", making the object linked twice in the objects list ?
Question: if there's a single "reganim" for two "delanim", what is the cause ? Who messed up with the list's invariant ?

Sunday, March 22, 2015

Au ralenti ...

J'avais noté de fort ralentissement sur émulateur dans le nouveau niveau. Il me restait malheureusement à constater que les ralentissement se produisent aussi sur la console, du moins pendant que les encriers projettent et si j'ai assomé beaucoup de dumbladors sans permettre à leur pieds de les rejoindre.

Slow downs are only fun when you control'm
The new level wasn't playing well on emulator. In fact, it was so slow (about 15 fps rather than 60) that I  could barely test it. I first thought my PC was unable to emulate it -- I had similar issues with e.g. Yoshi's Island DS when trying to get some snapshots. Running it on real hardware seemed to work fine until I started stunning bladors. With their feet as additional GOBs to animate, the system started to show slow downs occasionally (when jumping while inkjets throws drops). In my former level, I was 4 monsters, 16 drops and 14 feet under the threshold of what the 66MHz of the Nintendo DS can sustain.

Il y a 45 intervenants dans le nouveau niveau, dont 8 encriers et 7 dumbladors, soit un potentiel pour générer 30 objets de plus -- près du double. Oui, c'est risible comparé aux systèmes de particules qui peuvent tourner sur les PC moderne. Même pour 66MHz, ça ne devrait pas faire une telle différence. Mais voilà, à permettre aux gouttes d'encre de s'arrêter sur les éponges, à veiller à ce qu'un encrier puisse interrompre la marche d'un pendat ... bref, en ajoutant des interaction entre les ennemis et pas uniquement entre Bilou et ses ennemis, j'augmente les nombre de tests de collision, et il est possible que ce soit ce genre de chose qui fasse grimper la charge de calcul.

Is it the extra processing with of those monsters, with the animation parsing ? I could get up to 100 of them in Apple Assault, unless I try to make them react against each other (that would mean ~10000 collision checks). I do have some of those monster-to-monster tests, especially with ink droplets since I want them stopped by spongebops you carry along.
My plan so far to address this issue was to create sections of the game and to allow for collision testing only between objects belonging to the same section. Putting that into code lines didn't come out fluently, so I opted for the Commander-Keen inspired approach of freezing GOBs that are out of display range (that is, more than one screen away of what's on-screen). You can still try to collide against them when they're frozen, but *they* won't try to do anything. It was enough. It works well with the current game's flow. I'll revise it to something more generic later.


J'ai bien esquissé un système permettant de rassember les ennemis selon des sections, les collisions étant limitées entre personnages appartenant à une même section. Mais au moment de passer à l'implémentation, rien ne se combinait avec le code existant de façon souple. Je suis donc repassé à une technique plus traditionnelle -- le gel complet des personnages qui se retrouvent en-dehors de l'écran, qui était déjà à moitié implémenté (ils n'étaient plus traités par la couche graphique). Les listes à parcourir pour les collisions restent longues, mais c'est tolérable, d'autant qu'il y a moins de personnages qui provoquent des tests de collision. Et vu la constitution du code "GameObject::play", avec ses chaînes de contrôleurs et ses listes de commandes d'animation à interpréter, il n'est pas évident que la centaine de comparaison de coordonnées supplémentaire pèse tellement lourd par rapport à 5 objets supplémentaires à gérer entièrement.

There's an important effect of this mechanism. Yesterday, the position of a moving inkjet when you approach it depended on the time you took to reach it, from the start of the level. Today, it only depends on the speed you approached it for the last 2 screens. It will make learning of patterns easiers, as the level can be broken down into smaller chunks. It also means that the pattern of multiple GOBs (like two-inkjet-close-to-each-other) can now be manipulated by how you approach them... much like in Super Mario World.

Monday, December 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.