Programmation avec le 6502 : les sprites de la NES, ou comment coder le jeu Pac-Man

Magazine
Marque
Hackable
Numéro
36
Mois de parution
janvier 2021
Spécialité(s)


Résumé

Dans le précédent article, nous avons commencé à nous familiariser avec la partie graphique de la console NES (Nintendo Entertainment System). Aujourd’hui, nous allons réaliser un véritable jeu, ou du moins nous allons suffisamment le débuter pour qu’il commence à être intéressant.


Body

Comme nous l’avons vu la dernière fois, le PPU (Pixel Processing Unit) de la NES permet d’afficher un décor statique grand comme deux fois l’écran et permet de le faire défiler au pixel près. Bien entendu, cela ne suffit pas vraiment pour faire un jeu intéressant.

Nous allons ajouter des éléments qui se déplacent (sprites) et la possibilité d’en déplacer un en particulier à l’aide du Joypad. Pour cela, j’ai choisi d’implémenter une partie du jeu Pac-Man qui est connu de tout le monde et qui est très documenté [1], ce qui permet d’en faire une version aussi fidèle que l’on veut.

1. Affichage du labyrinthe et scrolling

1.1 Le labyrinthe du Pac-Man officiel

Avant de nous lancer dans l’affichage des sprites proprement dit, il va falloir définir dans quel environnement ils vont évoluer et étudier un peu les règles du jeu que l’on veut implémenter. Heureusement, le Pac-Man dossier [1] va nous aider. On peut y voir que le terrain de jeu est composé de cases de 8x8 pixels, ce qui tombe bien pour nous. Là où ça se complique, c’est qu’il demande 28 cases horizontales par 36 verticales. Et même si on pourrait se passer de certaines lignes : deux en bas pour afficher le nombre de vies restantes et 3 en haut pour afficher le score, le labyrinthe en lui-même fait tout de même 31 cases (de 8 pixels) de haut, alors que la NES ne peut en afficher que 30 (soit 240 pixels).

Et il est recommandé de laisser 2 lignes en haut et en bas, car ces quatre lignes peuvent être mal affichées sur une vraie NES connectée à un vieil écran cathodique (et certains émulateurs reproduisent cette limitation). Il faut donc afficher 35 lignes comme on peut le voir sur la figure 1.

v-figure-1 base-laby

Figure 1 : Notre labyrinthe découpé en 32x35 cases.

Cependant, cela n’est pas vraiment un problème insurmontable puisque la NES offre la possibilité de « scroller » (faire défiler) le décor à peu de frais. C’est d’ailleurs ce qu’on avait fait lors du dernier article, simplement le défilement était horizontal, cette fois-ci, il sera vertical. Pour cela, il faudra modifier l’entête de notre « ROM » en indiquant que l’on veut un scrolling vertical :

;; L’entête pour les émulateurs
   DC.B "NES", $1a ; L’entête doit toujours commencer ainsi
   DC.B 1          ; Le nombre de boîtiers de 16 Ko de ROM CPU (1 ou 2)
   DC.B 1          ; Le nombre de boîtiers de 8 Ko de ROM PPU
   DC.B 0          ; Direction de scrolling et type de cartouche
                   ; Ici, on veut le type le plus simple (0)
                   ; avec un scrolling vertical (0 aussi)
   DS.B 9, $00     ; Puis juste 9 zéros pour faire 16 en tout

Et on utilisera les nametables $2000 pour la partie haute et $2800 pour la partie basse.

1.2. Lecture du Joypad

La dernière fois, le décor défilait tout seul. Aujourd’hui, nous allons contrôler ça à l’aide du Joypad, directement dans un premier temps, puis via la position de Pac-Man par la suite.

Il y a 8 boutons sur le Joypad de la NES (A, B, Select, Start, Haut, Bas, Gauche, Droite). On pourrait alors penser que l’état de ces ces huit boutons puisse être accédé directement dans un octet donné de la mémoire. Il n’en est rien. Nintendo a préféré une autre méthode, un peu plus complexe à mettre en œuvre, mais qui permet de supporter plein de types de manettes (et qui revient moins cher en électronique). Ainsi, pour lire l’état d’un Joypad, il faudra commencer par indiquer que l’on veut récupérer cet état en écrivant un 1 suivi d’un 0 à l’adresse $4016, qui appartient à l’espace d’adressage de l’APU, mais qui prend aussi en charge les Joypads et les transferts DMA. Ensuite, chaque lecture à la même adresse donnera un 1 si un bouton est pressé et un 0 s’il est relâché à cet instant. Il faudra donc 8 lectures pour avoir l’état des 8 boutons du Joypad classique de la NES. Si un autre périphérique est branché à la place d’un Joypad, on pourra avoir une valeur autre que 1 si un de ses boutons est pressé, mais cela sort du cadre de cet article.

Le bout de code suivant permet d’inspecter l’état du Joypad pour vérifier si les boutons Haut ou Bas sont pressés et met à jour la variable scroll_offset.

  ; Définition de cette adresse
JOYPAD1   EQU $4016
  ; Gestion du JoyPad
  LDA #1      ; On réinitialise la lecture du Joypad
  STA JOYPAD1 ;   en écrivant un 1
  LDA #0      ;     suivi d’un 0
  STA JOYPAD1 ;       dans cette adresse
 
  ; Ensuite, on peut lire l’état de chaque bouton :
  LDA JOYPAD1 ; Lecture du bouton A (ignoré)
  LDA JOYPAD1 ; Lecture du bouton B (ignoré)
  LDA JOYPAD1 ; Lecture du bouton Select (ignoré)
  LDA JOYPAD1 ; Lecture du bouton Start (ignoré)
 
  ; Test du bouton Up
  LDA JOYPAD1       ; Lecture du bouton Up
  AND #1            ; S’il n’est pas pressé
  BEQ +             ;    On passe à la suite
  LDA scroll_offset ; Si l’offset est à zéro
  BEQ +             ;    On passe à la suite
  DEC scroll_offset ; Sinon on diminue cet offset
