This page is at least 11 years old !
Cet article est terminé.
Il y a déjà un bon moment, j'avais fait une vidéo traitant de la programmation sur Gameboy où je montrais vaguement comment faire un lecteur de son court sans vraiment trop entrer dans les détails. Cette vidéo était censée être le pilote d'une série, mais comme bien sûr je n'ai pas du tout eu le courage de faire les scripts, les exemples, les prises, et le montage pour la suite, j'en viens aujourd'hui à faire un seul et unique "tuto" de base dans lequel je vais montrer comment réaliser un pong infini de A à Z.
J'ai fait ce choix qui peut paraître douteux car programmer sur
ce genre de console devient vite compliqué, non seulement
à comprendre mais aussi à expliquer. Dans un souci de clarté et d'honnêteté, je propose tout d'abord de télécharger le programme final, qu'on peut lancer avec un émulateur (logiciel simulant une Gameboy sur PC, j'en parle plus tard dans la partie "Outils"). Passionnant comme jeu, n'est-ce pas ? Dernière chose avant de commencer: ce tuto a pris beaucoup plus que deux heures à réaliser. Par simple respect donc, je demanderai à ceux qui veulent le copier de préciser son origine en donnant un lien vers cette page. Merci !
Choisir un langage pour coder sur Gameboy est simple, car il n'y a vraiment qu'une solution fiable: l'assembleur. Il existe des compilateurs C qui peuvent fonctionner, mais avoir un tel niveau d'abstraction sur une machine si simple a tendance à me déranger un peu... Surtout quand il est question de tirer un maximum de celle-ci. C'est pour ça que personnellement, j'aime coder en assembleur:
on se fait très chier pour pas grand chose, mais on a le
contrôle sur TOUT. Je sais, le logiciel porte le même nom que le langage, ça peut porter à confusion mais j'y peux rien. Ces nombres seront ensuite inscrits dans une mémoire (un circuit intégré), qu'on appelle ROM. Une mémoire en électronique est simplement un tableau avec des cases numérotés (adresse) dans lesquelles on inscrit des nombres (données). ROM signifie Read Only Memory, c'est un type de mémoire qu'on ne peut que lire (on la programme une seule fois). C'est aussi le nom qu'on donne aux fichiers informatiques qui contiennent le contenu de ces circuits intégrés. C'est un abus de langage et le vrai nom devrait être en fait "ROM dump" (vidange de ROM). Avec les langages haut niveau, on a plutôt l'habitude d'utiliser
la notation décimale pour les nombres, en assembleur nous allons
plus souvent avoir à faire à l'héxadécimal
et au binaire. L'héxadécimal est en base 16, c'est à dire
qu'on compte de 0 à F: 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F. Le binaire est en base 2, on ne compte qu'avec des 0 et des 1
(les bits). Si on ajoute deux 1 ensemble, ça fait une retenue sur le bit de gauche. Pour convertir entre le binaire et l'héxadécimal, on peut
séparer les nombres en deux, puisque 4 bits correpondent
à un chiffre en héxadécimal. Comme en binaire toutes les informations doivent être codés avec des 0 et des 1, on ne peut pas avoir de signe. Si on veut utiliser des nombres signés, on va devoir utiliser un bit supplémentaire qui indiquera si les suivants représentent un nombre positif ou négatif. On utilise pour ça le bit de poids fort (ou MSB). 1 indiquera que le nombre est : négatif, et 0 qu'il est positif. Avec 8 bits, on peut donc soit compter de 0 à 255, soit de -128 à 127 (toujours 256 possibilités dans les deux cas).
Vous remarquerez que sur un octet, si on fait la soustraction $00-1, on boucle sur $FF, qui veut bien dire -1. Tel quel, si on regarde un nombre binaire, rien n'indique qu'il est signé
ou non. C'est au programmeur de savoir ce qu'il fait, ou au reverse-engineer de deviner !
![]() Le CPU, GPU, APU et le Bootstrap ROM de la Gameboy Pocket dans le même circuit intégré. On ne peut pas programmer sur une machine à un niveau si bas si on ne sait pas de quoi elle est faite et comment elle fonctionne. L'architecture de la Gameboy reste globalement simple, on retrouve les élements de base d'un ordinateur complet: un processeur (CPU), de la mémoire de travail (RAM), de la mémoire vidéo (VRAM), un processeur vidéo (GPU), un générateur de son (APU), des entrées/sorties (I/O) et un ou plusieurs ROMs (cartouche). Passons ces élements en revue: -Le CPU: Dans la Gameboy, il est très similaire au Z80.
Il était très en vogue dans les années 80 et toujours
au début des années 90. Comme beaucoup de processeurs, il possède
un bus de données et un bus d'addresse. Un bus est
un groupe de connections electriques qui permettent d'accèder
à d'autres composants en spécifiant une adresse et
en écrivant ou lisant une valeur. -Le ROM: Une mémoire morte (qui ne s'efface théoriquement
jamais), qui contient le programme que le CPU va lire et executer
afin de commander tous les autres composants. C'est le composant principal
d'une cartouche, le jeu est stocké à l'intérieur. -La RAM: Une mémoire volatile (qui s'efface quand
on éteint la console), qui permet au programme de stocker et
relire des nombres pour réaliser des calculs et differents traitements.
On peut y mettre ce qu'on veut à n'importe quel moment. -La VRAM (Video RAM): Une mémoire similaire à la RAM, mais qui est utilisée pour l'affichage vidéo. On ne peut y accéder (lire et écrire dedans), que quand le GPU n'y touche pas. Les graphismes et les positions des objets à l'écran y sont stockés. Sa taille est de 8ko également (16ko sur Gameboy Color). -Le GPU (Graphics Processing Unit): Physiquement intégré avec le CPU dans le même circuit intégré, il a été conçu spécifiquement par Nintendo. C'est lui qui se charge de créer une image d'après les données contenues dans la VRAM et de l'envoyer à l'écran LCD. Il possède des registres (petites cases mémoire) dans lesquels on écrit pour le configurer. -L'APU (Audio Processing Unit): Comme le GPU, il est aussi intégré dans le même circuit intégré et a été conçu spécialement pour la GameBoy. Il ne possède pas sa propre RAM, mais uniquement des registres qui vont lui indiquer quel son jouer à quel moment. Il possède 4 canaux, chacun ayant ses caractèristiques et chacun pouvant jouer un son à la fois. -Les entrées/sorties: Ou I/O pour "Input/Output", sont les moyens pour la Gameboy de savoir ce qui se passe dans le monde exterieur. En l'occurence, ce sont les boutons, le port link et le port infrarouge. Parfois, les cartouches apportent des I/O supplémentaires, comme par exemple celles qui ont un vibreur ou un lecteur de code barres, mais ce sont des cas spéciaux. On a vu que le processeur était capable d'adresser sur 64ko maximum. Dans ces 64ko sont réparties des plages attribuées aux différents composants. L'organisation de ces plages est appellé Memory Map ("carte de la mémoire"). Sur GameBoy elle est constituée ainsi:
Les 8ko de RAM sont répétés (mirrored), c'est à dire que la valeur qui est à $C000, se retrouvera automatiquement à $E000, de même pour celle à $C001 et $E001, et ainsi de suite... C'est une situation qui se produit lorsqu'une zone de la plage mémoire adressable n'est pas utilisée. Il vaut mieux ne pas y prêter attention et n'utiliser que la zone $C000 à $DFFF. L'OAM (Object Attribute Memory) peut être vue comme une petite extension de la VRAM. Elle ne concerne que les sprites (abordés plus tard). Les 128 octets de HRAM sont utilisables comme de la RAM, mais sont souvent réservés au stack (voir plus bas) ou à la routine DMA (pas abordé ici).
Ce qui est bien avec les machines qui datent, c'est qu'on a généralement pas besoin d'un IDE de 500Mo avec des licences hors de prix pour coder dessus, car il y a déjà des amateurs qui ont écrit des outils gratuits et très fiables qui nous permettent aujourd'hui d'écrire nos programmes librement sans avoir à passer par Nintendo. A l'époque, si on voulait publier le jeu et faire produire des cartouches, il fallait une licence très couteuse et leur payer des royalties. Des documents très détaillés concernant tous les aspects techniques de la GameBoy ont aussi vu le jour il y a quelques années sur le net, ils sont communément appellé les "pandocs" (http://nocash.emubase.de/pandocs.htm), ils sont absolument indispensables pour programmer sur GameBoy. Ils décrivent tous les registres, du GPU à l'APU en passant par le port link et les boutons. Pour écrire notre code, on va avoir besoin d'un editeur de texte, le bloc-notes de Windows suffit largement. Si vous voulez de la coloration syntaxique avec des onglets, des raccourcis, de la gestion de projet et tout ça, je conseillerai un editeur gratuit qui s'appelle Context. On va aussi avoir besoin d'un petit editeur graphique de bonne renommée qui s'appelle Tile Layer Pro, qui est capable de gérer le format un peu spécial des graphismes. Il est bien sur possible de dessiner dans n'importe quel logiciel, mais il faut ensuite les convertir au format que le GPU de la GameBoy demande. Même si le programme pourra fonctionner sur une vraie GameBoy, on va faire nos tests sur PC avec l'émulateur BGB, qui malgre que son auteur soit une petite raclure, est incontestablement le meilleur émulateur GameBoy après NO$GMB. Et finalement, l'assembleur que nous allons utiliser: WLA-DX. Il est gratuit et tourne sous Windows et Linux, il est rapide, super fiable, et surtout, il est fait spécialement pour le processeur de la GameBoy. Vous pouvez le télécharger ici (en local, zip contenant seulement le nécessaire).
Pour faire son travail, le CPU possède ce qu'on appelle un jeu d'instructions et des registres. Le jeu d'instructions est une liste d'opérations de base
que le CPU sait faire, et qui serviront à constituer notre programme.
La plupart des instructions nécessitent une ou deux operandes,
qui précisent quelles données utiliser pour réaliser
l'opération. Un jeu complet contient typiquement des dizaines de milliers d'instructions qui ont pour la plupart été tapées une par une à la main. Le CPU lit ces instructions, réalise l'opération demandee puis passe à la suivante. L'integralite du jeu ne repose que sur ce simple principe, repete tres rapidement. Quand on écrit du code en assembleur, on tape la mnemonique
de l'instruction, c'est à dire son "nom". Il est en général
composé de 2 à 4 lettres majuscules (par exemple:
ADD, LD, SUB...). Chacune de ces mnemoniques correspond à une opcode,
qui est leur représentation numérique . La plupart
des opcodes font un octet, mais certaines en font deux. Les registres de travail sont des cases mémoires 8 bits dans le CPU, qui servent en quelque sorte de variables "fixes", ils peuvent prendre n'importe quelle valeur mais ils ne peuvent pas être renommés. Ils sont au nombre de 7 et s'appellent A, B, C, D, E, H, et L. Ils peuvent parfois être combinés ensembles pour former des registres 16 bits: AF, BC, DE et HL (notez que F est un registre spécial). On ne peut pas les combiner autrement, et seules certaines instructions acceptent ces paires. Trois registres spéciaux sont également présents:
Organisation des bits du registre F:
Les instructions ne peuvent pas toutes fonctionner avec les mêmes operandes. Certaines ne peuvent utiliser que le registre A par exemple, d'autres ne peuvent pas utiliser les paires de registres... Il y a donc plusieurs façons de fournir des arguments aux instructions, ces façons s'appellent "mode d'adressage". Voici les 4 utilisés par le CPU de la GameBoy (le Z80 original en avait bien plus): -Direct: on donne une valeur fixe (8 bits). -Indirect: on donne une adresse, la valeur située à
cette addresse sera utilisée. Comme l'operande est un pointeur,
on met des parenthèses autour. -Registre: on donne le nom d'un registre, la valeur qu'il contient
sera utilisée. -Registre indirect: on donne le nom d'un registre qui contient
une adresse, la valeur située à cette addresse sera
utilisée. Comme l'argument est un pointeur, on met des parenthèses autour.
Presque toutes les instructions n'acceptent que le registre HL pour ce
mode.
Le pointeur de stack (registre SP) est typiquement initialisé
à l'adresse de fin de la HRAM (à l'adresse $FFF4, comme
Nintendo l'exigeait). Exemple: Initialiser SP SP:$DFFF Empiler 5 SP:$DFFE ($DFFF)=5 Empiler 12 SP:$DFFD ($DFFE)=12 Dépiler, donne 12 SP:$DFFE Empiler 3 SP:$DFFD ($DFFE)=3 Dépiler, donne 3 SP:$DFFE Dépiler, donne 5 SP:$DFFF Comme le registre SP "descend" et n'est pas limité
(il peut couvrir les 64ko adressables en entier), il est possible de remplir
accidentellement la RAM, d'écraser des données
qui y étaient déjà placées, ou meme d'aller ecrire au GPU sans le vouloir ! Finalement, un dernier système du CPU à voir: les interruptions. Les interruptions sont des sauts automatiques dans le programme
(PC est modifié) lorsqu'un évenement externe se produit.
Sur la GameBoy, il a plusieurs interruptions différentes mais nous
n'allons en utiliser qu'une seule: l'interruption vblank. L'interruption vblank se produit a chaque fois que le GPU termine de rafraichir l'ecran (c'est a dire environ 60 fois par seconde).
Pour afficher des choses sur l'écran de 160*144 pixels,
on dispose de plusieurs moyens.
Ces tiles sont donc constitués de 64 pixels, qui chacun peuvent prendre l'une de 4 couleurs (4 niveaux de gris sur la GameBoy DMG). Comme 4 choix ne demandent que 2 bits (00, 01, 10 et 11), on peut
mettre 4 pixels dans un même octet. Un tile pèse donc
8*8/4 = 16 octets. Par exemple, si on a dans la VRAM les valeurs $0B puis $23, ceci donne en binaire: 00001011 Si on lit verticalement de bas en haut, on a les paires 00,00,10,00,01,00,11,11, soit les couleurs 0,0,2,0,1,0,3,3. Le GPU fournit trois moyens d'afficher ces tiles: sur le background, sur la window, ou dans des sprites.
Ca y est, on peut s'y mettre ! On va ouvrir l'éditeur de texte et enregistrer le fichier avec l'extension .s, c'est pas nécessaire mais c'est la convention. Ce qu'il faut faire avant tout, c'est renseigner WLA-DX
sur comment on veut que le programme soit assemblé. Ca concerne
surtout les détails propres aux cartouches GameBoy et la
taille du programme final. Pour renseigner l'assembleur sur ces choses, on utilise des directives. C'est des mots clés qui commencent par un point, et qui ne seront pas retranscrits pas dans le programme final. C'est un peu comme des arguments en ligne de commande sauf qu'ils sont dans le fichier source. Toutes ces directives sont expliquées dans la doc de WLA-DX. On va d'abord spécifier les directives qui vont dire à
WLA-DX comment générer le header du ROM. .ROMDMG
.NAME "PONGDEMO" .CARTRIDGETYPE 0 .RAMSIZE 0 .COMPUTEGBCHECKSUM .COMPUTEGBCOMPLEMENTCHECK .LICENSEECODENEW "00" .EMPTYFILL $00 .ROMDMG indique que notre programme n'est pas spécialement
fait pour la Gameboy Color. "DMG" veut dire "Dot Matrix Game" et etait le nom de code pour la premiere
GameBoy, la "brique". Finalement, on décrit comment les zones du ROM vont être organisées. .MEMORYMAP
SLOTSIZE $4000 DEFAULTSLOT 0 SLOT 0 $0000 SLOT 1 $4000 .ENDME .ROMBANKSIZE $4000 .ROMBANKS 2 .MEMORYMAP et .ENDME délimitent le bloc d'info qui décrit
l'organisation du ROM. Ici on va avoir qu'une zone de $8000 octets
de long, c'est à dire 32ko. .BANK 0 SLOT
0
On se place au tout dans les premiers 16ko du ROM. .ENUM $C000
RaquetteY DB BalleX DB BalleY DB VitX DB VitY DB .ENDE Comme 7 registres du CPU ne vont pas suffir pour stocker les données
de notre jeu, on va avoir besoin de variables. On indique donc
où elles vont être placées en RAM. Maintenant que les directives sont inscrites, on peut passer au code en lui-même. La première chose qu'on va définir, c'est quel morceau de code ("routine") appeller lorsque l'interruption vblank est déclenchée. Pour celà, on dit à WLA-DX de placer un saut vers un label (pointeur vers du code qu'on écrira plus tard) à l'adresse de l'interruption, soit $40 en utilisant la directive .ORG. L'instruction CALL place PC dans le stack et saute vers la routine nommée "VBlank". Une fois cette routine executée, PC revient ici et passe à l'instruction RETI, qui indique que le traitement de l'interruption est terminé et que le programme peut reprendre son cours normal (PC est depile). A noter que pour retourner d'une interruption, il faut utiliser l'instruction RETI et non RET. Quand une interruption est declenchee, celles-ci sont automatiquement desactivees (pour empecher qu'une autre se produise trop rapidement et provoque du "nesting"). RETI permet donc de les reactiver en combinant les instruction RET et EI (Enable Interrupts). .ORG $0040
call VBlank reti Ensuite, on va se placer à l'adresse $100. C'est là que PC est initialisé quand le jeu démarre (et non pas $0000). Nintendo conseillent de mettre une instruction NOP (No OPeration, ne rien faire), avant tout. Puis un saut vers notre code: à un label qu'on appelle "start". JP est different de CALL: il ne fait qu'un saut, sans empiler PC. .ORG $0100
nop jp start A l'adresse $104, on va devoir placer une suite de valeurs fixes qui correpondent en fait aux données graphiques du logo Nintendo affiché lorsque la GameBoy est allumée. Si ces données ne sont pas correctes, le jeu ne se lancera pas. Pour définir des valeurs brutes qui seront simplement insérées par WLA-DX, on utilise à nouveau la directive .DB. Notez que le format de ces donnees graphiques ne sont pas les memes que celles des tiles. Elles sont uniquement destinees au bootstrap. .ORG $0104
On peut désormais passer au code d'initialisation du jeu. On se place à l'adresse $150 (après le header qui, rappellez-vous, sera inséré automatiquement dans les parages par WLA-DX grâce aux directives qu'on lui a spécifié precedemment). A l'adresse $150 on est tranquille, on ne va pas écrire par dessus. Notez que tout ce qui suit un point-virgule est un commentaire et sera ignore par WLA-DX. Ils sont presents pour detailler ce que chaque instruction fait dans ce tuto. .org $0150 On définit le label "start", auquel on va sauter depuis
l'adresse $100 (rappellez vous du "JP start"). xor a ;A=0
ldh ($26),a ;($FF26)=A XOR A est une petite astuce qui consiste à faire l'opération
binaire XOR de A sur A lui-même, ce qui a pour conséquence
de mettre A à 0. On l'utilise souvent car c'est plus
rapide que de faire LD A,$00. waitvbl: Pour initialiser la VRAM sans afficher de la merde à l'écran (le GPU tourne deja !),
on va l'éteindre. C'est à dire indiquer au GPU de
ne rien afficher du tout. Avant d'éteindre l'écran, il
faut attendre d'être dans le vblank (une courte période
pendant laquelle l'écran n'est pas rafraichi). Pour celà,
on va attendre que le GPU finisse de rafraîchir la ligne 144
de l'écran, c'est à dire qu'il passe à la ligne
"virtuelle" 145, qui n'est pas physiquement sur l'écran physique. Nintendo précisent bien qu'il ne faut pas éteindre l'écran n'importe quand car on risque de le déteriorer physiquement. Oui, on peut theoriquement faire un ROM qui nique l'ecran, doucement mais surement ! xor a ;A=0 Ensuite, on met le registre $FF40 à 0, le bit 7 étant à 0, l'écran est éteint (oui il faut encore aller voir les Pandocs !). Le reste des bits, on s'en fout pour l'instant, on laisse tout à 0. ld b,3*8*2 ;B=48 Pour celà, on va devoir copier les données graphiques de ces tiles depuis notre ROM, vers la VRAM. On veut copier 3 tiles, donc 3 blocs de 8 lignes de pixels. Comme chaque ligne de 8 pixels demande deux octets, on va devoir copier 3*8*2 = 48 octets. Pour réaliser une boucle, on charge le registre B avec le nombre
d'itérations nécessaires. On utilise DE comme pointeur
vers les données dans notre ROM, et HL comme pointeur vers
la VRAM. $8000 est la toute première adresse de la VRAM, elle
correspond au tile numéro zéro, c'est comme ça,
ça bouge pas. ld de,32*32 ;DE=1024 On veut ensuite nettoyer le background. Comme le bootstrap a dessine le logo Nintendo dessus, il faut le virer. De même qu'avant, une boucle est utilisée pour remplir
ces 32*32 cases. Bien sûr, il n'est pas nécessaire de remplir
toute la map puisque certaines de ces zones ne seront pas affichées
à l'écran, mais c'est plus simple de faire ainsi et de la remplir
complètement. Une limitation du Z80 fait que le flag Z (zéro) n'est pas mis à 1 si le résultat d'un DEC sur un registre 16 bits est égal à zéro (c'est con, il faut le savoir). On doit donc ruser et utiliser l'opération logique OR pour mélanger les registres D et E dans le registre A, pour voir si tous le bits sont à zéro. On peut ensuite faire un JR NZ comme avant. ld hl,$FE00 ;HL=$FE00 Encore une boucle de nettoyage, cette fois ci dans
l'OAM (addresse $FE00). La zone de définition des positions
et des attributs des sprites est mise à zéro. Attention, remarquez qu'ici, "LD (HL)" puis "INC L" sont utilisés
au lieu d'un "LDI (HL)" comme précedemment. xor a ;A=0 Retour aux Pandocs pour lire que les registres $FF42 et $FF43 correspondent aux valeurs de scroll X et Y du background (défilement en nombre de pixels depuis la gauche et le haut de l'écran respectivement). Pour caler le background en haut à gauche, on met ces deux registres à zéro. On prépare ensuite les sprites. On va en utiliser 5 en tout: 4 pour la
raquette (disposés de haut en bas), et un pour la balle. La raquette
va donc faire 8*32 pixels, et la balle 8*8. ld hl,$FE00 ;HL=$FE00 Le 5ème sprite (celui de la balle) est préparé à part, en dehors de la boucle. Il utilisera le tile numéro 2 et sera placé vers le centre de l'écran. ld (hl),$80 ;(HL)=$80 OAM sprite
Y On initialise nos variables. La position de la raquette est fixée
à $20 (32) pixels à partir du haut de l'écran.
La balle est mise à X = $80, Y = $80. Et les vitesses sont toutes
les deux positives (déplacement en diagonale vers le bas et la droite). ld a,$20 On arrive maintenant au stade où l'écran pour être réactivé. Juste avant, on initialise la palette avec les
"couleurs" par défaut. Cette palette sert à
définir quel chiffre correspond à quelle couleur. L'utilité
de cette palette est de pouvoir faire des effets vidéos en changeant
toutes les "couleurs" de l'écran sans avoir à
modifier les données graphiques (clignotements, flashs...). Finalement, on active l'écran en écrivant de nouveau dans le registre $FF40. Les bits indiquent que le background, les sprites et l'affichage sont activés, et que l'adresse des graphismes du tile numéro 0 seront placés à partir de $8000 (voir les Pandocs encore). ld a,%11100100 ;11=Noir 10=Gris
foncé 01=Gris clair 00=Blanc/transparent ld a,%00010000 ; Interruptions
VBlank activées ei ;Activer la prise en charge des interruptions loop:
VBlank: La première chose qu'on veut savoir pour mettre à jour
l'affichage, c'est quelles touches sont appuyés. Ici on ne va
s'intéresser qu'aux touches haut et bas de la croix directionelle, pour deplacer la raquette. A noter que certaines version des Pandocs comportent une erreur: pour choisir le groupe des touches de direction, il faut mettre le bit 5 du registre $FF00 à 1, et non pas le bit 4. ld a,%00100000 ; Selection
touches de direction L'état des touches se trouve maintenant dans les 4 premiers bits du registre B dans l'ordre suivant (bits 3 a 0): Bas, Haut, Gauche, Droite. Lorsque la touche est appuyée, le bit correspondant est mis à zéro, sinon il est à 1 (logique inverse). On va donc tester les bits 3 et 2 pour savoir si les touches Bas et Haut sont appuyées ou non. Si l'une ou l'autre le sont, la position de la raquette sera décrémentée ou incrémentée en fonction. bit $3,b ;Tester bit 3 de
B (croix directionnelle bas) L'instruction BIT permet de tester l'état d'un bit d'un registre.
On utilise ensuite un saut conditionnel pour sauter ou non le bloc de
code qui déplace la raquette (label "nod" pour "no down"). bit $2,b ;Tester bit 2 de
B (croix directionnelle haut) La même chose doit se dérouler pour la touche Haut. Cette
fois la position est décrémentée et la limite est
le haut de l'écran. ld hl,$FE00 ;Mise à
jour de la position de la raquette en OAM
Pour les 4 bordures de l'écran, pas de problème: ça sera 0 et 160 à l'horizontale, 0 et 144 à la verticale. Pour la balle, on a besoin de ses 4 bords: sa position X et Y pour ses bords gauche et haut, et sa position X+8 et Y+8 pour les bords bas et droit. Pour la raquette, on va utiliser de même sa position Y et Y+32 (4*8 = 32 pixels de hauteur) pour ses bords haut et bas, et sa position X+8 pour son bord droit (le bord gauche ne servira pas).
Il faut aussi savoir que les coordonnées des sprites dans l'OAM
sont décalés de 8 pixels sur X et 16 pixels sur Y. On ajustera la position des collisions selon ces décalages...
On va d'abord s'occuper du plus simple: les rebonds sur les bords de
l'écran. Ca sera deux blocs de code assez similaires: l'un pour
les collisions horizontales, l'autre pour les collisions verticales. ld hl,BalleX ;Mise à jour
position X de la balle selon la vitesse horizontale Notez bien que la position n'est ici pas encore mise à jour, elle demeure dans le registre A. La balle à l'écran n'est pas encore déplacée. C'est donc le moment de detecter les collisions. D'abord le bord droit: cp 160 La nouvelle position est comparée à 160 (la position du
bord droit de l'écran + la largeur de la balle - le décalage
de 8 pixels). Un "JR C" saute au label "nocxr" si
la position est inférieure: le code de rebond est esquivé.
Si la position est égale ou supérieure, le code suivant
est executé. Un CALL appelle la routine "lowbeep", qui
sera écrite plus tard et qui sert à jouer un bip grave. cp 2 La position est ensuite comparée à 2 (la position du bord gauche de l'écran + la vitesse max). De la même façon, un saut conditionnel permet de ne pas faire de rebond si la position est supérieure à 2. Si un rebond se produit, le même bip est joué, la vitesse est fixée à 2, et la position fixée à 8 (soit 0 en réalité, à cause du décalage de 8 pixels). ld (hl),a ;Fixage position X Enfin, la position mise à jour est inscrite dans la variable BalleX,
toujours pointée par le registre HL. ld hl,BalleY ;Mise à jour
position Y de la balle selon la vitesse verticale Le bord bas est testé (144 - la hauteur de la balle + le décalage de 16 pixels). cp 144+8 Puis le bord haut (le décalage de 16 pixels) cp 16
On va d'abord voir si la position X de la balle est entre 10 et 16. On
laisse donc 6 pixels de marge sur le bord droit de la raquette pour "attraper"
la balle. ld a,(BalleX) Ensuite, on regarde si la vitesse est négative pour savoir si la balle vient de devant ou derrière la raquette. Si elle vient de derrière, il n'y a pas de rebond: comme dit precedemment, la balle passera simplement au travers de la raquette. ld a,(VitX) Le troisième et dernier test consiste à savoir si la balle est alignée verticalement avec la raquette. Pour celà on va voir si la position du bord bas de la balle est supérieure à celle du bord haut de la raquette, et si celle du bord haut de la balle est inférieure à celle du bord bas de la raquette. On teste d'abord la limite haute (bas de la balle, haut de la raquette): ld a,(BalleY) Si ce test ne passe pas, c'est que la balle se situe forcément dans cette zone Pour prendre notre décision, on finit pas le test de la limite basse (haut de la balle, bas de la raquette):
ld hl,BalleY
Si tout ces tests sont passés sans que le programme ne saute au label "nopaddle", il y a un rebond sur la raquette. On joue donc un son plus aigu avec un CALL vers une autre routine, et on inverse la vitesse horizontale.
call hibeep ;Rebond paddle, bip aigu Quoi qu'il arrive (rebonds ou pas), la position de la balle peut maintenant
être mise à jour au niveau de l'OAM pour que le sprite
soit déplacé à l'écran. nopaddle: ld hl,$FE10 ; $FE00 + 4*4
(Sprite numéro 3) pop hl ;Dépiler HL
lowbeep: hibeep:
setsnd: .ORG $800
Assembler la source en lancant "make-gb bpong" pour avoir le ROM bpong.gb.
On va pouvoir observer le code qu'on vient d'écrire sous forme graphique, c'est cet amas de pixels qui semble aléaoire au tout début du fichier. Comme Tile Layer n'a aucun moyen de savoir si les nombres qu'il lit depuis le fichier sont du programme ou des graphismes, il se content d'afficher tout.
Une fois qu'on est à $800, on peut dessiner nos 3 tiles dans l'ordre.
Il ne reste plus qu'à sauvegarder le fichier et à le lancer dans BGB. Voilà, le jeu est terminé ! |