Aller au contenu

Le simulateur

Le simulateur tourne dans le navigateur, et il joue : il n'écrit rien. Un jeu s'écrit dans l'atelier de l'App, qui le lance avec le même moteur. Le simulateur 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, la barre d'espace est le bouton A (sauter, tirer), X le bouton B. Z marche aussi comme A, pour les doigts qui l'ont appris ainsi.

Un jeu publié

Le simulateur joue les jeux publiés dans l'App — il n'y a pas de dossier local à ouvrir. La liste de gauche donne tous les jeux publiés (la dernière version de chacun, par qui que ce soit ; les exemples de la galerie y vivent). Un jeu publié a sa propre adresse, …/sim/?jeu=<auteur>/<projet>, que la barre du navigateur montre dès qu'on l'ouvre : c'est elle qu'on partage.

Cliquer un .lua en montre le code, en lecture seule ; cliquer un PNG en montre l'aperçu et signale un format non indexé. Une erreur mène à sa ligne.

Pour modifier un jeu, clonez-le dans l'atelier : Public library, onglet Games, Clone into my projects. Connecté, le simulateur en donne le lien (clone it in the workshop ↗). Le clone est à vous, avec les niveaux du jeu, leurs cartes, modèles et sprites, prêts à être modifiés et recompilés.

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.

Lancer

  • Ctrl+Entrée (ou ⌘+Entrée) relance le jeu.
  • Les séparations se tirent : entre l'écran et le code (l'écran grandit par paliers entiers, ×1, ×2…), sous les listes de jeux et de fichiers, entre ces deux listes, et au-dessus des erreurs. Les tailles sont retenues par le navigateur ; un double-clic sur une séparation rend la taille d'origine.

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.