+
 
  ; Test du bouton Down
  LDA JOYPAD1       ; Player 1 - Down
  AND #1            ; S’il n’est pas pressé
  BEQ +             ;    On passe à la suite
  LDA scroll_offset ; Si l’offset
  CMP #64           ;   vaut 64 (8 lignes de 8 pixels)
  BEQ +             ;    On passe à la suite
  INC scroll_offset ; Sinon on l’augmente de 1
+

Ceci, combiné au programme de la dernière fois permet d’avoir notre première étape : un labyrinthe qui s’affiche et défile comme on peut le voir sur la figure 2.

Le code complet de ce programme est disponible sur le GitHub du magazine ([2]) dans le répertoire programmation-NES/01-scrolling-laby/. Ce répertoire contient plusieurs fichiers, dont scrolling-laby.nes destiné à être utilisé directement sur un émulateur tel que FCEUX [3] ou Nestopia [4] par exemple. Mais vous pouvez aussi récupérer le code source et l’assembler vous-même (et le modifier !) avec la ligne de commande suivante :

asm6 scrolling-laby.asm scrolling-laby.nes

Cela nécessite l’assembleur ASM6 [5], qui est celui que j’utilise pour cette série d’articles depuis le début.

Notre rédacteur en chef préféré a déniché un émulateur / compilateur en ligne supportant de multiples consoles rétro (voir [6]). La NES est évidemment une des consoles supportées et on peut tester différents programmes exemples écrits en assembleur ou en C, et même les modifier en voyant le résultat immédiatement. Malheureusement, l’interface permettant de créer de nouveau projet n’est pas complète et il est difficile que j’y mette mes programmes exemples pour qu’ils soient facilement modifiables. Cependant, ce site est en constante évolution, et des améliorations pourraient bien arriver rapidement.

 

figure-2 scrolling-laby

Figure 2 : Le labyrinthe vide dans Nestopia.

2. Affichons Pac-Man

Afficher un labyrinthe, c’est bien, mais il est un peu vide. Pour ajouter de la vie à tout cela, la NES permet d’ajouter des objets par-dessus le décor. En effet, en plus de l’affichage des nametables qui sont assez lourdes à mettre à jour, le PPU prend automatiquement en charge l’affichage de 64 sprites.

Rappelons que le décor est composé de deux écrans de 30 lignes de 32 cases de 8x8 pixels, soit 1920 cases. Et chacune de ces cases doit être choisie parmi 256 tuiles définies une fois pour toutes dans la partie graphique de la ROM de la cartouche de jeu. Cette ROM contient en fait deux banques de 256 tuiles et si la première est réservée pour le décor, la seconde est réservée pour les sprites : les objets animés qui évoluent dans le décor. À chaque instant, on peut donc afficher 64 sprites de 8x8 à choisir parmi les 256 disponibles. Et pour savoir quoi afficher et où afficher, le PPU dispose d’une mémoire interne de 256 octets (4 par sprite) spécialement prévue à cet effet. Cette mémoire est appelée OAM, pour « Object Attribute Memory », la mémoire des attributs des objets.

Dans cette mémoire, on trouvera pour chaque sprite les informations suivantes, dans l’ordre :

  • un octet pour la position en Y du coin supérieur gauche du sprite sur l’écran ;
  • un octet pour le numéro de tuile à utiliser pour ce sprite ;
  • un octet d’attribut (sous-palette de couleur entre autres) ;
  • un octet pour la position en X du coin supérieur gauche du sprite sur l’écran.

Oui, l’ordre de ces octets est assez bizarre et j’avoue que je n’en connais pas la raison. Notez que les coordonnées X et Y sont relatives au coin supérieur gauche de l’écran, quelque soit la valeur de décalage (scrolling) du décor. Les sprites ne défilent donc pas avec le reste. Et comme l’écran a une taille de 256x240, tous les sprites qui ont un Y supérieur ou égal à 240 seront invisibles, et il s’agit d’ailleurs de la seule façon de cacher un sprite.

L’octet d’attribut contient le numéro de sous-palette de ce sprite dans ses deux bits de poids faible, et d’autres informations que nous allons voir plus tard. En effet, comme pour le décor, les tuiles ne peuvent utiliser que 3 couleurs plus une qui est commune (et transparente dans le cas des sprites), et comme pour le décor, il est possible de définir 4 ensembles (appelés sous-palettes) de 3 couleurs.

Tout cela sera probablement plus simple à comprendre avec un schéma et un exemple.

v-figure-3 pacman-sprite

Figure 3 : Pac-Man composé de 4 sprites.

Comme on peut le voir sur la figure 3, Pac-Man est composé de 4 sprites : celui en haut à gauche sera associé à la tuile numéro 8, celui en haut à droite à la tuile numéro 9, etc. Les « couleurs » utilisées sont le jaune et le transparent, que l’on retrouve dans la première sous-palette, les attributs de ces quatre sprites seront donc tous 0 (numéro de la première palette). Et on voudra placer ces sprites aux coordonnées (128,128), (136,128), (128,136) et (136,136).

Pour le premier sprite, on voudra donc transmettre les valeurs 128, 8, 0, 128 (rappelez-vous de l’ordre X, tuile, attribut, Y). Pour le second 128, 9, 0, 136. Puis 136, 10, 0, 128 et 136, 11, 0, 136.

Reste à savoir comment transmettre tout cela. Le PPU possède deux registres à cet effet : OAMADDR ($2003) qui permet d’indiquer à partir de quelle adresse OAM (de 0 à 255) on veut écrire des informations et OAMDATA ($2004) qui nous permettra de donner chacune de ces informations. Heureusement, à chaque fois que l’on écrit dans OAMDATA, OAMADDR est automatiquement incrémenté (comme pour PPUDATA et PPUADDR), ce qui permet de définir l’adresse une fois seulement pour toute une série de transferts de données.

