Manipuler la mémoire
La mémoire vive (ou RAM) de PICO-8 contient les graphismes et les sons de votre jeu, mais aussi le contenu actuel de l'écran et quelques autres données. Il est possible de la manipuler directement avec les fonctions peek()
et poke()
que nous verrons plus bas, mais dans la plupart des cas, vous pouvez vous contenter d'utiliser les fonctions de base, qui se chargent de communiquer avec la mémoire pour vous.
Par exemple, appuyer sur une touche active un bit dans la mémoire, et btn()
se charge de lire les adresses mémoire où se trouvent ces bits. Et lorsque vous utilisez une fonction de dessin comme spr()
, elle écrit dans la région de la mémoire qui correspond à l'écran.
Mais tout n'est pas possible en utilisant ces fonctions ! Accéder à la mémoire directement peut vous permettre de réaliser toutes sortes de choses uniques : générer des sons avec des algorithmes, étirer l'écran, communiquer avec des circuits électroniques... ou encore activer le clavier et la souris !
La mémoire est composée d'octets, et chaque octet a une adresse que l'on écrit généralement en hexadécimal, par exemple 0x5f2c. Vous ne comprenez rien à ce que je viens de dire ? Pas de problème ! Prenons un instant pour comprendre ce que contient un octet, et quelles sont les différentes façons de compter en informatique.
Compter avec des bits
Dans un ordinateur, le plus petit élément d'information que l'on peut manipuler est le bit. Il ne peut avoir que deux valeurs : le signal est soit éteint, soit allumé, ce que l'on désigne par les chiffres 0 et 1.
Dans le système décimal, nous avons 10 chiffres pour représenter tous les nombres qui existent, c'est pourquoi on parle aussi de base 10. Lorsqu'on arrive au bout des 10 chiffres à notre disposition, on ajoute une dizaine et on repart du premier chiffre.
Quand on compte en binaire, on n'a que 2 chiffres, c'est donc une base 2. On va atteindre les dizaines, les centaines et ainsi de suite beaucoup plus rapidement !
Décimal │ Binaire Décimal │ Binaire
─────────────── ───────────────
0 │ 0 9 │ 1001
1 │ 1 10 │ 1010
2 │ 10 11 │ 1011
3 │ 11 12 │ 1100
4 │ 100 13 │ 1101
5 │ 101 14 │ 1110
6 │ 110 15 │ 1111
7 │ 111 16 │ 10000
8 │ 1000 17 │ 10001
En informatique, on rassemble généralement les bits en octets, qui sont des suites de 8 bits. Avec un octet, on peut stocker 256 valeurs différentes.
Décimal │ Octet
────────────────
0 │ 00000000
1 │ 00000001
2 │ 00000010
3 │ 00000011
4 │ 00000100
────────────────
251 │ 11111011
252 │ 11111100
253 │ 11111101
254 │ 11111110
255 │ 11111111
Si l'on veut stocker des nombres plus grands, il faudra utiliser plusieurs octets. Par exemple, les nombres dans les variables de PICO-8 sont codés sur 4 octets (32 bits), ce qui pourrait permettre d'aller de 0 à 2 147 483 647. Cela dit, pour que PICO-8 puisse stocker des nombres négatifs et à virgule, l'intervalle utilisable va en réalité de -32768 à 32767,9999. Lorsque vous essayez de dépasser cette limite, le nombre va boucler sur lui-même, et c'est d'ailleurs un problème épineux lorsque l'on souhaite créer des scores gigantesques par exemple.
Le système binaire n'est pas la seule façon originale de compter à laquelle vous serez confronté·e au cours de votre vie de programmeur ou programmeuse. Lorsque vous choisissez une couleur dans un logiciel de graphisme, vous avez probablement déjà eu affaire au système hexadécimal, qui est en base 16. Par exemple, ce code de violet #9e96d0
est une suite de trois nombres en hexadécimal :
│ Rouge │ Vert │ Bleu
──────────────────────────────────────
Hexadécimal │ 9e │ 96 │ d0
Décimal │ 158 │ 150 │ 208
Octet │ 10011110 │ 10010110 │ 11010000
Pour représenter la base 16, on utilise les lettres de l'alphabet : les 16 chiffres sont 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E et F. Par exemple, le nombre 31 s'écrit 1F, et le nombre 32 s'écrit... 20. Oui, c'est un peu l'embrouille.
Malgré tout, ce format est populaire en informatique car chaque chiffre correspond à 4 bits, étant donné que 4 bits permettent de compter de 0 à 15. Cela permet une écriture plus compacte : vous pouvez représenter le contenu d'un octet en seulement deux chiffres. C'est pourquoi les codes couleur, qui contiennent 3 octets pour les 3 couleurs, tiennent en 6 caractères.
Pour bien savoir dans quelle base le nombre est écrit, on utilise un préfixe qui dépend du langage de programmation. Dans PICO-8, 0x
désigne un nombre hexadécimal et 0b
désigne un nombre binaire.
Bien, nous avons les grandes bases. N'ayez crainte : vous n'avez pas besoin de tout retenir, mais il est utile d'avoir une petite idée de comment cela fonctionne. Nous pouvons maintenant accéder à la mémoire !
Peek et poke
peek(adresse)
permet de lire l'octet situé à cette adresse de la mémoire. Cela dit, il vaut mieux utiliser l'opérateur @
car il économise un token et s'exécute plus rapidement.
peek(0x5f4c)
@0x5f4c
Prenons l'adresse 0x5f4c
pour l'exemple : c'est l'octet qui contient les boutons appuyés par le joueur 1. Si vous l'affichez en jeu avec print()
, il apparaîtra en version décimale, mais en réalité, il représente bien les 8 bits qui valent 0 ou 1 en fonction des touches appuyées.
Pour écrire à une adresse, on utilise poke(adresse, valeur)
. Par exemple, l'adresse 0x5f2c concerne le mode d'affichage, et si l'on y change des bits, on peut appliquer différents types de distorsions !
poke(0x5f2c, 0b10000010) -- inversement vertical de l'écran
poke(0x5f2c, 130) -- le même nombre en décimal
Mais avant de voir plus de pokes utiles, je vous propose de prendre un peu de recul et d'examiner comment toutes ces adresses mémoire sont organisées.
Anatomie de la mémoire
Il existe 3 types de mémoire dans PICO-8 : la ROM ou cartouche de jeu, la RAM que l'on peut modifier, et la RAM de Lua.
La ROM du jeu
Vous avez probablement déjà lu le terme ROM dans le domaine de l'émulation de jeux par exemple. Il signifie Read Only Memory, ou mémoire morte en français. On l'utilise souvent pour désigner les cartouches de jeu, et comme son nom l'indique, le contenu d'une ROM est fixé et ne peut pas être modifié par l'utilisateur, contrairement à la RAM.
Pour plus de rapidité, dans la plupart des consoles de jeu et des ordinateurs, les données stockées dans une ROM sont copiées dans la RAM avant d'être traitées, et c'est aussi le cas dans PICO-8. La ROM (votre cartouche) est chargée au lancement du jeu, mais vous pouvez aussi recharger une partie précise de la cartouche (et même d'une autre cartouche) en plein jeu avec la fonction reload()
. Cela peut-être utile lorsque vous avez modifié les sprites ou la map dans la RAM et souhaitez réinitialiser leur état. Cependant, le code ne peut être rechargé de la même façon, car il est protégé et stocké dans un espace de RAM différent.
La RAM de base
C'est dans cet espace de 32 Kio que l'on pourra peek et poke pour modifier le comportement de PICO-8 pendant le jeu. La RAM est compartimentée en différentes sections, chaque adresse ayant une utilité précise :
Un carré équivaut à 128 octets.
Comme vous pouvez le voir, le début de la RAM, de "sprites" jusqu'à "sons", n'est autre que votre ROM chargée au lancement. On trouve ensuite :
- Un espace libre dans lequel vous pouvez stocker ce que vous souhaitez. Par exemple, vous pouvez y recopier une partie de l'écran pour sauvegarder une photo.
- Un fichier de sauvegarde pouvant contenir 64 chiffres et qui s'enregistre sur l'ordinateur du joueur ou de la joueuse.
- Le draw state et le hardware state, qui contiennent des adresses que l'on pourra poker pour obtenir des effets amusants.
- Les informations GPIO, qui permettent à votre jeu de communiquer avec des circuits électroniques ou une page web.
- Le contenu de l'écran, qui est modifié par les fonctions de dessin comme
spr()
.
Pour les plus curieuses et curieux d'entre vous, le fonctionnement de chacune de ces sections est détaillé sur notre wiki !
La RAM de Lua
Cet espace limité à 2 Mio contient le code de votre programme compilé ainsi que vos variables en cours de jeu. Elle est entièrement séparée de la RAM de base et vous ne pouvez pas y accéder avec peek, poke et compagnie. Cependant, comme je vous l'avais dit précédemment, vous pouvez connaître la quantité de mémoire actuellement occupée avec stat(0)
qui vous donne le nombre d'octets utilisés.
Les 2 Mio disponibles peuvent sembler énormes en comparaison des 32 Kio de la RAM de base, et il est vrai que cela vous laisse une très grande marge de manœuvre ! Vous n'aurez sans doute pas à vous soucier du nombre de variables dans votre jeu.
Pokes utiles
Désactiver le menu de pause
Les boutons btn(x)
pour x
allant de 0 à 5 sont les boutons documentés : les quatre flèches, X et O. En réalité, il existe aussi btn(6)
, correspondant au bouton Pause et déclenché avec Entrée ou Echap, mais il est impossible de l'utiliser normalement étant donné qu'il retourne true
pendant la frame juste avant que le menu de Pause ne s'ouvre.
C'était sans compter l'adresse 0x5f30, qui n'est pas documentée officiellement mais peut désactiver la prochaine ouverture du menu de pause avec la valeur 1. Il faut remettre cette valeur à 1 après chaque tentative bloquée ; je vous suggère donc l'écriture suivante :
if (btn(6)) poke(0x5f30, 1)
Vous disposez maintenant d'un bouton supplémentaire, qui pourrait vous servir à créer votre propre menu de pause.
Options graphiques
L'adresse 0x5f2c concerne le mode d'affichage. En changeant quelques bits, vous pouvez étirer l'écran, le retourner ou créer un effet miroir ! Voici un petit résumé des possibilités juste pour vous :
Valeur | Effet |
---|---|
0 | Normal |
1 | Etirement horizontal |
2 | Etirement vertical |
3 | 1+2 : Etirement horizontal et vertical |
5 | Miroir vertical : moitié gauche recopié à droite |
6 | Miroir horizontal : moitié haute recopié en bas |
7 | 5+6 : quart haut-gauche recopié trois fois |
129 | Retournement horizontal |
130 | Retournement vertical |
131 | 129+130 |
133 | Rotation 90 degrés |
134 | Rotation 180 degrés (même résultat que 131) |
135 | Rotation -90 degrés |
Tous ces effets sont amusants mais ne pourraient pas forcément servir dans un jeu conventionnel. Cela dit, je trouve la valeur 3 intéressante : c'est comme si on faisait un jeu en 64×64 pixels !
Utiliser le clavier et la souris
Les boutons de la manette sont chacun branchés à un emplacement précis de la mémoire et allument un bit. En fait, la fonction btn()
lit la mémoire pour savoir si un bouton est appuyé ou non ! Mais ce n'est pas tout : une adresse de la mémoire peut aussi contenir l'état du clavier et de la souris. Par défaut, cette fonctionnalité est désactivée, mais on peut poker une adresse pour activer le mode devkit :
poke(0x5f2d, 1)
Il devient alors possible d'accéder à l'état du clavier et de la souris avec stat()
!
Clavier
stat(30)
est un booléen qui vaut vrai lorsqu'une touche du clavier parmi celles reconnues est appuyée, avec un fonctionnement proche de btnp
. stat(31)
renvoie un string contenant la lettre ou le caractère en question. Quelques touches spéciales sont aussi reconnues :
Touche | stat(31) |
---|---|
Retour arrière | "\b" |
Tab | "\t" |
Entrée | "\r" |
N'oubliez pas que P et Entrée ouvrent le menu de pause, mais qu'on peut le désactiver avec if (btn(6)) poke(0x5f30, 1)
.
La touche Shift ne peut être reconnue par elle-même mais permet de produire des symboles plutôt que des lettres, de la même façon que dans l'éditeur de code. Les autres touches, telles que Control et Alt, ne produisent pas de caractère et ne sont donc pas reconnues.
stat(31)
ne peut contenir qu'un caractère à la fois, mais il est tout de même possible de reconnaître que plusieurs touches sont appuyées au même moment. Lorsque votre jeu constate que stat(30)
est vrai puis lit le contenu de stat(31)
, PICO-8 en rafraîchit immédiatement la valeur pour passer au caractère suivant à lire, ou bien passe stat(30)
à faux s'il n'en reste plus.
keys = {}
while stat(30) do
add(keys, stat(31))
end
Souris
Le support de la souris est lui aussi assez particulier.
Pour commencer, stat(32)
et stat(33)
vous donnent tout simplement les coordonnées X et Y de la souris, ce qui vous permet d'afficher un curseur que vous aurez dessiné vous-même dans un sprite.
Là où c'est plus original, c'est en ce qui concerne les boutons. stat(34)
renvoie une suite de 3 bits pour les 3 boutons de la souris :
stat(34) | Boutons |
---|---|
0b001 | Clic gauche |
0b010 | Clic droit |
0b100 | Clic molette |
Je vous invite à tester print(stat(34))
et vous verrez que le résultat est affiché en décimal, ce qui produit un nombre allant de 0 à 7. Nous pourrions évaluer les 7 cas de figure possibles avec 7 conditions (par exemple, 6 veut dire qu'on appuie sur le clic droit et la molette), mais nous ne sommes pas des sauvages et je suis là pour vous apprendre des choses ! Je vous propose plutôt de réaliser une opération bit à bit, afin de regarder directement si le bit qu'on cherche est activé ou non !
Mettons que vous souhaitiez vérifier le bit du clic gauche, c'est à dire 0b001
. Nous allons vérifier si stat(34)
a ce bit d'activé à l'aide de l'opération AND. Voici quelques exemples :
101 000 001 <-- valeurs possibles de stat(34)
AND 001 AND 001 AND 001 <-- bit du clic gauche
= 001 = 000 = 001
Pour résumer, band(stat(34), 0b001)
renverra le résultat 0b001 si les deux nombres ont le bit en commun, et renverra 0b000 sinon. Voici comment vérifier les trois types de clic :
Pour plus de concision, j'ai écrit 1, 2 et 4, qui sont les écritures décimales de 0b001, 0b010 et 0b1000. Les deux écritures sont toujours interchangeables !
Si les opérations bit à bit vous intéressent, je vous laisse consulter la section Bitwise Operations du manuel de PICO-8 ainsi que les exemples de Wikipédia.
Pour finir, stat(36)
vous donne ce qu'a parcouru la molette depuis la dernière frame. Cela donnera -1 si vous l'avez faite défiler d'un cran vers le haut, 0 si elle n'a pas bougé et 1 pour un cran vers le bas.
Méfiez-vous cependant : zep, le développeur de PICO-8, indique que la souris ne fonctionne pas encore idéalement sur navigateur, mais cela semble tout de même satisfaisant pour la plupart des jeux.
Gardez également à l'esprit que toutes les machines exécutant PICO-8 ne disposent pas forcément d'une souris et d'un clavier complet ; il est donc recommandé de rendre le mode devkit optionnel lorsque vous publiez votre jeu sur le site officiel. La première fois qu'une de ces stat()
est lue par le jeu dans un contexte où la présence d'un clavier et d'une souris n'est pas garantie, PICO-8 affiche un court avertissement en bas de l'écran.