vendredi, février 05, 2016

Tests passed, Guru still meditating.

It's a bit disappointing: I set up an environment to stress-test the code, make sure uninitialized memory has garbage content, etc, and everything runs fine. I use the same code in SchoolTest (rush) application on real hardware and it crash before starting to run the title screen. What else could I do but add the "memory inspector" mode of the Guru Meditation screen in the game ?

Pff. Râlant. J'ai fait des cas de tests, j'ai émulé, j'ai fait des corrections, j'ai vérifié mon "build system". Tout semble bon, mais quand je fais tourner le vrai code sur la vraie DS, j'ai toujours un gros crash vers la fin du chargement de l'écran de titre. Il ne me reste donc qu'à reprendre le code d'inspection de mémoire hors de runme et l'ajouter dans le jeu pour repasser en mode "débugging post-mortem"...

Edit: Grâce à l'inspection libre de la mémoire de myGuruHandler(), j'ai pu reconstruire les différents groupes de paramètres des fonctions qui ont appelé un "free()"  là où il n'aurait pas dû, corrigé le système de tests pour qu'il vérifie le "tag magique" inséré par malloc(). Jubilation: le jeu lance maintenant son écran-titre.
Déception: dès le premier contact spécial avec un autre personnage, le moteur de jeu se re-plante.

lundi, janvier 25, 2016

Leakage: 0%

Un script pour décoder les traces d'allocation de mémoire combinées à un mécanisme qui remplace malloc/free par une progression linéaire dans l'espace-mémoire et j'ai pu éliminer la totalité des fuites de mémoire dans le parsing des scripts.

Deuxième étape, une inspection systématique des listes d'animations maintenues par l'Engine lui-même histoire de s'assurer qu'il ne reste aucune référence à un objet libéré (c'est désormais facile, puisque chaque objet libéré se voit écrasé par des 0xfefefefe et que sa mémoire n'est pas ré-attribuée lors du test). Me voici armé pour tester les enchaînement de niveaux, mais jusqu'à un certain point seulement: au bout de quelques itérations, la mémoire disponible pour le test aura été complètement utilisée. Allez, je devrais pouvoir passer aux chargements-suite-à-une-fin-de-niveaux dans le courant de la semaine, et étudier les "script reset" expérimentaux supposés accélérer les chargements de niveaux.

I reached stable parsing, where no trailing objects exist when the GameScript is destroyed. The next test I implemented pointed out an object that was "recycled" but that was still referenced by the Engine's global state. So I am now ready to investigate what happens during levels transitions, both regular and "just reload" experiments. More testing to follow.

dimanche, janvier 17, 2016

When the player sees the challenge

http://imgur.com/a/215BY
The good thing with Mario Maker is that it makes it easy to illustrate how Mario games are built and where are the high-quality level design element. One point for which I had hard time to understand Kirby Kid's discourse was about coins placement. He typically objects that rings in Sonic are less interesting than coins in Mario
because some rings can't be obtained in one play through, the encouraging function of the rings becomes diminished. [...] Instead of saying "you can do it, keep trying," they say, "too bad you missed. better luck next time."
I ultimately got the true meaning of that thought while watching "Super Mario Fixer, episode 2" and see how KirbyKid played it, how he pushed effort into getting all the coins doing only one jump per platform, without going backwards. An exercise that may sound futile at first but that he describes as "recognize the challenge for themselves and let the player go for them".


Vous vous souvenez de Kirby Kid et de ses précieux commentaires sur le gameplay de ce qui devait devenir School Rush ? Un que je n'ai toujours pas pris en compte -- parce que je n'en comprenais pas la pleine mesure -- c'est la nature "généreuse et dense" des bonus sur les niveaux de School Rush. Forcément, il y en a assez bien, entre autres parce que c'est un enfant de 4 ans qui a choisi où les placer.

Où est le problème donc ? Eh bien, voyez-vous, si les bonus sont bien positionnés, ils peuvent proposer des façons alternatives de parcourir le niveau, par exemple "d'une traite, sans faire demi-tour ni s'arrêter mais en prenant tout de même tous les bonus". Ça représente une sorte de challenge supplémentaire que le joueur aguerri peut se fixer lui-même une fois que "finir le niveau" n'est plus un challenge intéressant pour lui. Mais pour se faire, il faudra veiller à ce que chaque pièce soit placée avec précision, pour qu'il n'y ait pas besoin de plusieurs passages pour être sûr de les ramasser toutes.