Ainsi, pour afficher la totalité du Pac-Man, on aura le code suivant :

draw_pacman:
  LDA #0
  STA OAMADDR
 
  LDA #128     ; Y
  STA OAMDATA
  LDA #8
  STA OAMDATA ; Tile
  LDA #0
  STA OAMDATA ; Attr
  LDA #128     ; X
  STA OAMDATA
 
  LDA #128     ; Y
  STA OAMDATA
  LDA #9
  STA OAMDATA ; Tile
  LDA #0
  STA OAMDATA ; Attr
  LDA #136     ; X
  STA OAMDATA
 
  LDA #136     ; Y
  STA OAMDATA
  LDA #10
  STA OAMDATA ; Tile
  LDA #0
  STA OAMDATA ; Attr
  LDA #128     ; X
  STA OAMDATA
 
  LDA #136     ; Y
  STA OAMDATA
  LDA #11
  STA OAMDATA ; Tile
  LDA #0
  STA OAMDATA ; Attr
  LDA #136     ; X
  STA OAMDATA
 
  RTS

On reconnaît la série de 16 (4x4) groupes de LDA/STA qui permettent d’écrire les données de ces 4 sprites dans la mémoire OAM qui est initialisée à 0 : les quatre sprites modifiés seront donc les quatre premiers.

Vous pouvez trouver l’entièreté de ce programme sur le GitHub du magazine [2], dans le répertoire programmation-NES/02-pacman-sprite. La seule différence avec le programme précédent est ce bout de code et son appel qui est réalisé dans la routine VBL et non pas dans la partie « boucle principale ». En effet, le PPU de la NES ne peut faire qu’une seule chose à la fois : quand il est occupé à dessiner l’écran (la majorité du temps, en fait), on ne peut pas écrire dans ses registres de manière fiable. Il faut donc le faire pendant le temps qui dure depuis après l’affichage de la dernière ligne d’une frame jusqu’à l’affichage de la première ligne de la frame suivante. Ce temps est très court, mais heureusement, le hardware de la NES fait que la routine VBL est appelée automatiquement dès que le PPU a fini de dessiner la 240e ligne. Toutes les écritures de mise à jour de l’écran devront donc se passer dans cette routine. La partie principale devra donc se cantonner à tester les Joypads, mettre à jour des variables générales, mettre en place la logique du jeu, etc., ce qui l’occupera déjà bien.

figure-4 pacman-sur-laby

Figure 4 : Pac-Man « posé » sur le labyrinthe.

La figure 4 montre le résultat de programme, avec Pac-Man posé un peu n’importe où sur le labyrinthe. Si vous testez ce programme dans un émulateur (ou sur une une vraie NES, si vous avez la chance d’en posséder une), vous verrez que le labyrinthe semble glisser sous Pac-Man lorsqu’on le déplace. Cela est normal, les coordonnées de Pac-Man sont pour l’instant fixées par rapport à l’écran. Nous allons corriger cela bientôt.

3. Et maintenant, il bouge !

On arrive donc à avoir des choses affichées qui sont indépendantes de la position du décor, ce qui est la base de tout jeu. Cependant, cette façon de faire est assez limitée, car cela demande de mettre beaucoup de code dans la fonction VBL, et nous venons de voir qu’il vaut mieux réserver le contenu de cette fonction pour les différentes mises à jour du PPU.

Dans la pratique, on préférera toujours préparer les données à envoyer au PPU dans la boucle principale dans le format le plus compact possible et envoyer ces données rapidement pendant la VBL. Pour l’exemple des 64 sprites, les données en question sont donc 64 x 4 = 256 octets. On pourrait donc avoir dans la VBL une simple boucle qui lit ces 256 octets et les envoie un par un au PPU en écrivant dans OAMDATA.

Mais, même ainsi, cela prendrait beaucoup de temps. Il est possible d’aller au moins 3 fois plus vite !

3.1. La gestion du DMA

En effet, la NES propose pour cela (et uniquement pour cela) un transfert DMA (Direct Memory Access). Il s’agit d’un mécanisme qui arrête complètement le CPU et le PPU le temps d’un transfert de 256 octets depuis la mémoire reliée au CPU (RAM ou ROM) justement vers la zone où le PPU stocke le paramétrage des sprites. Cette opération prend 512 cycles (256 lectures et 256 écritures), et pour la déclencher, il suffit d’écrire une valeur dans le registre OAMDMA (qui est à l’adresse $4014). Appelons cette valeur DMA. Le transfert se fera alors depuis l’adresse DMA * 256 + OAMADDR.

Ainsi, si on stocke le paramétrage des sprites à partir de l’adresse $200, les quelques lignes suivantes (à mettre dans la fonction VBL, donc) suffisent à transférer les 64 sprites :

  ; Envoi des 64 sprites par DMA
  LDA #0       ; Poids faible d’abord
  STA OAMADDR ;
  LDA #2       ; Puis le poids fort.
  STA OAMDMA

Et cela remplacera l’appel JSR draw_pacman que l’on avait précédemment.

3.2. Les variables concernant Pac-Man

Pour le programme de cette partie, on va également en profiter pour faire bouger notre Pac-Man à l’aide du Joypad. Mais pour cela, on va devoir définir un certain nombre de variables, donc des positions en RAM. La zone $000-$0ff est normalement réservée pour les variables « rapides », de $100 à $1ff, il y a la pile d’appels, de $200 à $2ff, il y aura les paramètres des sprites, on va donc mettre les variables générales à partir de $300.

On déclarera donc au début de la page $300 les variables suivantes :

