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.
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.
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 :
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.
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 :
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.
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.
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 :
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.
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 :
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 :
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 :
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) :
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 :
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.
Cela nous donne donc le code suivant pour le calcul des coordonnées en X :
Et on a le même genre de code pour la coordonnée Y.
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).
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 :
Et on peut ensuite remplir les numéros des tuiles ainsi :
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 :
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).
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 :
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 :
Et l’affichage d’un fantôme commence ainsi :
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.
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 :
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