Architecture générale
Le jeu à proprement parler tient dans un code assembleur de 2Ko. Il utilise quelques tables de données pour accélérer son fonctionnement. La partie consacrée à la configuration du jeu est réalisée en externals et est plus volumineuse que le code lui-même !!! Le reste, c'est 1Ko de tableaux et des sprites.
J'ai tout programmé sur la HP48, car à l'époque je n'avais pas d'émulateur pour PC.
Comment dessiner (rapidement) une ligne horizontale
Tout dans babal est une question d'affichage... Faire sauter la boule, c'est du bidon, mais dessiner la piste c'est nettement plus pénible.
Il fallait pour obtenir un affichage d'une rapidité correcte, utiliser les particularités de la HP48 au mieux, en particulier au niveau des accès mémoire. Et un accès mémoire sur HP48, c'est super pénible, parce que les pointeurs D0 et D1 ils sont pas super agréables à manipuler. En plus, la HP48G annonce 4Mhz d'horloge interne, c'est sans compter la bonne vingtaine de tops horloge nécessaires à un pauvre accès mémoire. Par contre, il y a un aspect très positif dans l'architecture du microprocesseur SATURN, c'est qu'il possède des registres énormes de 64 bits. Et quand on écrit 64 bits en mémoire ça prend à peine plus de temps que d'écrire un quartet.
C'est ainsi que m'est venue une idée toute bête, qui consiste à afficher ligne par ligne les dalles de la piste. Et vous constaterez que la dalle qui à l'écran apparaît comme la plus large a une taille de 60 ou 61 pixels à la base. Eh oui, babal utilise un tableau de données où sont stockés tous les binaires de 64 bits du type : 0...0001 0...0011 0...0111 0...1111 etc... Alors pour dessiner un ligne de n pixels de large, la procédure devient simple et rapide :
- Charger dans un registre le n-ième élément de ce tableau
- Le multiplier par 1, 2, 4, ou 8 selon que la ligne à afficher commence par un pixel situé à une abscisse respectivement égale à 0,1,2 ou 4.
- Faire un ou logique avec la mémoire vidéo.
Quand faut-il afficher une dalle
C'est bête comme question mais il faut pouvoir y répondre, et vite ! Parce que la question se pose 64*8=512 fois par affichage.
On pourrait se demander quelle sont les dalles visibles puis les afficher, dalle par dalle. On pourrait. Mais moi j'ai fait autrement. Je balaye l'écran ligne par ligne et effectue pour chaque ligne les opérations suivantes :
1)
Trouver à quelle ligne du grob représentant le tableau correspond la ligne écran qui est en cours d'affichage. C'est pas si dur que ça, il suffit de se servir du fait que la piste ne bouge pas verticalement. C'est la boule qui saute, et pas la piste qui descend. Donc l'écran reste fixe par rapport au plan qui contient la piste. On se contente de translater la piste dans ce plan. Et surtout on ne lui fait pas subir de rotations ! Car ainsi elle est toujours parfaitement perpendiculaire à l'écran. Donc à une ligne écran d'ordonnée h correspond une droite de position y dans le plan de la piste, perpendiculaire à l'axe de la piste. Il suffit de pré-calculer un tableau contenant les 64 positions y correspondant à chaque ligne de l'écran. Et ce y est calculé dans une unité judicieusement choisie qu'on appellera picomufle, et le picomufle possède la propriété suivante (très intéressante): "une dalle mesure 256 picomufles". Cette merveilleuse unité sert aussi pour le calcul de la position de la balle. Et donc en additionnant la position de la balle à la position y correspondant à une ligne h puis en divisant le tout par 256 on obtient miraculeusement l'indice de la ligne du grob qui nous intéresse.
2)
Stocker dans un registre la ligne du grob qu'on a récupérée avec le procédé décrit ci-dessus.
3)
Effectuer un test bête comme "hello world" sur chaque bit de cet octet pour savoir si oui ou non on doit afficher quelque chose dans cette ligne.
Remarque : il est possible qu'une dalle mesure 1024 ou 4096 picomufles, ça marche aussi (qui l'eût cru...) !
Où afficher la dalle
C'est très joli tout ça, on parle de lignes à l'écran, de savoir quand afficher ces lignes, mais la question est de savoir où les afficher. Et plus précisément à partir de quelle colonne écran commence une ligne de dalle, et où elle se termine.
C'est assez simple, me direz-vous, les bords des dalles sont portés par des fuyantes qui convergent toutes en un point, le point de fuite, situé sur la ligne d'horizon. Y'a pas besoin d'avoir inventé l'eau chaude pour savoir ça. Eh bien, il suffit donc de les calculer, ces fuyantes. Et un moyen de les calculer, c'est de partir du point de fuite, et de calculer de proche en proche, pour chaque ligne écran d'ordonnée y, l'abscisse x à laquelle la fuyante intercepte l'axe d'ordonnée y. Et pour un tel calcul, il suffit de connaître la composante x du vecteur qui permet de passer du point d'intersection de la fuyante avec l'axe d'ordonnée y au point d'intersection de la fuyante avec l'axe d'ordonnée y+1. Dans le cas de babal, le point de fuite est situe 21 pixels au-dessus de l'écran. Donc il suffit de multiplier l'abscisse x de ce vecteur par 21, de lui ajouter 65 parce que c'est la colonne du milieu de l'écran et on obtient le point de départ de notre jolie fuyante. Ensuite, pour calculer les points de passage de la fuyante au niveau des lignes suivantes il suffit d'ajouter l'abscisse x de notre vecteur magique à la dernière valeur calculée. Et on répète ça pour les 9 fuyantes.
Il va de soi que les vecteurs sont précalculés pour les 9 fuyantes, et stockés dans un tableau. Et pour déplacer la piste vers la gauche ou vers la droite, il suffit, ô miracle de simplicité, d'ajouter un ch'tit offset à ces vecteurs. Par exemple, si les offsets de base sont : -3.5 -2.5 -1.5 -0.5 +0.5 +1.5 +2.5 +3.5 +4.5 alors en ajoutant 1 à tous ces offsets on se déplace d'une case sur le cote car le tableau devient -2.5 -1.5 -0.5 +0.5 +1.5 +2.5 +3.5 +4.5 +5.5 Cela revient en fait à remplacer la fuyante n par la fuyante n+1...
Il faut noter que l'absicsse x du vecteur a souvent des valeurs du type 1,53 pixels. On utilise donc des nombres avec virgule fixe et tout va très vite. Dans babal, la précision permet d'ajuster l'affichage au quart de pixel près en bas de l'écran. C'est juste ce qu'il faut. Tombera, tombera pas Pour savoir si la balle est sur une dalle ou si elle doit tomber, le test est d'une simplicité désarmante ! Il se contente de regarder à l'écran, directement, l'état de 2 pixels dont j'ai décidé qu'ils avaient une tête à correspondre au point de contact entre la piste et la balle. Si un des 2 pixels au moins est noirci, alors on n'est pas tombé, sinon AAAARRRGHH...
Page flipping
Tous les jeux d'action (ou presque) utilisent ce procédé archi-classique, qui consiste à dessiner pépère son écran dans une zone mémoire non visible et à afficher tout cela d'un coup, ce qui évite des clignotements. Je dois être bête, mais je n'ai pas réussi à me servir du swap écran proposé par la HP48, en particulier je n'arrivais pas à synchroniser le swap avec le rafraîchissement physique de l'écran. Pourtant je disposais de toutes les docs ! Donc j'ai opté pour un recopiage bien bourrin de mon écran virtuel dans la mémoire vidéo, et je ne me synchronise avec rien du tout et le jeu ne clignote pas. D'autant qu'un recopiage de mémoire vidéo avec des registres de 64-bits ça va très vite. Il me semble même que ça va presque plus vite que d'attendre le rafraîchissement, mais là je m'avance peut-être un peu. Et puis ça m'a permis d'implémenter une inversion vidéo à peu de frais.
Optimisations
C'est pas là-dessus que j'ai perdu mon temps. Le code est plutôt sale. Moi-même, 3 ans après, je n'y comprends plus grand chose. Ca m'évitera de perdre du temps à optimiser mes routines ! A mon avis, en se cassant la tête pendant des heures on pourrait peut-être gagner 30% de rapidité en se servant du même algorithme. Avec un algorithme différent on peut peut-être faire encore mieux... Mais bon, moi j'étais déjà content de faire de la 3D en temps réel sans aucune multiplication ni division. A part une multiplication par une constante : 21. Mais là c'est pas très dur, il suffit de calculer y=x+4x+16x. C'est la seule routine optimisée de tout le jeu :-)