pac_position_x DS.B 1 ; Coordonnée en X de la case où est Pac-Man
pac_position_y DS.B 1 ;               Y
pac_sub_step_x DS.B 1 ; Coordonnée en X dans la case (de -7 à +7)
pac_sub_step_y DS.B 1 ;               Y
pac_direction_x DS.B 1 ; Direction en X (-1, 0 ou 1)
pac_direction_y DS.B 1 ;              Y
pac_next_dir_x DS.B 1 ; Prochaine direction en X
pac_next_dir_y DS.B 1 ;                        Y
pac_orientation DS.B 1 ; 0 = gauche, 1 = droite, 2 = haut, 3 = bas
pac_anim        DS.B 1 ; Ouverture de la bouche de Pac-Man.

Quelques explications s’imposent. J’ai choisi de repérer Pac-Man par sa position en termes de case dans le labyrinthe (entre 0 et 31 en X par exemple). Cela permettra de facilement tester si on se dirige vers un mur ou si on avale une pilule. Mais pour l’affichage, on aura besoin d’un positionnement plus précis, à l’intérieur d’une case. C’est ce que j’ai appelé sub_step, qui vaut entre -7 et +7. La position en X du Pac-Man à l’écran sera alors pac_position_x * 8 + pac_sub_step_x. Les multiplications par 8 sont particulièrement simples à réaliser sur le 6502.

Les autres variables ont, je pense, un nom assez parlant, ou sont suffisamment commentées.

Au démarrage du programme, ces variables sont toutes initialisées à 0, sauf pac_position_x et pac_direction_y qui le sont respectivement à 15 et 21 afin de placer Pac-Man dans sa position de départ habituelle dans le labyrinthe :

init_pac:
  LDA #15             ; On initialise la position
  STA pac_position_x ;   de Pac-Man à
  LDA #21             ;   (15, 21)
  STA pac_position_y
  LDA #0              ; Et tout le reste à 0
  STA pac_direction_x
  STA pac_direction_y
  STA pac_sub_step_x
  STA pac_sub_step_y
  STA pac_next_dir_x
  STA pac_next_dir_y
  STA pac_orientation
  STA pac_anim
  RTS

Un généreux relecteur m’a fait remarquer qu’il était inutile d’initialiser ces variables à zéro puisqu’on initialise déjà toute la RAM disponible à cette valeur. C’est vrai, cependant, j’aime bien faire les choses explicitement. J’en ai profité pour modifier l’initialisation générale et mettre la valeur 255 partout dans la page $200. Cela initialisera en particulier les positions Y de tous les sprites à 255, mais comme seuls ceux qui ont un Y inférieur à 240 sont affichés, ce changement permet donc de cacher tous les sprites par défaut.

3.3 Le Joypad

La gestion du Joypad va maintenant être plus complète. En effet, on va maintenant gérer les 4 directions dans lesquelles Pac-Man peut se déplacer. Mais n’oublions pas qu’on se déplace sur un ensemble de cases de 8x8 pixels, et pour simplifier les choses, on va supposer que Pac-Man se déplace d’un pixel à chaque frame (50 fois par seconde). Un appui sur une direction du Joypad ne mettra pas directement à jour les variables pac_direction_[xy], mais les variables pac_next_dir_[xy] qui seront utilisées par la suite.

Ce n’est pas très compliqué à réaliser, on aura ce genre de code pour chacune des directions (ici, pour aller à droite) :

  ; Test du bouton Right
  LDA JOYPAD1        ; Lecture du bouton Right
  AND #1             ; S’il n’est pas pressé
  BEQ +              ;    On passe à la suite
  LDA #1             ; Sinon, on enregistre l’envie
  STA pac_next_dir_x ;    d’aller à droite :
  LDA #0             ;     dx = 1, dy = 0
  STA pac_next_dir_y
+

3.4. Le déplacement de Pac-Man

Pour gérer le déplacement de Pac-Man, on a deux grands cas : soit il est entre deux cases et il continue simplement son mouvement actuel (en ajoutant pac_direction_[xy] à pac_position_[xy]), soit il est exactement au centre d’une case et il pourra choisir une nouvelle direction.

Dans ce deuxième cas, on modifiera les variables pac_position_[xy] avant de remettre les pac_next_dir_[xy] à zéro. Puis, on mettra à jour la nouvelle direction en la copiant de pac_next_dir_[xy].

Et dans tous les cas, on mettra à jour le numéro de séquence de l’animation de la bouche de Pac-Man (INC pac_anim). Et comme on sait maintenant dans quelle direction Pac-Man se déplace, on le fera regarder devant lui. Par exemple, le bout de code suivant mettra la variable pac_orientation à la valeur PACMAN_FACING_LEFT dès que sa direction en X sera égale à -1 :

  LDA pac_direction_x      ; Si Pac-Man ne va pas
  CMP #-1                  ;   vers la gauche,
  BNE +                    ;   on passe à la suite
  LDX #PACMAN_FACING_LEFT ; Sinon, on met à jour
  STX pac_orientation      ;   la variable pac_orientation

Je vous laisse étudier le code de la fonction move_pacman en détail pour déceler les dernières subtilités que j’ai utilisées pour mettre toutes ces variables à jour. Les commentaires devraient vous aider à vous y retrouver.

3.5. Affichage du Pac-Man animé

Maintenant que nous avons toutes les variables nécessaires à disposition, nous pouvons nous pencher sur l’affichage de Pac-Man proprement dit.

Cette partie se fera dans la fonction draw_pacman qui sera appelée directement dans la boucle principale. En effet, comme nous utilisons le transfert DMA décrit plus haut, il nous suffit de remplir la page de $200 à $2ff pour décrire nos sprites. Et comme Pac-Man est composé de 4 sprites, il nous faudra remplir les octets correspondants aux coordonnées X et Y, au numéro de tuile, et d’attribut, soit 16 octets au total. Heureusement, nous n’avons plus à transmettre ces données dans l’ordre bizarre Y, tuile, attribut, X.