That's all about having some room for extra challenge for those who have full mastery of Super Mario. At that level, a single coin placement can make the difference. A single misplaced coin and the challenge is just impossible, and thus actually doesn't exist at all.

I want to have two ways of playing School Rush. One is power-play, rushing through. The other is exploring and experimenting. I can use some bonuses to tease the explorer and reward her. I can use some other bonuses to tease the rusher and reward him. Because I don't have a single coin, but multiple bonuses. I can have e.g. the "A" letters show the path for rush-and-be-amazing, and other letters for exploring places that require to do multiple pass over the level.

dimanche, janvier 10, 2016

100 bytes for a level

Il m'aura fallu me promener dans le code désassemblé de Super Mario Bros 1 pour finir par y croire. Oui, sur cette cartouche de 40KB, les niveaux sont extrêmement compacts. Oui, la technique pour en faire le rendu est à la base de ce que j'avais observé avec l'éditeur pour NSMB: bien que le hardware travaille avec des "tiles", le niveau est décrit par une série de commandes (un byte de coordonnées, un byte indiquant le type de commande et éventuellement une taille). On aura ainsi des "un tuyau de 3 blocs de haut" puis "une rangée de 5 briques" et "un escalier de hauteur 3". Voire même "un trou dans le sol de largeur 4". Pas de "patterns extensibles" derrière ces codes, mais directement du code assembleur!
Les buissons, nuages, le sol sont eux décrits sur une autre couche qui est utilisée en premier lieu pour remplir la zone mémoire sur laquelle le niveau sera ensuite dessiné.

Thanks fly to Daniel Turner who teased my interest for how SMB1 encoded its levels. The cartridge was incredibly small (40KB) and indeed encoded levels as a list of "painters" operation, each giving a coordinate on the current screen, a type and possibly a size. Painters are simply dedicated assembly routine, and thus they could have something as complex as "add a staircase of 9 steps".

What I beleived to be an original technique in NSMB was thus a good old recipe. Would it be useful to shrunk those 64K levels of Bilou : School Rush ? Well, hardly, because compared to SMB1 I have much more variety in the graphics of the level, and I used those "alternate tiles" to make books, folders and pencils looks as unique as I could and avoid giving the feeling of a world built by copy-pasting. That's a bit sad, because SMB approach definitely makes editing the level much easier.

L'équipe RD4 aura profité au maximum du fait que le jeu ne fait pas marche arrière: le niveau est encodé écran par écran, et les coordonnées des objets sont données à l'intérieur de l'écran en cours. Un marqueur est ajouté aux objets qui se trouvent sur un nouvel écran par rapport à leur prédécesseur.

