Le simulateur¶
Le simulateur tourne dans le navigateur, et il est aussi l'éditeur. Il a précédé la carte, et il en reproduit le contrat mesuré : coûts du blitter, pool, fond tuilé, lenteur du mode texte. Tout ce qui définit le comportement de la console est le même code que le firmware ; le simulateur n'y ajoute que le plumbing du navigateur. C'est pour ça que son rastériseur est logiciel plutôt qu'en WebGL : il doit épuiser le même budget par ligne que le silicium et rater pareil, une ligne trop chargée s'affiche périmée, un sprite pas encore dans le pool manque une trame. Un backend à base de quads texturés donnerait « ça marche dans le simulateur, ça clignote sur la carte ».
Touches : les flèches sont la croix directionnelle, Z est le bouton A (tirer dans les exemples), X le bouton B.
Un jeu est un projet¶
Le simulateur vit dans l'App, et un jeu est un projet du nuage — il n'y a pas de dossier local à ouvrir. L'arborescence de gauche liste deux sortes de projets : les jeux publiés (la dernière version publiée de chacun, par qui que ce soit — les exemples de la galerie y vivent), en lecture seule et clonables, et vos brouillons, modifiables et enregistrés dans le nuage. Pour modifier un jeu publié, clonez-le : le clone est à vous.
Cliquer un .lua l'ouvre dans l'éditeur ; cliquer un PNG en montre l'aperçu et signale un format non indexé.
Seuls les PNG en couleurs indexées sont acceptés, la sortie native d'Aseprite. Sur la carte l'art est pré-quantifié en NOR ; quantifier à la volée dans le navigateur réintroduirait une divergence sur les indices. L'index 0 est transparent pour les sprites, et c'est aussi le trou du fond : un sprite dessiné « derrière le fond » n'apparaît que là.
Les modules Lua passent par le même canal : require("enemies") demande l'asset enemies.lua au dossier du jeu. Pas de package.path, pas de chemin, un nom plat, résolu là et nulle part ailleurs. Sur la carte, c'est la micro-SD ou la NOR derrière le même mécanisme.
Le dossier du jeu porte ses groupes de palette (game.pal, game.atlas, les PNG que l'atlas nomme), ses variantes (game.nuit.pal) et ses fonds tuilés (game-niveau1.map + game-niveau1-tiles.png) ; gfx.bundle() installe tout. Voir un premier jeu.
Éditer et lancer¶
- Ctrl+Entrée (ou ⌘+Entrée) relance le jeu, Ctrl+S (⌘+S) sauve le fichier — dans le nuage, dans votre brouillon.
- Un jeu publié est figé : la sauvegarde est refusée, clonez-le d'abord.
La barre d'état¶
La barre d'état donne le seul chiffre qui compte : la charge de la scanline la plus chargée. Le budget est par ligne, pas par écran ; trente sprites alignés ne passent pas là où deux cents bien répartis passent. Les lignes en retard, affichées périmées sur la carte, sont teintées en rouge ; les sprites qui manquent (pas dans le pool à temps) sont comptés.
Manettes¶
La Gamepad API demande un contexte sécurisé, un geste dans la page et une pression sur la manette. Sur macOS, une manette Nintendo appairée en Bluetooth n'est vue que par Safari ; Chromium ne la voit pas, et ce n'est pas un réglage.
Déterminisme et traces¶
La trame logique est l'appel à _update(), pas le balayage vidéo. La console ralentit, elle ne saute jamais. Aucun rattrapage par accumulateur : le nombre d'appels ne doit pas dépendre du temps réel écoulé, sans quoi deux exécutions de la même partie divergent.
Ce qui va avec : budget en instructions de la VM et non en microsecondes, entrée verrouillée en tête de trame, PRNG semé explicitement (math.randomseed() sans argument est refusé), et _draw() qui ne modifie pas l'état.
Une trace est la suite des entrées d'une partie — environ 2,5 Ko par minute de jeu réel, puisque rien n'est écrit tant que rien ne change. Un jeu annonce son score par sys.score(n). C'est ce que fait le serveur du classement : il ne croit aucun score sur parole, il rejoue la trace et le reproduit.