On commencera donc par les coordonnées X et Y, et pour que tout soit plus facile à écrire (et à lire, j’espère !), on donnera des noms à chacun de ces 16 octets. Par exemple, au lieu d’utiliser directement $207, on pourra écrire SPR_PAC_NE_X pour indiquer qu’il s’agit de la coordonnée en X du sprite en haut à droite (North-East).

Commençons par les coordonnées en X. pac_position_x contient la position en X de la case sur laquelle est Pac-Man, et comme les cases font 8 pixels de large, il faudra multiplier cette valeur par 8, ce qui se fait bien en assembleur 6502 par juste trois décalages à gauche (ASL). À cela, il faudra ajouter le déplacement à l’intérieur de la case, pac_sub_step_x. Et comme on peut le voir sur la figure 5, Pac-Man déborde de la case dans laquelle il est situé (en rouge), il faut donc soustraire 4 pixels pour avoir le coin en haut à gauche. Les deux sprites de la partie droite sont juste 8 pixels à droite des deux de gauche.

v-figure-5 pacman-sur-case

Figure 5 : Les sprites de Pac-Man dans la position initiale.

Cela nous donne donc le code suivant pour le calcul des coordonnées en X :

  LDA pac_position_x ; On récupère la position (en cases)
  ASL                 ; en X de Pac-Man
  ASL                 ;   que l’on multiplie
  ASL                 ;   par 8
  CLC                 ;   et on ajoute
  ADC pac_sub_step_x ;   la sous-position
  SBC #4              ; et on enlève 4 pixels
  STA SPR_PAC_NW_X    ; pour la partie gauche
  STA SPR_PAC_SW_X    ;     des sprites
  CLC
  ADC #8              ; et on ajoute 8 pixels
  STA SPR_PAC_NE_X    ;    pour les sprites
  STA SPR_PAC_SE_X    ;    de la partie droite

Et on a le même genre de code pour la coordonnée Y.

v-figure-6 tuiles

Figure 6 : Les tuiles des sprites de notre jeu.

Il faut ensuite indiquer quelles tuiles utiliser. J’ai mis sur la figure 6 l’ensemble des tuiles des sprites du jeu. Comme on peut le voir, il y a les graphismes pour Pac-Man et pour les fantômes. On va donc s’intéresser pour l’instant uniquement aux 24 premières tuiles qui vont permettre de représenter Pac-Man de 6 manières différentes.

En effet, afin d’exprimer pleinement sa gloutonnerie, Pac-Man sera représenté avec trois positions de bouche : grande ouverte (tuiles de 0 à 7), moitié ouverte (de 8 à 15) et fermée (de 16 à 23), et deux directions : horizontale ou verticale (voir figure 7).

v-figure-7 pacman-anim

Figure 7 : Les phases d’animation du Pac-Man (vers la gauche).

L’animation de la bouche se fera en incrémentant un compteur pac_anim régulièrement, et en ne gardant que deux bits, ce qui donnera une valeur entre 0 et 3. Cela nous donnera la séquence 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3. En remplaçant les 3 par des 1 et en multipliant par 8, on obtient 0, 8, 16, 8, 0, 8, 16, 8, 0, etc. Ce qui est le numéro de tuile de la bonne phase d’animation (voir encore la figure 7). Le code suivant réalise cela :

  LDA pac_anim        ; On change l’animation
  LSR                 ;   une fois sur deux
  CLC                 ; On se décale de 1 pour
  ADC #1              ;   commencer par la bonne position
  AND #3              ; On ne garde que les valeurs de 0 à 3
  CMP #3              ;   et on transforme les 3
  BNE +               ;    en 1
  LDA #1              ;     pour avoir la séquence
+ ASL                 ;       0,1,2,1,0,1,2,1,0,1,2,1,0,1,etc.
  ASL                 ;     et on multiplie par huit pour avoir
  ASL                 ;       0,8,16,8,0,8,16,8,0,8,16,8,etc.

Et on peut ensuite remplir les numéros des tuiles ainsi :

  LDX pac_orientation      ; Il reste à vérifier l’orientation de Pac-Man
  CPX #PACMAN_FACING_LEFT ; S’il va à gauche,
  BNE +
  TAY                 ; On copie A dans Y
  STY SPR_PAC_NW_TILE ; qui contient le numéro de la tuile North West (NW)
  INY                 ; Et les suivantes
  STY SPR_PAC_NE_TILE ; se suivent
  INY                 ;    dans l’ordre
  STY SPR_PAC_SW_TILE ; NW, NE, SW et SE
  INY                 ; Par exemple, si A vaut 8
  STY SPR_PAC_SE_TILE ; on utilisera les tuiles 8, 9, 10, 11

Si Pac-Man se déplace vers le haut, on ajoutera simplement 4 au numéro de tuile et on aura le même genre de code.

Cependant, vous l’aurez sans doute remarqué, nous ne disposons pas des tuiles permettant de dessiner Pac-Man allant vers la droite ou vers le bas. Ce n’est pas un oubli, c’est même très fréquent dans les jeux NES. Le PPU est en effet capable de retourner les tuiles à la volée, soit horizontalement, soit verticalement, soit les deux. Et cela est très facile à mettre en œuvre : il suffit de positionner le bit 7 (pour une symétrie verticale) ou le bit 6 (pour une symétrie horizontale) de l’octet d’attribut correspondant. Les attributs contiennent donc à la fois les symétries et la sous-palette de couleurs pour chaque sprite.

Dans notre cas, la sous-palette pour Pac-Man est la 0 (les autres sont pour les fantômes), et les attributs seront donc 0 s’il va à gauche ou en haut, $40 (bit 6 positionné) s’il va à droite et $80 (bit 7 positionné) s’il va vers le bas.