Un monde de différence, donc, avec le stockage brutal d'un tableau de MxN éléments que j'utilise dans LEDS (avec des niveaux de 32 ou 64KB. Plus gros que l'entièreté du jeu SMB1, donc), mais qui me permet en contre-partie une souplesse totale pour que les livres ne soient pas tous exactement les même. Allez, apparemment, une simple compression .zip pourrait réduire les niveaux à une taille d'environ 8KB.

mardi, janvier 05, 2016

10 ans de DS

Voilà. Il y a 10 ans, je déballais ma première DS. Juste un an après, je démarrais le projet "dsgametools" sur sourceforge. Bonne année 2016 à tous. J'espère parvenir à finaliser "School Rush" cette année ^_^ Le prochain projet de Bilou "Infinite Pyramid" a déjà commencé à me faire cogiter.



Happy new Year, everyone. Know what ? This year, it's been exactly 10 years since I got my first Nintendo DS. I still hope I'll be able to complete and release School Rush this year so that I can start working more effectively on my next homebrew project: Bilou in the Infinite Pyramid.

mercredi, décembre 30, 2015

Je peux améliorer mon C++

Grande différence entre mon "nouveau" boulot (depuis Mars 2014) et mon ancien poste universitaire: ici, il y a des revues de code. Et mes collègues "Hergé et Jigé" ont un sacrément haut niveau en C++ comparé au mien. Alors autant profiter de mes deux semaines de "Super Papa Bros" pour essayer de remanier le code de mon moteur de jeu, le rendre plus fiable, plus lisible, et peut-être plus efficace.

J'avais introduit un mécanisme de gestion de mémoire inspiré du cours "compilateurs" : le "tank", avec un seul bloc de mémoire qui est découpé progressivement en sous-blocs qui auront tous la même durée de vie. L'ennui principal, c'est que ce "tank" n'a aucun moyen de retenir quels objets ont été créés ni d'appeler les destructeurs en fin de cycle. Du coup, tout objet "standard" présent dans les morceaux du tank sont une fuite de mémoire potentielle.


Parmi les "nouveaux trucs" appris cette année qui pourront m'être utiles, il y a la fonction "foreach", les fonctions template (et en particulier leur utilisation pour faire de la programmation assertive), les namespaces anonymes, et les structures-internes-pour-masquer-l'implémentation.


Let me collate a few C++ tricks I practiced this year and hope to use in my hobby tools/game engine to improve them.
If it make sense to have a function applied on all members of a collection, foreach can help:

- for (vector<Tire>::iterator it = wheels.begin(), e = wheels.end(); it != e; it++) {
- checkPressure(*it);
- }
+ for_each(wheels.begin(), wheels.end(), checkPressure);


Template function do exist. Template functions do not need their template argument to be specified when it can be inferred from function arguments. E.g.

template<typename T>
void assert(T a, T b, const std::string msg) {
  if (a != b) throw AssertException(msg);
}

can be invoked as
+ assert(myCar, TimeTravellingDelorean, "timed' out");
-assert<Car>(myCar, TimeTravelling ...);;

And as we're talking about templates stack overflow's question on puzzling template error messages can help.

You don't need to declare your functions static to avoid interference with other translation units of the program. Simply put them in an anonymous namespace.

You don't need to explicitly track the "object setup sequence" with an init_level if you can do it with contents of the regular members of the objects,


Car::~Car() {
- switch(init_level) {
- case TIRES_MOUNTED: RecycleTires();
- case ENGINE_INSTALLED: RecycleEngine();

- // FIXME: what do you do for default: ?
- }

+ if (tires!=UNDEF) RecycleTires();
+ if (engine!=UNDEF) RecycleEngine();

PS: UNDEF could just be 0 for pointers to components.

You can have compact structure initialization with (optionally-)named fields but it must be *trivial*, e.g.

  • you may not swap the order of components;
  • you may not omit a field if there are other field after it
  • but you *can* omit items at the tail of the description

If you want a class/struct to look more like a first-class citizen, think about
  • copy constructor : Car(const Car &that) : engine(that.engine), tires(that.tires) {}
  • comparison operator : bool operator==(const Car &that) { return that.tires==tires && that.engine==engine; }
  • ostream-compatibility: this requires a additional std::ostream& operator<<(std::ostream& os, const Car& that) { os << "powered by " << engine << " on " << tires; return os; } function. Note that it is *not* a member of the Car struct/class and that it will need to be declared friend of the Class in case of a class.

Something thrown as throw new std::runtime_error(..) is caught by catch (...) { releaseResources(); throw; }, but not by catch(const exception& e). That latest one only catch stack-allocated exceptions, e.g. throw std::runtime_error(...);. Reading more on this I should.

mardi, décembre 22, 2015

Usage: 15% ; Leakage: 12%

I guess you all know what a "memory leak" is. Well, you might not have a lot of gameplay updates in the following weeks: I just enabled a sort of report that measures how much of the state created when parsing the title screen of School Rush. 12%. I've got some code-cleanup to go through.

Bon, bin les premiers résultats de l'analyseur mémoire intégré à mon système de test-de-code-DS-sur-x86 sont précis, mais aussi assez effrayant: 12% de la mémoire allouée lors de l'initialisation de l'écran-titre ne sont pas rendus à la fin du niveau. Dans un cas pareil, la mémoire de la DS (4Mo) finit systématiquement par être saturée après un certain temps d'utilisation du jeu, ce qui ce traduira fort probablement par un crash.

Allez, joyeuses fêtes à vous aussi. (Et, euh, non, n'espérez pas trop une version "spéciale Noël" de School Rush avec de la neige sur les bancs et des bonnets à ponpon sur les gommes sauteuses, du coup)

PS: I suspect this could be linked to the migration of most state-machine data structure into the "tank" memory allocator: because the tank doesn't know objects themselves, and that it's quite unlikely that it invokes their destructors when "flushing" the tank. I'll have to make sure that structures that were pushed into the tank are flat enough or that their sub-components are allocated in the tank too.