Je vous laisse regarder en détail le contenu de la fonction draw_pacman du fichier rotating-pacman.asm pour comprendre les derniers détails permettant d’afficher Pac-Man.

Avec tout cela, la taille du fichier source de notre programme a presque doublé, mais on a un comportement qui est tout de même nettement plus attractif. Je vous encourage à tester le résultat : rotating-pacman.nes

4. La gestion des collisions

Même si on peut maintenant déplacer notre héros facilement et de manière fluide, on est encore loin du gameplay du jeu Pac-Man. Une partie importante est bien évidemment de respecter les murs du labyrinthe et donc empêcher Pac-Man de les traverser (ou de les chevaucher).

Pour cela, nous allons avoir besoin de trouver une façon astucieuse de représenter les murs qui soit facile à tester. Tout d’abord, on peut considérer qu’une case contient soit un mur, soit autre chose, un seul bit d’information suffit donc par case : un 1 pour un mur et un 0 pour du vide. Notre labyrinthe est composé de 31 lignes de 32 colonnes. On a donc besoin de 32 bits soit 4 octets par ligne.

En représentant ces octets en binaire, il est assez facile de se créer une table de 31 fois 4 octets dans laquelle on peut voir les chemins possibles représentés par les 0 :

  DC.B %11111111,%11111111,%11111111,%11111111 ; ligne 0
  DC.B %11100000,%00000001,%10000000,%00000111 ; ligne 1
  DC.B %11101111,%01111101,%10111110,%11110111 ; ligne 2
  DC.B %11101111,%01111101,%10111110,%11110111 ; ligne 3
  DC.B %11101111,%01111101,%10111110,%11110111 ; ligne 4
  ...
  DC.B %11100000,%00000000,%00000000,%00000111 ; ligne 29
  DC.B %11111111,%11111111,%11111111,%11111111 ; ligne 30

Vous trouverez évidemment l’intégralité de cette table dans le fichier source de cette partie 04-collisions.asm.

Il est donc facile de créer cette représentation, mais est-ce facile de tester si une case (X, Y) est un mur ou non ? Eh bien oui, cela se fait même avec assez peu de lignes d’assembleur. Il faut tout d’abord trouver le bon octet dans la table, qui est juste Y * 4 + X / 8 (que des opérations faciles à réaliser). Puis à l’intérieur de cet octet, il faut tester si le (X modulo 8)ième bit (en partant de la gauche) est positionné ou non. Le plus simple pour cela est d’utiliser la table suivante (n’oublions pas que le 6502 est particulièrement doué pour rechercher les valeurs dans une table).

bits:
DC.B %10000000
DC.B %01000000
DC.B %00100000
DC.B %00010000
DC.B %00001000
DC.B %00000100
DC.B %00000010
DC.B %00000001

Il suffira ensuite d’effectuer une opération AND entre une valeur de cette table et l’octet récupéré dans la table des murs. Si cette opération renvoie 0, c’est qu’il n’y a pas de mur à la position recherchée. Voici le bout de code correspondant à ce test :

check_for_wall:
  LDA param_y ; On multiplie param_y par 4
  ASL
  ASL
  STA tmp_var ; tmp_var = Y * 4
 
  LDA param_x ; On divise param_x par 8
  LSR
  LSR
  LSR
  CLC
  ADC tmp_var ; tmp_var = Y * 4 + X / 8
  TAX
  LDA wall_mask_8bit,X ; On récupère le bon octet
  STA tmp_var          ; de la table des murs
 
  LDA param_x ; Et la coordonnée en X modulo 8
  AND #7       ;   nous donne le bit à tester
  TAX
  LDA bits,X
  AND tmp_var ; Positionne le flag Z s’il n’y a pas de mur.
  RTS

Comme précédemment, vous pourrez trouver le code source complet de ce programme dans le répertoire programmation-NES/04-collisions du GitHub du magazine [2]. Si vous le testez, vous pourrez constater que maintenant notre Pac-Man respecte les murs, et peut même emprunter le fameux tunnel.

5. L’arrivée des fantômes

Un Pac-Man tout seul dans son labyrinthe risque fort de s’ennuyer ! On va lui adjoindre ses quatre fantômes favoris.

5.1. Affichons 4 fantômes

Afficher les fantômes ne pose pas de problème particulier, maintenant que l’on sait afficher Pac-Man. On aura vraiment le même genre de code. Il faut simplement réfléchir un peu pour les couleurs des fantômes.

Nos fantômes ont chacun leur couleur, plus du blanc pour les yeux (voir figure 8). Or, les sous-palettes de couleurs ne permettent de définir que 3 couleurs et le jaune de Pac-Man utilise déjà une place dans la première sous-palette (la 0). J’ai choisi d’utiliser la sous-palette 1 pour les couleurs des deux premiers fantômes (plus du blanc pour les yeux) et la sous-palette 2 pour les deux autres fantômes.

L’affichage des fantômes est évidemment pratiquement le même pour chaque fantôme. Aussi, la fonction draw_ghosts appellera quatre fois la fonction draw_one_ghost. Cette fonction, qui ressemble beaucoup à la fonction d’affichage de Pac-Man en plus simple (les fantômes n’y sont pas encore animés, et on ne gère pour l’instant que des cases entières), attend dans le registre X le numéro du fantôme à afficher et dans le registre Y l’emplacement dans la page $200 des données des sprites correspondants.

L’affichage des fantômes se résume donc à cette fonction :

; Affichage des 4 fantômes
; Ici, on appelle juste la sous fonction draw_one_ghost
; 4 fois, avec dans X le numéro de fantôme et dans Y
; l’emplacement en RAM, dans la page $200 où seront
; stockées les informations (coordonnées x et y, tuile
; et attribut) de chaque sprite du fantôme
draw_ghosts:
  LDX #0             ; Le premier fantôme, Blinky,
  LDY #$10           ; utilise de $210 à $21f
  JSR draw_one_ghost
  LDX #1             ; Le second, Pinky,
  LDY #$20           ; utilise de $220 à $22f
  JSR draw_one_ghost
  LDX #2             ; Le troisième, Inky,
  LDY #$30           ; utilise de $230 à $23f
  JSR draw_one_ghost
  LDX #3             ; Et le quatrième, Clyde,
  LDY #$40           ; utilise de $240 à $24f
  JSR draw_one_ghost
  RTS

Et l’affichage d’un fantôme commence ainsi :

; Affiche un fantôme
; Le numéro de fantôme doit être dans X
; Et l’endroit où seront stockées ses infos dans Y
draw_one_ghost:
  ; On commence par la coordonnée en x
  LDA ghost_position_x,X ; La position x en cases
  ASL                    ;   est multipliée
  ASL                    ;   par 8
  ASL
  SBC #4                 ; Puis on enlève 4 pixels
  STA SPR_NW_X,Y         ; pour la partie gauche
  STA SPR_SW_X,Y         ;    des sprites
  CLC
  ADC #8                 ; Et on ajoute 8 pixels
  STA SPR_NE_X,Y         ;   pour la partie droite
  STA SPR_SE_X,Y
  ...

Ce qui diffère de l’affichage de Pac-Man est le fait que la lecture des données (LDA) est indexée par le registre X qui contient le numéro du fantôme, et l’écriture des données dans la page $200 (STA) est indexée par le registre Y. Le reste est vraiment très semblable, c’est pourquoi je n’ai mis que le début de cette fonction ici.

Vous pourrez sans problème étudier le reste du code présent dans le fichier 05-fantomes/fantomes.asm ou tester le comportement de ce programme dans un émulateur.

Cependant, nous ne sommes pas au bout de nos peines. En effet, afin de mettre en évidence une nouvelle limitation de la NES, j’ai choisi de placer les fantômes sur la même ligne (voir figure 8, partie du haut). Cette situation est rare dans le jeu, mais peut toutefois arriver.

v-figure-8 pb-fantomes

Figure 8 : La limite des 8 sprites par ligne.

Si tout se passe bien sur le haut de la figure 8, dans les deux autres situations, c’est-à-dire lorsque Pac-Man est au même niveau, on voit que le pauvre Clyde (le quatrième fantôme) disparaît en partie. Cela est une limitation du PPU de la NES : il ne peut pas afficher plus de 8 sprites sur une même ligne de pixels. Et lorsque Pac-Man est au même niveau que les 4 fantômes, cela nous fait 10 sprites à afficher. Comme Clyde est le dernier de la liste, c’est lui qui fait les frais de ce problème.

Il n’y a pas vraiment de moyen de corriger cela totalement, mais on peut en réduire les effets. Si un fantôme disparaît complètement pendant quelques secondes du jeu, cela peut être très dommageable pour le joueur.

La solution que la plupart des jeux NES apportent à ce problème est de ne pas faire subir cette disparition soudaine toujours au même personnage. Il suffit pour cela de changer à chaque frame l’ordre des fantômes. Cela aura pour effet de faire disparaître chaque fantôme seulement un quart du temps. Sur un émulateur, cela le fait légèrement clignoter, et on retrouve effectivement cet effet de clignotement sur presque tous les jeux. Sur une vraie NES, connectée à une télé cathodique comme à l’époque, la rémanence fait que les fantômes deviennent simplement très légèrement transparents. Et ceci n’est pas très gênant (ce sont des fantômes après tout !) et rappelons-nous que cette situation est très rare dans le jeu.

5.2. La gestion des priorités des sprites

Maintenant que nous avons une idée de la solution du problème, essayons de l’implémenter.

Une manière de faire est de s’assurer d’afficher les fantômes dans l’ordre 0,1,2,3 puis 1,2,3,0, puis 2,3,0,1, etc. suivant les frames. Heureusement, avec notre manière de les afficher, cela est assez facile. La fonction draw_one_ghost reçoit dans le registre X le numéro de fantôme que l’on affiche, entre 0 et 3. Il suffit alors d’ajouter le numéro de la frame qui est justement disponible dans la variable vbl_cnt, et de garder le résultat modulo 4 (ce qui se fait avec l’opcode AND #3).

On a donc une solution très simple à ce problème qui semblait compliqué. Il suffit en effet de modifier le début de la fonction draw_one_ghost ainsi :

; Affiche un fantôme.
; Le numéro de fantôme doit être dans X
; Et l’endroit où seront stockées ses infos dans Y
draw_one_ghost:
  ; Rotation des associations fantôme <-> sprites
  TXA         ; On copie le numéro du fantôme dans A
  CLC         ;   et on ajoute une valeur qui change
  ADC vbl_cnt ;   toutes les frames
  AND #3      ; Puis, on se remet entre 0 et 3
  TAX         ; Avant de remettre la valeur dans X
  ; On commence par la coordonnée en x
  LDA ghost_position_x,X ; La position x en cases
  ...

Vous pouvez vérifier avec le programme présent dans le répertoire 06-priorités-des-sprites que la situation est maintenant beaucoup plus acceptable.

6. Déplacements des fantômes

Il reste encore beaucoup de choses à faire pour avoir un jeu complet, mais nous avons pratiquement fait le tour des possibilités et limitations des sprites sur la NES.

Avec ce que nous savons déjà, il est possible et presque facile d’ajouter des fonctionnalités comme l’affichage des pilules que Pac-Man peut manger (et leur disparition éventuelle après gloutonnerie), le déplacement plus ou moins intelligent des fantômes, la gestion des collisions entre Pac-Man et les fantômes, le score, la musique, etc. Mais cela sort du domaine de cet article (et de ce magazine).

Je vous propose une implémentation fortement commentée de certains de ces aspects dans le programme du répertoire programmation-NES/07-mouvements-et-pilules si cela vous intéresse.

7. La prochaine fois

La prochaine fois, nous explorerons des possibilités inattendues de la NES, comme les possibilités de scrollings différentiels ou l’extension des capacités de cette console par certains types de cartouches.

Références

[1] Pac-Man Dossier : https://www.gamasutra.com/view/feature/3938/the_pacman_dossier.php?print=1

[2] Le GitHub du magazine : https://github.com/Hackable-magazine/Hackable36

[3] FCEUX, un émulateur NES multiplateforme : https://fceux.com/web/home.html

[4] Nestopia, un émulateur NES multiplateforme : http://nestopia.sourceforge.net/

[5] ASM6, un assembleur pour la NES : http://3dscapture.com/NES/asm6.zip

[6] 8bitworkshop IDE : https://8bitworkshop.com/v3.7.0/?platform=nes



Article rédigé par

Par le(s) même(s) auteur(s)

Programmation avec le 6502 : vers des jeux plus évolués

Magazine
Marque
Hackable
Numéro
37
Mois de parution
avril 2021
Spécialité(s)
Résumé

Nous savons à présent comment exploiter les capacités du 6502 et du PPU de la NES afin de faire des jeux, comme le Pac-Man présenté lors du dernier article. J'espère d'ailleurs que certains d'entre vous ont essayé, et sont parvenus à améliorer ce programme, disponible sur le GitHub du magazine. Aujourd'hui, nous allons voir que les cartouches de jeux elles-mêmes peuvent renfermer des trésors d'ingéniosité électronique, permettant d'augmenter les capacités de base de la console.

Programmation avec le 6502 : découverte de la NES

Magazine
Marque
Hackable
Numéro
34
Mois de parution
juillet 2020
Spécialité(s)
Résumé

Dans les articles précédents, nous avons étudié de près le langage d'assemblage du microprocesseur 6502. Et même si j'ai essayé d'étayer le tout avec beaucoup d'exemples, tout cela est resté très théorique. Aujourd'hui, nous allons vraiment passer à la pratique en réalisant des programmes graphiques pouvant s'exécuter sur une véritable console NES ou sur un émulateur.

Programmation avec le 6502 : trigonométrons !

Magazine
Marque
Hackable
Numéro
33
Mois de parution
avril 2020
Spécialité(s)
Résumé

Lors du précédent article, nous avons parcouru les différents modes d'adressage du 6502, ce qui nous a permis d'élaborer quelques algorithmes simples, notamment pour réaliser des additions ou soustractions sur des nombres entiers de plus de 8 bits et même, des multiplications. Aujourd'hui, nous allons continuer dans cette voie en nous intéressant à la division et même aux nombres décimaux (à virgule), ce qui nous permettra de mettre un pied dans le monde effrayant de la trigonométrie !

Les derniers articles Premiums

Les derniers articles Premium

Stubby : protection de votre vie privée via le chiffrement des requêtes DNS

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Depuis les révélations d’Edward Snowden sur l’espionnage de masse des communications sur Internet par la NSA, un effort massif a été fait pour protéger la vie en ligne des internautes. Cet effort s’est principalement concentré sur les outils de communication avec la généralisation de l’usage du chiffrement sur le web (désormais, plus de 90 % des échanges se font en HTTPS) et l’adoption en masse des messageries utilisant des protocoles de chiffrement de bout en bout. Cependant, toutes ces communications, bien que chiffrées, utilisent un protocole qui, lui, n’est pas chiffré par défaut, loin de là : le DNS. Voyons ensemble quels sont les risques que cela induit pour les internautes et comment nous pouvons améliorer la situation.

Surveillez la consommation énergétique de votre code

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Être en mesure de surveiller la consommation énergétique de nos applications est une idée attrayante, qui n'est que trop souvent mise à la marge aujourd'hui. C'est d'ailleurs paradoxal, quand on pense que de plus en plus de voitures permettent de connaître la consommation instantanée et la consommation moyenne du véhicule, mais que nos chers ordinateurs, fleurons de la technologie, ne le permettent pas pour nos applications... Mais c'est aussi une tendance qui s'affirme petit à petit et à laquelle à terme, il devrait être difficile d'échapper. Car même si ce n'est qu'un effet de bord, elle nous amène à créer des programmes plus efficaces, qui sont également moins chers à exécuter.

Donnez une autre dimension à vos logs avec Vector

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Avoir des informations précises et détaillées sur ce qu’il se passe dans une infrastructure, et sur les applications qu'elle héberge est un enjeu critique pour votre business. Cependant, ça demande du temps, temps qu'on préfère parfois se réserver pour d'autres tâches jugées plus prioritaires. Mais qu'un système plante, qu'une application perde les pédales ou qu'une faille de sécurité soit découverte et c'est la panique à bord ! Alors je vous le demande, qui voudrait rester aveugle quand l'observabilité a tout à vous offrir ?

Les listes de lecture

9 article(s) - ajoutée le 01/07/2020
Vous désirez apprendre le langage Python, mais ne savez pas trop par où commencer ? Cette liste de lecture vous permettra de faire vos premiers pas en découvrant l'écosystème de Python et en écrivant de petits scripts.
11 article(s) - ajoutée le 01/07/2020
La base de tout programme effectuant une tâche un tant soit peu complexe est un algorithme, une méthode permettant de manipuler des données pour obtenir un résultat attendu. Dans cette liste, vous pourrez découvrir quelques spécimens d'algorithmes.
10 article(s) - ajoutée le 01/07/2020
À quoi bon se targuer de posséder des pétaoctets de données si l'on est incapable d'analyser ces dernières ? Cette liste vous aidera à "faire parler" vos données.
Voir les 88 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous